@thomasfosterau/pulpit 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1271 -1
- package/dist/blobs.d.ts +249 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +193 -0
- package/dist/blobs.js.map +1 -0
- package/dist/budgets.d.ts +255 -0
- package/dist/budgets.d.ts.map +1 -0
- package/dist/budgets.js +211 -0
- package/dist/budgets.js.map +1 -0
- package/dist/cache.d.ts +154 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +118 -0
- package/dist/cache.js.map +1 -0
- package/dist/collections.d.ts +242 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +215 -0
- package/dist/collections.js.map +1 -0
- package/dist/comments.d.ts +212 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +192 -0
- package/dist/comments.js.map +1 -0
- package/dist/content.d.ts +231 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +204 -0
- package/dist/content.js.map +1 -0
- package/dist/documents.d.ts +184 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +190 -0
- package/dist/documents.js.map +1 -0
- package/dist/edges.d.ts +645 -0
- package/dist/edges.d.ts.map +1 -0
- package/dist/edges.js +558 -0
- package/dist/edges.js.map +1 -0
- package/dist/filter.d.ts +461 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +387 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/filter-columns.d.ts +8 -0
- package/dist/internal/filter-columns.d.ts.map +1 -0
- package/dist/internal/filter-columns.js +43 -0
- package/dist/internal/filter-columns.js.map +1 -0
- package/dist/internal/list-order.d.ts +26 -0
- package/dist/internal/list-order.d.ts.map +1 -0
- package/dist/internal/list-order.js +92 -0
- package/dist/internal/list-order.js.map +1 -0
- package/dist/internal/owned-columns.d.ts +93 -0
- package/dist/internal/owned-columns.d.ts.map +1 -0
- package/dist/internal/owned-columns.js +98 -0
- package/dist/internal/owned-columns.js.map +1 -0
- package/dist/internal/plain-rows.d.ts +32 -0
- package/dist/internal/plain-rows.d.ts.map +1 -0
- package/dist/internal/plain-rows.js +32 -0
- package/dist/internal/plain-rows.js.map +1 -0
- package/dist/internal/reference-columns.d.ts +80 -0
- package/dist/internal/reference-columns.d.ts.map +1 -0
- package/dist/internal/reference-columns.js +102 -0
- package/dist/internal/reference-columns.js.map +1 -0
- package/dist/moderation.d.ts +392 -0
- package/dist/moderation.d.ts.map +1 -0
- package/dist/moderation.js +375 -0
- package/dist/moderation.js.map +1 -0
- package/dist/paths.d.ts +204 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +165 -0
- package/dist/paths.js.map +1 -0
- package/dist/proposals.d.ts +523 -0
- package/dist/proposals.d.ts.map +1 -0
- package/dist/proposals.js +505 -0
- package/dist/proposals.js.map +1 -0
- package/dist/publication.d.ts +148 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +183 -0
- package/dist/publication.js.map +1 -0
- package/dist/purge.d.ts +224 -0
- package/dist/purge.d.ts.map +1 -0
- package/dist/purge.js +151 -0
- package/dist/purge.js.map +1 -0
- package/dist/reactions.d.ts +246 -0
- package/dist/reactions.d.ts.map +1 -0
- package/dist/reactions.js +183 -0
- package/dist/reactions.js.map +1 -0
- package/dist/registry.d.ts +316 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +305 -0
- package/dist/registry.js.map +1 -0
- package/dist/search.d.ts +366 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +273 -0
- package/dist/search.js.map +1 -0
- package/dist/social.d.ts +311 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +205 -0
- package/dist/social.js.map +1 -0
- package/dist/subtype.d.ts +1613 -0
- package/dist/subtype.d.ts.map +1 -0
- package/dist/subtype.js +1300 -0
- package/dist/subtype.js.map +1 -0
- package/dist/testing.d.ts +675 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +1022 -0
- package/dist/testing.js.map +1 -0
- package/dist/version-chain.d.ts +702 -0
- package/dist/version-chain.d.ts.map +1 -0
- package/dist/version-chain.js +489 -0
- package/dist/version-chain.js.map +1 -0
- package/dist/working-copy.d.ts +116 -0
- package/dist/working-copy.d.ts.map +1 -0
- package/dist/working-copy.js +95 -0
- package/dist/working-copy.js.map +1 -0
- package/package.json +138 -4
- package/src/blobs.ts +384 -0
- package/src/budgets.ts +338 -0
- package/src/cache.ts +218 -0
- package/src/collections.ts +427 -0
- package/src/comments.ts +392 -0
- package/src/content.ts +238 -0
- package/src/documents.ts +284 -0
- package/src/edges.ts +1185 -0
- package/src/filter.ts +523 -0
- package/src/index.ts +153 -0
- package/src/internal/filter-columns.ts +50 -0
- package/src/internal/list-order.ts +117 -0
- package/src/internal/owned-columns.ts +140 -0
- package/src/internal/plain-rows.ts +54 -0
- package/src/internal/reference-columns.ts +156 -0
- package/src/moderation.ts +728 -0
- package/src/paths.ts +267 -0
- package/src/proposals.ts +1014 -0
- package/src/publication.ts +225 -0
- package/src/purge.ts +313 -0
- package/src/reactions.ts +347 -0
- package/src/registry.ts +524 -0
- package/src/search.ts +540 -0
- package/src/social.ts +469 -0
- package/src/subtype.ts +3095 -0
- package/src/testing.ts +1873 -0
- package/src/version-chain.ts +1093 -0
- package/src/working-copy.ts +192 -0
package/dist/social.d.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The social graph** (pulpit#195) — follow, mute and block between
|
|
3
|
+
* accounts, as relationship facts with their own gates, the follower and
|
|
4
|
+
* following lists, a {@link FollowerSeam} built from the graph so
|
|
5
|
+
* `followers` rows work end to end, and an opt-in read filter over the
|
|
6
|
+
* accounts a viewer silenced.
|
|
7
|
+
*
|
|
8
|
+
* The behavioural spec is gardeneria's `social.follows` and its operations
|
|
9
|
+
* (`apps/api/src/operations/follows.ts`), with their tests.
|
|
10
|
+
*
|
|
11
|
+
* ## The model (owner decisions on pulpit#195)
|
|
12
|
+
*
|
|
13
|
+
* - **A relationship is a fact**, keyed `(kind, from, to)` for the kinds
|
|
14
|
+
* `follow`, `mute` and `block`: no history; asserting one twice is a
|
|
15
|
+
* no-op, removing it hard-deletes it. Only the actor writes their own
|
|
16
|
+
* (`from` is always the viewer).
|
|
17
|
+
* - **Following is immediate.** There are no follow requests (a later
|
|
18
|
+
* slice).
|
|
19
|
+
* - **A block suppresses interaction only.** Neither side may follow the
|
|
20
|
+
* other while either blocks the other ({@link InteractionBlocked}), a
|
|
21
|
+
* block removes both accounts' follows of each other, and other modules
|
|
22
|
+
* gate an interaction (a reaction, a reply, a mention) on
|
|
23
|
+
* {@link SocialOps.interactionAllowed}. It hides no content by itself:
|
|
24
|
+
* {@link SocialOps.silencedFilter} is the opt-in read filter that drops
|
|
25
|
+
* the authors a viewer muted or blocked from a feed or a thread.
|
|
26
|
+
* - **A mute is one-way and private**: it only feeds that read filter.
|
|
27
|
+
*
|
|
28
|
+
* Mute and block lists are their owner's alone; the follower and following
|
|
29
|
+
* lists are public, filtered through the application's account visibility
|
|
30
|
+
* ({@link AccountVisibility}).
|
|
31
|
+
*
|
|
32
|
+
* @copyright Thomas Foster
|
|
33
|
+
* @module pulpit/social
|
|
34
|
+
* @since 0.1.0
|
|
35
|
+
*/
|
|
36
|
+
import * as Effect from "effect/Effect";
|
|
37
|
+
import * as HashSet from "effect/HashSet";
|
|
38
|
+
import * as Option from "effect/Option";
|
|
39
|
+
import * as Schema from "effect/Schema";
|
|
40
|
+
import { type ListFilter } from "./filter.ts";
|
|
41
|
+
import { type FollowerSeam, type Page, type Unauthorized, type Viewer } from "./subtype.ts";
|
|
42
|
+
/**
|
|
43
|
+
* The relationship kinds.
|
|
44
|
+
*
|
|
45
|
+
* @since 0.1.0
|
|
46
|
+
*/
|
|
47
|
+
export declare const RelationshipKind: Schema.Literals<readonly ["follow", "mute", "block"]>;
|
|
48
|
+
/**
|
|
49
|
+
* The {@link RelationshipKind} literal union.
|
|
50
|
+
*
|
|
51
|
+
* @since 0.1.0
|
|
52
|
+
*/
|
|
53
|
+
export type RelationshipKind = typeof RelationshipKind.Type;
|
|
54
|
+
/**
|
|
55
|
+
* One stored relationship: `from` follows, mutes or blocks `to`.
|
|
56
|
+
*
|
|
57
|
+
* @since 0.1.0
|
|
58
|
+
*/
|
|
59
|
+
export interface Relationship {
|
|
60
|
+
readonly kind: RelationshipKind;
|
|
61
|
+
readonly from: string;
|
|
62
|
+
readonly to: string;
|
|
63
|
+
readonly at: Date;
|
|
64
|
+
}
|
|
65
|
+
declare const InteractionBlocked_base: Schema.Class<InteractionBlocked, Schema.TaggedStruct<"InteractionBlocked", {
|
|
66
|
+
readonly accountId: Schema.String;
|
|
67
|
+
}>, import("effect/Cause").YieldableError & {
|
|
68
|
+
readonly brand: unique symbol;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Either account blocks the other, so the interaction is refused: the
|
|
72
|
+
* wire's 403. It tells the actor a block exists between the two, as every
|
|
73
|
+
* social network's refusal does.
|
|
74
|
+
*
|
|
75
|
+
* @since 0.1.0
|
|
76
|
+
*/
|
|
77
|
+
export declare class InteractionBlocked extends InteractionBlocked_base {
|
|
78
|
+
}
|
|
79
|
+
declare const SelfRelationship_base: Schema.Class<SelfRelationship, Schema.TaggedStruct<"SelfRelationship", {
|
|
80
|
+
readonly kind: Schema.Literals<readonly ["follow", "mute", "block"]>;
|
|
81
|
+
}>, import("effect/Cause").YieldableError & {
|
|
82
|
+
readonly brand: unique symbol;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* A relationship with oneself: the wire's 422.
|
|
86
|
+
*
|
|
87
|
+
* @since 0.1.0
|
|
88
|
+
*/
|
|
89
|
+
export declare class SelfRelationship extends SelfRelationship_base {
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The relationship table an application supplies (gardeneria's
|
|
93
|
+
* `social.follows`, widened by a `kind` column), keyed
|
|
94
|
+
* `(kind, from_id, to_id)` with both account foreign keys cascading. A store
|
|
95
|
+
* seam: no policy, never opens a transaction, fails only with its `E`.
|
|
96
|
+
* `Following` is the follower seam's set (a subquery in SQL). See the
|
|
97
|
+
* package README → Seams.
|
|
98
|
+
*
|
|
99
|
+
* @since 0.1.0
|
|
100
|
+
*/
|
|
101
|
+
export interface SocialStore<Following, E, R> {
|
|
102
|
+
/** Insert the relationship (`ON CONFLICT DO NOTHING`); whether a row was written. Used for a mute. */
|
|
103
|
+
readonly add: (relationship: Relationship) => Effect.Effect<boolean, E, R>;
|
|
104
|
+
/**
|
|
105
|
+
* Insert a follow unless a block stands between the two accounts, either
|
|
106
|
+
* way: `"added"`, `"present"` (already following) or `"blocked"` (nothing
|
|
107
|
+
* written). The check and the insert must be **one atomic step against
|
|
108
|
+
* {@link SocialStore.addBlock} on the same pair**, so a concurrent block
|
|
109
|
+
* can never leave a follow standing: take a lock on the unordered pair
|
|
110
|
+
* that `addBlock` also takes (`pg_advisory_xact_lock` on it, in the
|
|
111
|
+
* caller's transaction), then `INSERT … SELECT … WHERE NOT EXISTS (a
|
|
112
|
+
* block either way) ON CONFLICT DO NOTHING`.
|
|
113
|
+
*/
|
|
114
|
+
readonly addFollow: (relationship: Relationship) => Effect.Effect<"added" | "present" | "blocked", E, R>;
|
|
115
|
+
/**
|
|
116
|
+
* Insert a block and delete both follows between the two accounts, as one
|
|
117
|
+
* atomic step against {@link SocialStore.addFollow} on the same pair (the
|
|
118
|
+
* same pair lock); whether the block row was written.
|
|
119
|
+
*/
|
|
120
|
+
readonly addBlock: (relationship: Relationship) => Effect.Effect<boolean, E, R>;
|
|
121
|
+
/** Delete the relationship; whether a row went. */
|
|
122
|
+
readonly remove: (kind: RelationshipKind, from: string, to: string) => Effect.Effect<boolean, E, R>;
|
|
123
|
+
/** Whether the relationship is stored. */
|
|
124
|
+
readonly has: (kind: RelationshipKind, from: string, to: string) => Effect.Effect<boolean, E, R>;
|
|
125
|
+
/**
|
|
126
|
+
* One page of an account's relationships of `kind`, newest first
|
|
127
|
+
* (`at DESC`, then the other account's id): by `from` (whom it follows,
|
|
128
|
+
* mutes or blocks) or by `to` (who follows it), with the unpaged total.
|
|
129
|
+
*/
|
|
130
|
+
readonly list: (query: {
|
|
131
|
+
readonly kind: RelationshipKind;
|
|
132
|
+
readonly side: "from" | "to";
|
|
133
|
+
readonly accountId: string;
|
|
134
|
+
readonly limit: number;
|
|
135
|
+
readonly offset: number;
|
|
136
|
+
}) => Effect.Effect<Page<{
|
|
137
|
+
readonly accountId: string;
|
|
138
|
+
readonly at: Date;
|
|
139
|
+
}>, E, R>;
|
|
140
|
+
/** Whether `follower` follows `followed`, as the follower seam asks it (never fails). */
|
|
141
|
+
readonly isFollowing: (follower: string, followed: string) => Effect.Effect<boolean, never, R>;
|
|
142
|
+
/** The accounts `follower` follows, as the list filter's `in` set (a subquery, never an array). */
|
|
143
|
+
readonly followingOf: (follower: string) => Following;
|
|
144
|
+
/** The accounts `viewer` muted or blocked, as the list filter's set (a subquery). */
|
|
145
|
+
readonly silencedBy: (viewer: string) => Following;
|
|
146
|
+
/** Delete every relationship from or to the account (account erasure, pulpit#207); how many went. */
|
|
147
|
+
readonly eraseAccount: (accountId: string) => Effect.Effect<number, E, R>;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Which accounts a viewer may see in a follower or following list: the
|
|
151
|
+
* application's own account visibility (a private profile, a suspended
|
|
152
|
+
* account). Given a page's account ids, the ones to show.
|
|
153
|
+
*
|
|
154
|
+
* @since 0.1.0
|
|
155
|
+
*/
|
|
156
|
+
export interface AccountVisibility<R> {
|
|
157
|
+
readonly visible: (viewer: Viewer, accountIds: ReadonlyArray<string>) => Effect.Effect<HashSet.HashSet<string>, never, R>;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The follower seam a social store fills (`./subtype`'s
|
|
161
|
+
* {@link FollowerSeam}), so a `followers` row is readable by the accounts
|
|
162
|
+
* that follow its owner.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* import { strictEqual } from "node:assert";
|
|
167
|
+
* import * as Effect from "effect/Effect";
|
|
168
|
+
* import { socialFollowers } from "@thomasfosterau/pulpit/social";
|
|
169
|
+
* import { makeInMemorySocialStore } from "@thomasfosterau/pulpit/testing";
|
|
170
|
+
*
|
|
171
|
+
* const store = makeInMemorySocialStore();
|
|
172
|
+
* Effect.runSync(store.add({ kind: "follow", from: "u-1", to: "u-2", at: new Date(0) }));
|
|
173
|
+
* const followers = socialFollowers(store);
|
|
174
|
+
* strictEqual(Effect.runSync(followers.isFollower("u-1", "u-2")), true);
|
|
175
|
+
* strictEqual(Effect.runSync(followers.isFollower("u-2", "u-1")), false);
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* @since 0.1.0
|
|
179
|
+
*/
|
|
180
|
+
export declare const socialFollowers: <Following, E, R>(store: Pick<SocialStore<Following, E, R>, "isFollowing" | "followingOf">) => FollowerSeam<Following, R>;
|
|
181
|
+
/**
|
|
182
|
+
* The social graph's declaration.
|
|
183
|
+
*
|
|
184
|
+
* @since 0.1.0
|
|
185
|
+
*/
|
|
186
|
+
export interface SocialDefinition<Following, E, R> {
|
|
187
|
+
readonly store: SocialStore<Following, E, R>;
|
|
188
|
+
/** The request's viewer. */
|
|
189
|
+
readonly viewer: Effect.Effect<Viewer, never, R>;
|
|
190
|
+
/** Which accounts the follower and following lists show; omitted, every one. */
|
|
191
|
+
readonly accounts?: AccountVisibility<R> | undefined;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* A page request: clamped to `[1, MAX_PAGE_LIMIT]` and an offset `≥ 0`.
|
|
195
|
+
*
|
|
196
|
+
* @since 0.1.0
|
|
197
|
+
*/
|
|
198
|
+
export interface SocialPageRequest {
|
|
199
|
+
readonly limit: number;
|
|
200
|
+
readonly offset: number;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* One account on a list, and when the relationship began.
|
|
204
|
+
*
|
|
205
|
+
* @since 0.1.0
|
|
206
|
+
*/
|
|
207
|
+
export interface Related {
|
|
208
|
+
readonly accountId: string;
|
|
209
|
+
readonly at: Date;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The operations {@link defineSocial} produces. Every write is the
|
|
213
|
+
* viewer's own and fails `Unauthorized` when anonymous; asserting a
|
|
214
|
+
* relationship again, or removing one that isn't there, changes nothing
|
|
215
|
+
* (`changed: false`).
|
|
216
|
+
*
|
|
217
|
+
* @since 0.1.0
|
|
218
|
+
*/
|
|
219
|
+
export interface SocialOps<Following, E, R> {
|
|
220
|
+
/**
|
|
221
|
+
* Follow `accountId`. Fails {@link SelfRelationship} for oneself and
|
|
222
|
+
* {@link InteractionBlocked} when either account blocks the other.
|
|
223
|
+
*/
|
|
224
|
+
readonly follow: (accountId: string) => Effect.Effect<{
|
|
225
|
+
readonly changed: boolean;
|
|
226
|
+
}, Unauthorized | SelfRelationship | InteractionBlocked | E, R>;
|
|
227
|
+
readonly unfollow: (accountId: string) => Effect.Effect<{
|
|
228
|
+
readonly changed: boolean;
|
|
229
|
+
}, Unauthorized | E, R>;
|
|
230
|
+
/** Mute `accountId` (one-way and private). Fails {@link SelfRelationship} for oneself. */
|
|
231
|
+
readonly mute: (accountId: string) => Effect.Effect<{
|
|
232
|
+
readonly changed: boolean;
|
|
233
|
+
}, Unauthorized | SelfRelationship | E, R>;
|
|
234
|
+
readonly unmute: (accountId: string) => Effect.Effect<{
|
|
235
|
+
readonly changed: boolean;
|
|
236
|
+
}, Unauthorized | E, R>;
|
|
237
|
+
/**
|
|
238
|
+
* Block `accountId`: also removes each account's follow of the other.
|
|
239
|
+
* Fails {@link SelfRelationship} for oneself.
|
|
240
|
+
*/
|
|
241
|
+
readonly block: (accountId: string) => Effect.Effect<{
|
|
242
|
+
readonly changed: boolean;
|
|
243
|
+
}, Unauthorized | SelfRelationship | E, R>;
|
|
244
|
+
readonly unblock: (accountId: string) => Effect.Effect<{
|
|
245
|
+
readonly changed: boolean;
|
|
246
|
+
}, Unauthorized | E, R>;
|
|
247
|
+
/**
|
|
248
|
+
* The accounts following `accountId`, newest first, as the application's
|
|
249
|
+
* account visibility shows them. A subject the viewer can't see answers
|
|
250
|
+
* as an account with no followers.
|
|
251
|
+
*/
|
|
252
|
+
readonly followers: (accountId: string, request: SocialPageRequest) => Effect.Effect<RelatedPage, E, R>;
|
|
253
|
+
/** The accounts `accountId` follows, as {@link SocialOps.followers} shows them. */
|
|
254
|
+
readonly following: (accountId: string, request: SocialPageRequest) => Effect.Effect<RelatedPage, E, R>;
|
|
255
|
+
/** The viewer's own mutes, newest first. Fails `Unauthorized` when anonymous. */
|
|
256
|
+
readonly muted: (request: SocialPageRequest) => Effect.Effect<Page<Related>, Unauthorized | E, R>;
|
|
257
|
+
/** The viewer's own blocks, newest first. Fails `Unauthorized` when anonymous. */
|
|
258
|
+
readonly blocked: (request: SocialPageRequest) => Effect.Effect<Page<Related>, Unauthorized | E, R>;
|
|
259
|
+
/** Whether `a` and `b` may interact: neither blocks the other. Always true for one account with itself. */
|
|
260
|
+
readonly interactionAllowed: (a: string, b: string) => Effect.Effect<boolean, E, R>;
|
|
261
|
+
/**
|
|
262
|
+
* The opt-in read filter that drops what the viewer silenced:
|
|
263
|
+
* `column` (an author column) not in the accounts the viewer muted or
|
|
264
|
+
* blocked, through `./filter`'s `notIn` (a `NULL` author is kept). The
|
|
265
|
+
* anonymous have silenced nobody: `always`.
|
|
266
|
+
*/
|
|
267
|
+
readonly silencedFilter: (column: string) => Effect.Effect<ListFilter<Following>, never, R>;
|
|
268
|
+
}
|
|
269
|
+
/** A page request clamped as every list's is (`SEC-INPUT-003`). */
|
|
270
|
+
/**
|
|
271
|
+
* One page of a public follower or following list. `total` counts only
|
|
272
|
+
* accounts the viewer may see: it is the store's total when the
|
|
273
|
+
* application declares no {@link AccountVisibility}, and none when it does
|
|
274
|
+
* (the store's count would include accounts the seam hides), so a filtered
|
|
275
|
+
* page may also hold fewer than `limit` rows.
|
|
276
|
+
*
|
|
277
|
+
* @since 0.1.0
|
|
278
|
+
*/
|
|
279
|
+
export interface RelatedPage {
|
|
280
|
+
readonly rows: ReadonlyArray<Related>;
|
|
281
|
+
readonly total: Option.Option<number>;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Bind the social graph's operations. Every operation is traced as
|
|
285
|
+
* `social.<operation>`, and the writes run in the caller's transaction.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```ts
|
|
289
|
+
* import { strictEqual } from "node:assert";
|
|
290
|
+
* import * as Effect from "effect/Effect";
|
|
291
|
+
* import * as Option from "effect/Option";
|
|
292
|
+
* import { defineSocial } from "@thomasfosterau/pulpit/social";
|
|
293
|
+
* import { makeInMemorySocialStore } from "@thomasfosterau/pulpit/testing";
|
|
294
|
+
*
|
|
295
|
+
* const social = defineSocial({
|
|
296
|
+
* store: makeInMemorySocialStore(),
|
|
297
|
+
* viewer: Effect.succeedSome({ id: "u-1", role: "user" }),
|
|
298
|
+
* });
|
|
299
|
+
* const program = Effect.gen(function* () {
|
|
300
|
+
* yield* social.follow("u-2");
|
|
301
|
+
* return yield* social.following("u-1", { limit: 10, offset: 0 });
|
|
302
|
+
* });
|
|
303
|
+
* // No AccountVisibility seam, so the total is the store's.
|
|
304
|
+
* strictEqual(Option.getOrElse(Effect.runSync(program).total, () => 0), 1);
|
|
305
|
+
* ```
|
|
306
|
+
*
|
|
307
|
+
* @since 0.1.0
|
|
308
|
+
*/
|
|
309
|
+
export declare const defineSocial: <Following, E, R>(definition: SocialDefinition<Following, E, R>) => SocialOps<Following, E, R>;
|
|
310
|
+
export {};
|
|
311
|
+
//# sourceMappingURL=social.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"social.d.ts","sourceRoot":"","sources":["../src/social.ts"],"names":[],"mappings":"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"}
|
package/dist/social.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The social graph** (pulpit#195) — follow, mute and block between
|
|
3
|
+
* accounts, as relationship facts with their own gates, the follower and
|
|
4
|
+
* following lists, a {@link FollowerSeam} built from the graph so
|
|
5
|
+
* `followers` rows work end to end, and an opt-in read filter over the
|
|
6
|
+
* accounts a viewer silenced.
|
|
7
|
+
*
|
|
8
|
+
* The behavioural spec is gardeneria's `social.follows` and its operations
|
|
9
|
+
* (`apps/api/src/operations/follows.ts`), with their tests.
|
|
10
|
+
*
|
|
11
|
+
* ## The model (owner decisions on pulpit#195)
|
|
12
|
+
*
|
|
13
|
+
* - **A relationship is a fact**, keyed `(kind, from, to)` for the kinds
|
|
14
|
+
* `follow`, `mute` and `block`: no history; asserting one twice is a
|
|
15
|
+
* no-op, removing it hard-deletes it. Only the actor writes their own
|
|
16
|
+
* (`from` is always the viewer).
|
|
17
|
+
* - **Following is immediate.** There are no follow requests (a later
|
|
18
|
+
* slice).
|
|
19
|
+
* - **A block suppresses interaction only.** Neither side may follow the
|
|
20
|
+
* other while either blocks the other ({@link InteractionBlocked}), a
|
|
21
|
+
* block removes both accounts' follows of each other, and other modules
|
|
22
|
+
* gate an interaction (a reaction, a reply, a mention) on
|
|
23
|
+
* {@link SocialOps.interactionAllowed}. It hides no content by itself:
|
|
24
|
+
* {@link SocialOps.silencedFilter} is the opt-in read filter that drops
|
|
25
|
+
* the authors a viewer muted or blocked from a feed or a thread.
|
|
26
|
+
* - **A mute is one-way and private**: it only feeds that read filter.
|
|
27
|
+
*
|
|
28
|
+
* Mute and block lists are their owner's alone; the follower and following
|
|
29
|
+
* lists are public, filtered through the application's account visibility
|
|
30
|
+
* ({@link AccountVisibility}).
|
|
31
|
+
*
|
|
32
|
+
* @copyright Thomas Foster
|
|
33
|
+
* @module pulpit/social
|
|
34
|
+
* @since 0.1.0
|
|
35
|
+
*/
|
|
36
|
+
import * as Arr from "effect/Array";
|
|
37
|
+
import * as Effect from "effect/Effect";
|
|
38
|
+
import * as HashSet from "effect/HashSet";
|
|
39
|
+
import * as Option from "effect/Option";
|
|
40
|
+
import * as Schema from "effect/Schema";
|
|
41
|
+
import { always, isNotIn } from "./filter.js";
|
|
42
|
+
import { nowDate, signedIn as signedInAs } from "./internal/plain-rows.js";
|
|
43
|
+
import { MAX_PAGE_LIMIT, } from "./subtype.js";
|
|
44
|
+
// ── Vocabulary ────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* The relationship kinds.
|
|
47
|
+
*
|
|
48
|
+
* @since 0.1.0
|
|
49
|
+
*/
|
|
50
|
+
export const RelationshipKind = Schema.Literals(["follow", "mute", "block"]);
|
|
51
|
+
// ── Errors ────────────────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Either account blocks the other, so the interaction is refused: the
|
|
54
|
+
* wire's 403. It tells the actor a block exists between the two, as every
|
|
55
|
+
* social network's refusal does.
|
|
56
|
+
*
|
|
57
|
+
* @since 0.1.0
|
|
58
|
+
*/
|
|
59
|
+
export class InteractionBlocked extends Schema.TaggedError()("InteractionBlocked", { accountId: Schema.String }) {
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A relationship with oneself: the wire's 422.
|
|
63
|
+
*
|
|
64
|
+
* @since 0.1.0
|
|
65
|
+
*/
|
|
66
|
+
export class SelfRelationship extends Schema.TaggedError()("SelfRelationship", {
|
|
67
|
+
kind: RelationshipKind,
|
|
68
|
+
}) {
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The follower seam a social store fills (`./subtype`'s
|
|
72
|
+
* {@link FollowerSeam}), so a `followers` row is readable by the accounts
|
|
73
|
+
* that follow its owner.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { strictEqual } from "node:assert";
|
|
78
|
+
* import * as Effect from "effect/Effect";
|
|
79
|
+
* import { socialFollowers } from "@thomasfosterau/pulpit/social";
|
|
80
|
+
* import { makeInMemorySocialStore } from "@thomasfosterau/pulpit/testing";
|
|
81
|
+
*
|
|
82
|
+
* const store = makeInMemorySocialStore();
|
|
83
|
+
* Effect.runSync(store.add({ kind: "follow", from: "u-1", to: "u-2", at: new Date(0) }));
|
|
84
|
+
* const followers = socialFollowers(store);
|
|
85
|
+
* strictEqual(Effect.runSync(followers.isFollower("u-1", "u-2")), true);
|
|
86
|
+
* strictEqual(Effect.runSync(followers.isFollower("u-2", "u-1")), false);
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @since 0.1.0
|
|
90
|
+
*/
|
|
91
|
+
export const socialFollowers = (store) => ({
|
|
92
|
+
isFollower: store.isFollowing,
|
|
93
|
+
following: store.followingOf,
|
|
94
|
+
});
|
|
95
|
+
const window = (request) => ({
|
|
96
|
+
limit: Math.max(1, Math.min(Math.trunc(request.limit), MAX_PAGE_LIMIT)),
|
|
97
|
+
offset: Math.max(0, Math.trunc(request.offset)),
|
|
98
|
+
});
|
|
99
|
+
/**
|
|
100
|
+
* Bind the social graph's operations. Every operation is traced as
|
|
101
|
+
* `social.<operation>`, and the writes run in the caller's transaction.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { strictEqual } from "node:assert";
|
|
106
|
+
* import * as Effect from "effect/Effect";
|
|
107
|
+
* import * as Option from "effect/Option";
|
|
108
|
+
* import { defineSocial } from "@thomasfosterau/pulpit/social";
|
|
109
|
+
* import { makeInMemorySocialStore } from "@thomasfosterau/pulpit/testing";
|
|
110
|
+
*
|
|
111
|
+
* const social = defineSocial({
|
|
112
|
+
* store: makeInMemorySocialStore(),
|
|
113
|
+
* viewer: Effect.succeedSome({ id: "u-1", role: "user" }),
|
|
114
|
+
* });
|
|
115
|
+
* const program = Effect.gen(function* () {
|
|
116
|
+
* yield* social.follow("u-2");
|
|
117
|
+
* return yield* social.following("u-1", { limit: 10, offset: 0 });
|
|
118
|
+
* });
|
|
119
|
+
* // No AccountVisibility seam, so the total is the store's.
|
|
120
|
+
* strictEqual(Option.getOrElse(Effect.runSync(program).total, () => 0), 1);
|
|
121
|
+
* ```
|
|
122
|
+
*
|
|
123
|
+
* @since 0.1.0
|
|
124
|
+
*/
|
|
125
|
+
export const defineSocial = (definition) => {
|
|
126
|
+
const { store } = definition;
|
|
127
|
+
const signedIn = signedInAs(definition.viewer);
|
|
128
|
+
const assert = (kind, account, to) => Effect.flatMap(nowDate, (at) => Effect.map(store.add({ kind, from: account.id, to, at }), (changed) => ({ changed })));
|
|
129
|
+
const retract = (kind, operation) => Effect.fn(`social.${operation}`)(function* (to) {
|
|
130
|
+
const account = yield* signedIn;
|
|
131
|
+
return { changed: yield* store.remove(kind, account.id, to) };
|
|
132
|
+
});
|
|
133
|
+
const notSelf = (kind, account, to) => account.id === to ? Effect.fail(new SelfRelationship({ kind })) : Effect.void;
|
|
134
|
+
const interactionAllowed = Effect.fn("social.interactionAllowed")(function* (a, b) {
|
|
135
|
+
if (a === b)
|
|
136
|
+
return true;
|
|
137
|
+
return !(yield* store.has("block", a, b)) && !(yield* store.has("block", b, a));
|
|
138
|
+
});
|
|
139
|
+
const follow = Effect.fn("social.follow")(function* (to) {
|
|
140
|
+
const account = yield* signedIn;
|
|
141
|
+
yield* notSelf("follow", account, to);
|
|
142
|
+
// The block check is part of the store's insert, never a read before
|
|
143
|
+
// it, so a concurrent block can't land in between.
|
|
144
|
+
const at = yield* nowDate;
|
|
145
|
+
const outcome = yield* store.addFollow({ kind: "follow", from: account.id, to, at });
|
|
146
|
+
if (outcome === "blocked")
|
|
147
|
+
return yield* new InteractionBlocked({ accountId: to });
|
|
148
|
+
return { changed: outcome === "added" };
|
|
149
|
+
});
|
|
150
|
+
const mute = Effect.fn("social.mute")(function* (to) {
|
|
151
|
+
const account = yield* signedIn;
|
|
152
|
+
yield* notSelf("mute", account, to);
|
|
153
|
+
return yield* assert("mute", account, to);
|
|
154
|
+
});
|
|
155
|
+
const block = Effect.fn("social.block")(function* (to) {
|
|
156
|
+
const account = yield* signedIn;
|
|
157
|
+
yield* notSelf("block", account, to);
|
|
158
|
+
// A block ends both follows between the two accounts, in the same atomic step.
|
|
159
|
+
const at = yield* nowDate;
|
|
160
|
+
return { changed: yield* store.addBlock({ kind: "block", from: account.id, to, at }) };
|
|
161
|
+
});
|
|
162
|
+
const publicList = (side, operation) => Effect.fn(`social.${operation}`)(function* (accountId, request) {
|
|
163
|
+
const { accounts } = definition;
|
|
164
|
+
if (accounts === undefined) {
|
|
165
|
+
const page = yield* store.list({ kind: "follow", side, accountId, ...window(request) });
|
|
166
|
+
return { rows: page.rows, total: Option.some(page.total) };
|
|
167
|
+
}
|
|
168
|
+
const viewer = yield* definition.viewer;
|
|
169
|
+
// A subject the viewer can't see answers as one with no relationships.
|
|
170
|
+
if (!HashSet.has(yield* accounts.visible(viewer, [accountId]), accountId)) {
|
|
171
|
+
return { rows: [], total: Option.none() };
|
|
172
|
+
}
|
|
173
|
+
const page = yield* store.list({ kind: "follow", side, accountId, ...window(request) });
|
|
174
|
+
const shown = Arr.isReadonlyArrayNonEmpty(page.rows)
|
|
175
|
+
? yield* accounts.visible(viewer, Arr.map(page.rows, (row) => row.accountId))
|
|
176
|
+
: HashSet.empty();
|
|
177
|
+
return {
|
|
178
|
+
rows: Arr.filter(page.rows, (row) => HashSet.has(shown, row.accountId)),
|
|
179
|
+
total: Option.none(),
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
const ownList = (kind, operation) => Effect.fn(`social.${operation}`)(function* (request) {
|
|
183
|
+
const account = yield* signedIn;
|
|
184
|
+
return yield* store.list({ kind, side: "from", accountId: account.id, ...window(request) });
|
|
185
|
+
});
|
|
186
|
+
const silencedFilter = (column) => Effect.map(definition.viewer, (viewer) => Option.match(viewer, {
|
|
187
|
+
onNone: () => always,
|
|
188
|
+
onSome: (account) => isNotIn(column, store.silencedBy(account.id)),
|
|
189
|
+
}));
|
|
190
|
+
return {
|
|
191
|
+
follow,
|
|
192
|
+
unfollow: retract("follow", "unfollow"),
|
|
193
|
+
mute,
|
|
194
|
+
unmute: retract("mute", "unmute"),
|
|
195
|
+
block,
|
|
196
|
+
unblock: retract("block", "unblock"),
|
|
197
|
+
followers: publicList("to", "followers"),
|
|
198
|
+
following: publicList("from", "following"),
|
|
199
|
+
muted: ownList("mute", "muted"),
|
|
200
|
+
blocked: ownList("block", "blocked"),
|
|
201
|
+
interactionAllowed,
|
|
202
|
+
silencedFilter,
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
//# sourceMappingURL=social.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"social.js","sourceRoot":"","sources":["../src/social.ts"],"names":[],"mappings":"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"}
|