@voltro/plugin-social 0.0.0-stage → 0.87.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.
@@ -0,0 +1,354 @@
1
+ import { SocialError as e, socialError as t } from "./errors.js";
2
+ import { RecentPlayerSchema as n, SocialRelationSchema as r, SocialSettingsSchema as i, SuggestionSchema as a } from "./contract.js";
3
+ import { socialRelationsTable as o } from "./relations.entity.js";
4
+ import { socialRecentTable as s } from "./recent.entity.js";
5
+ import { socialSettingsTable as c } from "./settings.entity.js";
6
+ import { Effect as l, Schema as u } from "effect";
7
+ import { createHash as d } from "node:crypto";
8
+ import { and as f, eq as p, gte as m, lt as h, or as g, queryFor as _ } from "@voltro/database";
9
+ //#region src/service.ts
10
+ var v = (e) => d("sha256").update(JSON.stringify(e)).digest("hex"), y = (n) => n instanceof e ? n : t("unavailable"), b = (e) => l.tryPromise({
11
+ try: e,
12
+ catch: y
13
+ }), x = (e) => {
14
+ if (typeof e != "string" || !e.trim() || e.length > 256 || e.includes("\0") || e === "~") throw t("invalid_input");
15
+ }, S = (e) => {
16
+ if (e.type !== "user") throw t("identity_required");
17
+ return x(e.tenantId), x(e.id), {
18
+ tenantId: e.tenantId,
19
+ ownerId: e.id
20
+ };
21
+ }, C = (e) => {
22
+ let n = e instanceof Date ? e.getTime() : typeof e == "string" ? Date.parse(e) : NaN;
23
+ if (!Number.isSafeInteger(n) || n < 0) throw t("unavailable");
24
+ return n;
25
+ }, w = (e) => u.decodeUnknownSync(r)({
26
+ ...e,
27
+ createdAt: C(e.createdAt),
28
+ respondedAt: e.respondedAt === null ? null : C(e.respondedAt)
29
+ }), T = (e) => u.decodeUnknownSync(n)({
30
+ ...e,
31
+ at: C(e.at)
32
+ }), E = (e) => ({
33
+ ownerId: e,
34
+ requestsFrom: "anyone",
35
+ showOnline: !1,
36
+ showActivity: !1,
37
+ allowPartyInvites: "friends",
38
+ metadata: {}
39
+ }), D = (e, n, r) => {
40
+ let d = () => {
41
+ let e = n.now();
42
+ if (!Number.isSafeInteger(e) || e < 0 || e > 864e13) throw t("unavailable");
43
+ return e;
44
+ }, D = (e, t) => e < t ? [e, t] : [t, e], O = (e, t, n, r) => v([e, ...r === "friend" ? D(t, n) : [t, n]]), k = (e, t, n, r) => "srel_" + v([O(e, t, n, r), r]).slice(0, 56), A = (e, t) => "sset_" + v([e, t]).slice(0, 56), j = async (e, n, r) => {
45
+ let a = (await e.query(_(c).where(p("id", A(n, r))).limit(1).descriptor))[0];
46
+ if (!a) return E(r);
47
+ if (a.tenantId !== n || a.ownerId !== r) throw t("unavailable");
48
+ return u.decodeUnknownSync(i)(a);
49
+ }, M = async (t, n) => {
50
+ for (let r of [...new Set(n)].sort()) {
51
+ let n = A(t, r);
52
+ try {
53
+ await e.insertIgnore(c.tableName, {
54
+ ...E(r),
55
+ id: n,
56
+ tenantId: t,
57
+ admission: 0
58
+ }, { conflictColumns: ["id"] });
59
+ } catch (i) {
60
+ let a = (await e.query(_(c).where(p("id", n)).limit(1).descriptor))[0];
61
+ if (!a || a.tenantId !== t || a.ownerId !== r) throw i;
62
+ }
63
+ }
64
+ }, N = async (n, r, i) => (await M(n, r), e.transactional(async (e) => {
65
+ for (let i of [...new Set(r)].sort((e, t) => A(n, e).localeCompare(A(n, t)))) {
66
+ let r = A(n, i);
67
+ if (await e.updateMany(c.tableName, { admission: 0 }, { where: p("id", r) }) !== 1) throw t("unavailable");
68
+ let a = (await e.query(_(c).where(p("id", r)).forUpdate().limit(1).descriptor))[0];
69
+ if (!a || a.tenantId !== n || a.ownerId !== i) throw t("unavailable");
70
+ }
71
+ return i(e);
72
+ })), P = async (e, n, r, i, a) => {
73
+ let s = (await e.query(_(o).where(p("id", k(n, r, i, a))).limit(1).descriptor))[0], [c, l] = a === "friend" ? D(r, i) : [r, i];
74
+ if (s && (s.tenantId !== n || s.fromId !== c || s.toId !== l || s.kind !== a || s.pairKey !== O(n, r, i, a))) throw t("unavailable");
75
+ return s;
76
+ }, F = async (e, t, n, r) => await P(e, t, n, r, "block") !== void 0 || await P(e, t, r, n, "block") !== void 0, I = async (e, n, r, i, a) => {
77
+ let s = f(p("tenantId", n), p("kind", i), i === "block" || i === "favorite" ? p("fromId", r) : g(p("fromId", r), p("toId", r))), c = await e.query(_(o).where(s).orderBy("id", "asc").limit(a + 1).descriptor);
78
+ if (c.length > a || c.some((e) => e.tenantId !== n || e.kind !== i || (i === "block" || i === "favorite" ? e.fromId !== r : e.fromId !== r && e.toId !== r))) throw t("unavailable");
79
+ return c;
80
+ }, L = async (e, r, i) => (await I(e, r, i, "friend", n.maxFriends)).map((e) => {
81
+ let n = e.fromId === i ? e.toId : e.fromId;
82
+ if (typeof n != "string") throw t("unavailable");
83
+ return n;
84
+ }), R = async (e, t, n, r) => {
85
+ let i = new Set(await L(e, t, n));
86
+ return (await L(e, t, r)).filter((e) => i.has(e)).length;
87
+ }, z = async (e, t, n, r, i) => {
88
+ let a = await P(e, t, n, r, i);
89
+ if (a) return a;
90
+ let [s, c] = i === "friend" ? D(n, r) : [n, r];
91
+ return e.insert(o.tableName, {
92
+ id: k(t, n, r, i),
93
+ tenantId: t,
94
+ fromId: s,
95
+ toId: c,
96
+ pairKey: O(t, n, r, i),
97
+ kind: i,
98
+ createdAt: new Date(d()),
99
+ respondedAt: i === "friend" ? new Date(d()) : null,
100
+ note: null,
101
+ metadata: {}
102
+ });
103
+ }, B = async (e, t, n, r, i) => {
104
+ let a = await P(e, t, n, r, i);
105
+ a && await e.delete(o.tableName, String(a.id));
106
+ }, V = (e, n, r) => b(async () => {
107
+ let { tenantId: i, ownerId: a } = S(e);
108
+ if (x(n), a === n) throw t("invalid_input");
109
+ return N(i, [a, n], (e) => r(e, i, a));
110
+ }), H = async (e, t, n, r) => {
111
+ await B(e, t, n, r, "request"), await B(e, t, r, n, "request");
112
+ }, U = {
113
+ request: (e, i) => l.gen(function* () {
114
+ let a = yield* l.try({
115
+ try: () => {
116
+ let n = S(e);
117
+ if (x(i), n.ownerId === i) throw t("invalid_input");
118
+ return n;
119
+ },
120
+ catch: y
121
+ }), o = yield* l.try({
122
+ try: d,
123
+ catch: y
124
+ }), s = yield* r.consume("social:request:" + v([a.tenantId, a.ownerId]), {
125
+ limit: n.requestLimit,
126
+ windowMs: n.requestWindowMs,
127
+ algorithm: "fixed-window",
128
+ burst: n.requestLimit
129
+ }, o).pipe(l.catchDefect(() => l.fail(t("unavailable"))));
130
+ return s.failOpenReason === void 0 ? s.allowed ? yield* V(e, i, async (e, r, a) => {
131
+ if (await F(e, r, a, i)) throw t("blocked");
132
+ let o = await P(e, r, a, i, "friend");
133
+ if (o) return w(o);
134
+ let s = await P(e, r, a, i, "request");
135
+ if (s) return w(s);
136
+ let c = await j(e, r, i);
137
+ if (n.strategies.requestPolicy({
138
+ settings: c,
139
+ mutualFriends: await R(e, r, a, i)
140
+ }) !== !0) throw t("request_refused");
141
+ for (let o of [a, i]) {
142
+ if ((await I(e, r, o, "friend", n.maxFriends)).length >= n.maxFriends) throw t("friend_limit");
143
+ if ((await I(e, r, o, "request", n.maxPendingRequests)).length >= n.maxPendingRequests) throw t("pending_limit");
144
+ }
145
+ return w(await z(e, r, a, i, "request"));
146
+ }) : yield* l.fail(t("rate_limited")) : yield* l.fail(t("unavailable"));
147
+ }),
148
+ accept: (e, r) => V(e, r, async (e, i, a) => {
149
+ if (await F(e, i, a, r)) throw t("blocked");
150
+ let o = await P(e, i, a, r, "friend");
151
+ if (o) return w(o);
152
+ if (!await P(e, i, r, a, "request")) throw t("not_found");
153
+ for (let o of [a, r]) if ((await L(e, i, o)).length >= n.maxFriends) throw t("friend_limit");
154
+ let s = w(await z(e, i, a, r, "friend"));
155
+ return await H(e, i, a, r), s;
156
+ }),
157
+ decline: (e, t) => V(e, t, (e, n, r) => B(e, n, t, r, "request")),
158
+ cancel: (e, t) => V(e, t, (e, n, r) => B(e, n, r, t, "request")),
159
+ remove: (e, t) => V(e, t, async (e, n, r) => {
160
+ await B(e, n, r, t, "friend"), await B(e, n, r, t, "favorite"), await B(e, n, t, r, "favorite");
161
+ }),
162
+ block: (e, t) => V(e, t, async (e, n, r) => {
163
+ await z(e, n, r, t, "block"), await H(e, n, r, t), await B(e, n, r, t, "friend"), await B(e, n, r, t, "favorite"), await B(e, n, t, r, "favorite");
164
+ }),
165
+ unblock: (e, t) => V(e, t, (e, n, r) => B(e, n, r, t, "block")),
166
+ favorite: (e, n, r) => V(e, n, async (e, i, a) => {
167
+ if (typeof r != "boolean") throw t("invalid_input");
168
+ if (!r) return B(e, i, a, n, "favorite");
169
+ if (await F(e, i, a, n)) throw t("blocked");
170
+ if (!await P(e, i, a, n, "friend")) throw t("not_found");
171
+ await z(e, i, a, n, "favorite");
172
+ }),
173
+ setSettings: (e, n) => b(async () => {
174
+ let { tenantId: r, ownerId: a } = S(e);
175
+ if (Object.keys(n).some((e) => ![
176
+ "requestsFrom",
177
+ "showOnline",
178
+ "showActivity",
179
+ "allowPartyInvites"
180
+ ].includes(e))) throw t("invalid_input");
181
+ return N(r, [a], async (e) => {
182
+ let t = u.decodeUnknownSync(i)({
183
+ ...await j(e, r, a),
184
+ ...n,
185
+ ownerId: a
186
+ });
187
+ return await e.update(c.tableName, A(r, a), t), t;
188
+ });
189
+ }),
190
+ getSettings: (t) => b(() => {
191
+ let { tenantId: n, ownerId: r } = S(t);
192
+ return j(e, n, r);
193
+ }),
194
+ friends: (t) => b(async () => {
195
+ let { tenantId: r, ownerId: i } = S(t);
196
+ return (await I(e, r, i, "friend", n.maxFriends)).map(w);
197
+ }),
198
+ favorites: (t) => b(async () => {
199
+ let { tenantId: r, ownerId: i } = S(t);
200
+ return (await I(e, r, i, "favorite", n.maxFriends)).map(w);
201
+ }),
202
+ requests: (t) => b(async () => {
203
+ let { tenantId: r, ownerId: i } = S(t);
204
+ return (await I(e, r, i, "request", n.maxPendingRequests)).map(w);
205
+ }),
206
+ blocked: (t) => b(async () => {
207
+ let { tenantId: r, ownerId: i } = S(t);
208
+ return (await I(e, r, i, "block", n.maxExportRows)).map(w);
209
+ }),
210
+ recent: (r) => b(async () => {
211
+ let { tenantId: i, ownerId: a } = S(r), o = await e.query(_(s).where(f(p("tenantId", i), p("ownerId", a), m("at", new Date(Math.max(0, d() - n.recentRetentionMs))))).orderBy("at", "desc").orderBy("id", "asc").limit(n.recentLimit).descriptor), c = [];
212
+ for (let n of o) {
213
+ if (n.tenantId !== i || n.ownerId !== a || typeof n.otherId != "string") throw t("unavailable");
214
+ await F(e, i, a, n.otherId) || c.push(T(n));
215
+ }
216
+ return c;
217
+ }),
218
+ suggestions: (r) => b(async () => {
219
+ let { tenantId: i, ownerId: o } = S(r), c = new Set(await L(e, i, o)), l = /* @__PURE__ */ new Map();
220
+ for (let r of c) for (let a of await L(e, i, r)) {
221
+ if (a === o || c.has(a) || await F(e, i, o, a)) continue;
222
+ let r = l.get(a), u = await e.query(_(s).where(f(p("tenantId", i), p("ownerId", o), p("otherId", a), m("at", new Date(Math.max(0, d() - n.recentRetentionMs))))).limit(n.recentLimit).descriptor);
223
+ if (u.some((e) => e.tenantId !== i || e.ownerId !== o || e.otherId !== a)) throw t("unavailable");
224
+ l.set(a, {
225
+ subjectId: a,
226
+ mutualFriends: (r?.mutualFriends ?? 0) + 1,
227
+ recentMatches: u.length
228
+ });
229
+ }
230
+ let h = n.strategies.suggestions([...l.values()], n.suggestionLimit);
231
+ if (h.length > n.suggestionLimit || new Set(h.map((e) => e.subjectId)).size !== h.length) throw t("unavailable");
232
+ for (let e of h) {
233
+ u.decodeUnknownSync(a)(e);
234
+ let n = l.get(e.subjectId);
235
+ if (!n || n.mutualFriends !== e.mutualFriends || n.recentMatches !== e.recentMatches) throw t("unavailable");
236
+ }
237
+ return h;
238
+ })
239
+ }, W = {
240
+ isBlocked: (t, n) => b(async () => {
241
+ let { tenantId: r, ownerId: i } = S(t);
242
+ return x(n), F(e, r, i, n);
243
+ }),
244
+ blockPairs: (r, i) => b(async () => {
245
+ if (r.type !== "user" && !(r.type === "serviceAccount" && r.scopes?.includes("social:blocks:read"))) throw t("identity_required");
246
+ if (x(r.tenantId), i.length > n.maxMatchParticipants || new Set(i).size !== i.length) throw t("invalid_input");
247
+ i.forEach(x);
248
+ let a = [];
249
+ for (let t = 0; t < i.length; t++) for (let n = t + 1; n < i.length; n++) {
250
+ let o = i[t], s = i[n];
251
+ (r.type !== "user" || o === r.id || s === r.id) && await F(e, r.tenantId, o, s) && a.push(D(o, s));
252
+ }
253
+ return a;
254
+ }),
255
+ canMessage: (t, r) => b(async () => {
256
+ let { tenantId: i, ownerId: a } = S(t);
257
+ if (x(r), r === a || await F(e, i, a, r)) return !1;
258
+ if (await P(e, i, a, r, "friend")) return !0;
259
+ let o = await j(e, i, r);
260
+ return n.strategies.requestPolicy({
261
+ settings: o,
262
+ mutualFriends: await R(e, i, a, r)
263
+ }) === !0;
264
+ }),
265
+ canInvite: (t, n) => b(async () => {
266
+ let { tenantId: r, ownerId: i } = S(t);
267
+ return x(n), n === i || await F(e, r, i, n) ? !1 : (await j(e, r, n)).allowPartyInvites === "anyone" || await P(e, r, i, n, "friend") !== void 0;
268
+ }),
269
+ recordRecent: (e, r) => b(async () => {
270
+ if (e.type !== "system" && !(e.type === "serviceAccount" && e.tenantId === r.tenantId && e.scopes?.includes("social:recent:write"))) throw t("identity_required");
271
+ if (x(r.tenantId), x(r.context), r.participants.length > n.maxMatchParticipants || new Set(r.participants).size !== r.participants.length) throw t("invalid_input");
272
+ r.participants.forEach(x);
273
+ let i = r.at ?? d(), a = d();
274
+ if (!Number.isSafeInteger(i) || i < 0 || i > a || i < a - n.recentRetentionMs) throw t("invalid_input");
275
+ await N(r.tenantId, r.participants, async (e) => {
276
+ for (let o of r.participants) {
277
+ for (let a of r.participants) {
278
+ if (o === a || n.strategies.recentPolicy({
279
+ ownerId: o,
280
+ otherId: a,
281
+ context: r.context,
282
+ at: i
283
+ }) !== !0 || await F(e, r.tenantId, o, a)) continue;
284
+ let c = "srec_" + v([
285
+ r.tenantId,
286
+ o,
287
+ a,
288
+ r.context
289
+ ]).slice(0, 56), l = (await e.query(_(s).where(p("id", c)).limit(1).descriptor))[0];
290
+ if (l) {
291
+ if (l.tenantId !== r.tenantId || l.ownerId !== o || l.otherId !== a || l.context !== r.context) throw t("unavailable");
292
+ C(l.at) < i && await e.update(s.tableName, c, { at: new Date(i) });
293
+ } else await e.insert(s.tableName, {
294
+ id: c,
295
+ tenantId: r.tenantId,
296
+ ownerId: o,
297
+ otherId: a,
298
+ context: r.context,
299
+ at: new Date(i),
300
+ metadata: {}
301
+ });
302
+ }
303
+ await e.deleteMany(s.tableName, { where: f(p("tenantId", r.tenantId), p("ownerId", o), h("at", new Date(Math.max(0, a - n.recentRetentionMs)))) });
304
+ let c = await e.query(_(s).where(f(p("tenantId", r.tenantId), p("ownerId", o))).orderBy("at", "desc").orderBy("id", "asc").descriptor);
305
+ for (let i of c.slice(n.recentLimit)) {
306
+ if (i.tenantId !== r.tenantId || i.ownerId !== o) throw t("unavailable");
307
+ await e.delete(s.tableName, String(i.id));
308
+ }
309
+ }
310
+ });
311
+ })
312
+ }, G = async (e, r) => {
313
+ if (r.tenantId === null) throw t("identity_required");
314
+ x(r.tenantId), x(r.subjectId);
315
+ let i = [
316
+ o,
317
+ s,
318
+ c
319
+ ], a = [], l = 0;
320
+ for (let c of i) {
321
+ let i = c === o ? g(p("fromId", r.subjectId), p("toId", r.subjectId)) : c === s ? g(p("ownerId", r.subjectId), p("otherId", r.subjectId)) : p("ownerId", r.subjectId), u = await e.query(_(c).where(f(p("tenantId", r.tenantId), i)).orderBy("id", "asc").limit(n.maxExportRows + 1).descriptor);
322
+ if (l += u.length, l > n.maxExportRows) throw t("export_limit");
323
+ if (u.some((e) => e.tenantId !== r.tenantId || (c === o ? e.fromId !== r.subjectId && e.toId !== r.subjectId : c === s ? e.ownerId !== r.subjectId && e.otherId !== r.subjectId : e.ownerId !== r.subjectId))) throw t("unavailable");
324
+ a.push({
325
+ table: c.tableName,
326
+ rows: u
327
+ });
328
+ }
329
+ return a;
330
+ };
331
+ return {
332
+ service: U,
333
+ blockPort: W,
334
+ exportSubject: (t) => b(() => e.transactional(async (e) => {
335
+ let n = await G(e, t);
336
+ return JSON.parse(JSON.stringify(n.map(({ table: e, rows: t }) => ({
337
+ table: e,
338
+ rows: t.map(({ admission: e, ...t }) => t)
339
+ }))));
340
+ })),
341
+ eraseSubject: (e) => b(async () => {
342
+ if (e.tenantId === null) throw t("identity_required");
343
+ await N(e.tenantId, [e.subjectId], async (t) => {
344
+ for (let { table: n, rows: r } of await G(t, e)) for (let e of r) await t.delete(n, String(e.id));
345
+ });
346
+ }),
347
+ pruneRecent: (r) => b(async () => {
348
+ if (r.type !== "serviceAccount" || !r.scopes?.includes("social:recent:write")) throw t("identity_required");
349
+ x(r.tenantId), await e.deleteMany(s.tableName, { where: f(p("tenantId", r.tenantId), h("at", new Date(Math.max(0, d() - n.recentRetentionMs)))) });
350
+ })
351
+ };
352
+ };
353
+ //#endregion
354
+ export { D as dataStoreSocial };
@@ -0,0 +1,20 @@
1
+ import { SETTINGS_TABLE as e } from "./contract.js";
2
+ import { boolean as t, id as n, integer as r, json as i, table as a, text as o } from "@voltro/database";
3
+ //#region src/settings.entity.ts
4
+ var s = a(e, {
5
+ id: n({ prefix: "ssettings" }),
6
+ tenantId: o().maxLength(256).externalId().sensitive("pii"),
7
+ ownerId: o().maxLength(256).externalId().sensitive("pii"),
8
+ requestsFrom: o().maxLength(18).oneOf([
9
+ "anyone",
10
+ "friends-of-friends",
11
+ "nobody"
12
+ ]).safe(),
13
+ showOnline: t().safe(),
14
+ showActivity: t().safe(),
15
+ allowPartyInvites: o().maxLength(16).oneOf(["friends", "anyone"]).safe(),
16
+ admission: r().serverOnly(),
17
+ metadata: i().sensitive("pii")
18
+ }).index(["tenantId", "ownerId"]);
19
+ //#endregion
20
+ export { s as socialSettingsTable };
@@ -0,0 +1 @@
1
+ export { accepted as default } from '@voltro/plugin-social/events'
@@ -0,0 +1 @@
1
+ export { requested as default } from '@voltro/plugin-social/events'
@@ -0,0 +1,4 @@
1
+ import { defineSubscriber } from '@voltro/runtime'
2
+ import { socialEventTenant, publishSocialChange } from '@voltro/plugin-social/events/server'
3
+ export default defineSubscriber({ table: '_voltro_social_relations', on: ['insert', 'update', 'delete'], once: true,
4
+ eventTenant: socialEventTenant, handler: publishSocialChange })
@@ -0,0 +1,85 @@
1
+ import { RedisStoreOptions } from '@voltro/plugin-ratelimit';
2
+ import { Schema } from 'effect';
3
+
4
+ export declare const defaultSocialStrategies: SocialStrategies;
5
+
6
+ export declare const distinctRecentPeers: (input: RecentPolicyInput) => boolean;
7
+
8
+ export declare const mutualMatchSuggestions: (candidates: ReadonlyArray<SocialSuggestion>, limit: number) => ReadonlyArray<SocialSuggestion>;
9
+
10
+ export declare interface RecentPolicyInput {
11
+ readonly ownerId: string;
12
+ readonly otherId: string;
13
+ readonly context: string;
14
+ readonly at: number;
15
+ }
16
+
17
+ export declare const recipientRequestPolicy: ({ settings, mutualFriends }: RequestPolicyInput) => boolean;
18
+
19
+ export declare interface RequestPolicyInput {
20
+ readonly settings: SocialSettings;
21
+ readonly mutualFriends: number;
22
+ }
23
+
24
+ export declare type ResolvedSocialOptions = ReturnType<typeof resolveSocialOptions>;
25
+
26
+ export declare const resolveSocialOptions: (options?: SocialOptions) => Readonly<{
27
+ maxFriends: number;
28
+ maxPendingRequests: number;
29
+ recentLimit: number;
30
+ recentRetentionMs: number;
31
+ maxMatchParticipants: number;
32
+ maxExportRows: number;
33
+ requestLimit: number;
34
+ requestWindowMs: number;
35
+ suggestionLimit: number;
36
+ strategies: Readonly<{
37
+ suggestions: typeof mutualMatchSuggestions;
38
+ requestPolicy: (input: RequestPolicyInput) => boolean;
39
+ recentPolicy: (input: RecentPolicyInput) => boolean;
40
+ }>;
41
+ rateLimit: RedisStoreOptions | undefined;
42
+ now: () => number;
43
+ }>;
44
+
45
+ export declare interface SocialOptions {
46
+ readonly maxFriends?: number;
47
+ readonly maxPendingRequests?: number;
48
+ readonly recentLimit?: number;
49
+ readonly recentRetentionMs?: number;
50
+ readonly maxMatchParticipants?: number;
51
+ readonly maxExportRows?: number;
52
+ readonly requestLimit?: number;
53
+ readonly requestWindowMs?: number;
54
+ readonly suggestionLimit?: number;
55
+ readonly strategies?: Partial<SocialStrategies>;
56
+ readonly rateLimit?: RedisStoreOptions;
57
+ readonly now?: () => number;
58
+ }
59
+
60
+ declare type SocialSettings = typeof SocialSettingsSchema.Type;
61
+
62
+ declare const SocialSettingsSchema: Schema.Struct<{
63
+ readonly ownerId: Schema.String;
64
+ readonly requestsFrom: Schema.Literals<readonly ["anyone", "friends-of-friends", "nobody"]>;
65
+ readonly showOnline: Schema.Boolean;
66
+ readonly showActivity: Schema.Boolean;
67
+ readonly allowPartyInvites: Schema.Literals<readonly ["friends", "anyone"]>;
68
+ readonly metadata: Schema.$Record<Schema.String, Schema.Unknown>;
69
+ }>;
70
+
71
+ export declare interface SocialStrategies {
72
+ readonly suggestions: typeof mutualMatchSuggestions;
73
+ readonly requestPolicy: (input: RequestPolicyInput) => boolean;
74
+ readonly recentPolicy: (input: RecentPolicyInput) => boolean;
75
+ }
76
+
77
+ declare type SocialSuggestion = typeof SuggestionSchema.Type;
78
+
79
+ declare const SuggestionSchema: Schema.Struct<{
80
+ readonly subjectId: Schema.String;
81
+ readonly mutualFriends: Schema.Number;
82
+ readonly recentMatches: Schema.Number;
83
+ }>;
84
+
85
+ export { }
@@ -0,0 +1,40 @@
1
+ import { socialError as e } from "./errors.js";
2
+ //#region src/strategies.ts
3
+ var t = ({ settings: e, mutualFriends: t }) => e.requestsFrom === "anyone" || e.requestsFrom === "friends-of-friends" && t > 0, n = (e, t) => [...e].sort((e, t) => t.recentMatches - e.recentMatches || t.mutualFriends - e.mutualFriends || (e.subjectId < t.subjectId ? -1 : +(e.subjectId > t.subjectId))).slice(0, t), r = (e) => e.ownerId !== e.otherId, i = Object.freeze({
4
+ suggestions: n,
5
+ requestPolicy: t,
6
+ recentPolicy: r
7
+ }), a = (t = {}) => {
8
+ let n = {
9
+ maxFriends: t.maxFriends ?? 500,
10
+ maxPendingRequests: t.maxPendingRequests ?? 100,
11
+ recentLimit: t.recentLimit ?? 50,
12
+ recentRetentionMs: t.recentRetentionMs ?? 2592e6,
13
+ maxMatchParticipants: t.maxMatchParticipants ?? 64,
14
+ maxExportRows: t.maxExportRows ?? 1e4,
15
+ requestLimit: t.requestLimit ?? 10,
16
+ requestWindowMs: t.requestWindowMs ?? 6e4,
17
+ suggestionLimit: t.suggestionLimit ?? 20,
18
+ strategies: Object.freeze({
19
+ ...i,
20
+ ...t.strategies
21
+ }),
22
+ rateLimit: t.rateLimit,
23
+ now: t.now ?? Date.now
24
+ };
25
+ for (let [t, r] of [
26
+ [n.maxFriends, 1e4],
27
+ [n.maxPendingRequests, 1e4],
28
+ [n.recentLimit, 1e3],
29
+ [n.recentRetentionMs, 31536e7],
30
+ [n.maxMatchParticipants, 256],
31
+ [n.maxExportRows, 1e5],
32
+ [n.requestLimit, 1e4],
33
+ [n.requestWindowMs, 864e5],
34
+ [n.suggestionLimit, 100]
35
+ ]) if (!Number.isSafeInteger(t) || t < 1 || t > r) throw e("invalid_input");
36
+ if (typeof n.now != "function" || Object.values(n.strategies).some((e) => typeof e != "function")) throw e("invalid_input");
37
+ return Object.freeze(n);
38
+ };
39
+ //#endregion
40
+ export { i as defaultSocialStrategies, r as distinctRecentPeers, n as mutualMatchSuggestions, t as recipientRequestPolicy, a as resolveSocialOptions };
@@ -0,0 +1,122 @@
1
+ import { Context } from 'effect';
2
+ import { Effect } from 'effect';
3
+ import { Layer } from 'effect';
4
+ import { Schema } from 'effect';
5
+ import { Subject } from '@voltro/protocol';
6
+ import { YieldableError } from 'effect/Cause';
7
+
8
+ export declare const fakeSocialBlockLayer: (overrides?: Partial<SocialBlockService>) => Layer.Layer<SocialBlockPort, never, never>;
9
+
10
+ export declare const fakeSocialBlockService: (overrides?: Partial<SocialBlockService>) => SocialBlockService;
11
+
12
+ declare interface RecentPolicyInput {
13
+ readonly ownerId: string;
14
+ readonly otherId: string;
15
+ readonly context: string;
16
+ readonly at: number;
17
+ }
18
+
19
+ declare interface RecordRecentInput {
20
+ readonly tenantId: string;
21
+ readonly participants: ReadonlyArray<string>;
22
+ readonly context: string;
23
+ readonly at?: number;
24
+ }
25
+
26
+ declare interface RequestPolicyInput {
27
+ readonly settings: SocialSettings;
28
+ readonly mutualFriends: number;
29
+ }
30
+
31
+ export declare const socialBlockFakeLayer: Layer.Layer<SocialBlockPort, never, never>;
32
+
33
+ declare class SocialBlockPort extends SocialBlockPort_base {
34
+ }
35
+
36
+ declare const SocialBlockPort_base: Context.ServiceClass<SocialBlockPort, "@voltro/SocialBlockPort", SocialBlockService>;
37
+
38
+ export declare const socialBlockPortFixture: {
39
+ kind: "port";
40
+ tag: typeof SocialBlockPort;
41
+ fakeLayer: Layer.Layer<SocialBlockPort, never, never>;
42
+ probe: {
43
+ run: () => Effect.Effect<boolean | "unavailable" | "identity_required" | "invalid_input" | "blocked" | "request_refused" | "not_found" | "friend_limit" | "pending_limit" | "rate_limited" | "export_limit", never, never>;
44
+ expected: string;
45
+ };
46
+ };
47
+
48
+ /** Chat, matchmaking and party admission depend on this port, never Social SQL. */
49
+ declare interface SocialBlockService {
50
+ readonly isBlocked: (subject: Subject, otherId: string) => Effect.Effect<boolean, SocialError>;
51
+ readonly blockPairs: (subject: Subject, subjectIds: ReadonlyArray<string>) => Effect.Effect<ReadonlyArray<readonly [string, string]>, SocialError>;
52
+ readonly canMessage: (subject: Subject, otherId: string) => Effect.Effect<boolean, SocialError>;
53
+ readonly canInvite: (subject: Subject, otherId: string) => Effect.Effect<boolean, SocialError>;
54
+ readonly recordRecent: (subject: Subject, input: RecordRecentInput) => Effect.Effect<void, SocialError>;
55
+ }
56
+
57
+ declare class SocialError extends SocialError_base {
58
+ }
59
+
60
+ declare const SocialError_base: Schema.Class<SocialError, Schema.TaggedStruct<"SocialError", {
61
+ readonly code: Schema.Literals<readonly ["identity_required", "invalid_input", "blocked", "request_refused", "not_found", "friend_limit", "pending_limit", "rate_limited", "unavailable", "export_limit"]>;
62
+ }>, YieldableError>;
63
+
64
+ export declare const socialRecentPolicyFixture: {
65
+ kind: "slot";
66
+ defaultImplementation: (input: RecentPolicyInput) => boolean;
67
+ defaultProbe: {
68
+ run: () => Effect.Effect<boolean, never, never>;
69
+ expected: boolean;
70
+ };
71
+ overrideProbe: {
72
+ run: () => Effect.Effect<boolean, never, never>;
73
+ expected: boolean;
74
+ };
75
+ };
76
+
77
+ export declare const socialRequestPolicyFixture: {
78
+ kind: "slot";
79
+ defaultImplementation: ({ settings, mutualFriends }: RequestPolicyInput) => boolean;
80
+ defaultProbe: {
81
+ run: () => Effect.Effect<boolean, never, never>;
82
+ expected: boolean;
83
+ };
84
+ overrideProbe: {
85
+ run: () => Effect.Effect<boolean, never, never>;
86
+ expected: boolean;
87
+ };
88
+ };
89
+
90
+ declare type SocialSettings = typeof SocialSettingsSchema.Type;
91
+
92
+ declare const SocialSettingsSchema: Schema.Struct<{
93
+ readonly ownerId: Schema.String;
94
+ readonly requestsFrom: Schema.Literals<readonly ["anyone", "friends-of-friends", "nobody"]>;
95
+ readonly showOnline: Schema.Boolean;
96
+ readonly showActivity: Schema.Boolean;
97
+ readonly allowPartyInvites: Schema.Literals<readonly ["friends", "anyone"]>;
98
+ readonly metadata: Schema.$Record<Schema.String, Schema.Unknown>;
99
+ }>;
100
+
101
+ declare type SocialSuggestion = typeof SuggestionSchema.Type;
102
+
103
+ export declare const socialSuggestionsFixture: {
104
+ kind: "slot";
105
+ defaultImplementation: (candidates: ReadonlyArray< SocialSuggestion>, limit: number) => ReadonlyArray< SocialSuggestion>;
106
+ defaultProbe: {
107
+ run: () => Effect.Effect<string[], never, never>;
108
+ expected: string[];
109
+ };
110
+ overrideProbe: {
111
+ run: () => Effect.Effect<string[], never, never>;
112
+ expected: string[];
113
+ };
114
+ };
115
+
116
+ declare const SuggestionSchema: Schema.Struct<{
117
+ readonly subjectId: Schema.String;
118
+ readonly mutualFriends: Schema.Number;
119
+ readonly recentMatches: Schema.Number;
120
+ }>;
121
+
122
+ export { }