@voltro/plugin-chat 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.
Files changed (50) hide show
  1. package/CHANGELOG.md +24329 -0
  2. package/LICENSE +61 -0
  3. package/README.md +516 -2
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +14931 -0
  6. package/dist/admissions.entity.js +11 -0
  7. package/dist/channels.entity.js +20 -0
  8. package/dist/chat-app/events/chat.mentioned.event.ts +1 -0
  9. package/dist/chat-app/events/chat.messageChanged.event.ts +1 -0
  10. package/dist/chat-app/events/chat.typing.event.ts +1 -0
  11. package/dist/chat-app/schedules/chat.retention.cron.tsx +16 -0
  12. package/dist/chat-app/subscribers/chat.messages.subscribe.ts +4 -0
  13. package/dist/components.js +432 -0
  14. package/dist/contract.js +92 -0
  15. package/dist/errors.d.ts +13 -0
  16. package/dist/errors.js +21 -0
  17. package/dist/eventProducer.d.ts +18 -0
  18. package/dist/eventProducer.js +46 -0
  19. package/dist/events.d.ts +38 -0
  20. package/dist/events.js +36 -0
  21. package/dist/evidence.entity.js +25 -0
  22. package/dist/index.d.ts +623 -0
  23. package/dist/index.js +181 -0
  24. package/dist/inspect.js +38 -0
  25. package/dist/media.entity.js +15 -0
  26. package/dist/members.entity.js +22 -0
  27. package/dist/mentionDraft.js +21 -0
  28. package/dist/mentionNotifications.js +110 -0
  29. package/dist/messages.entity.js +28 -0
  30. package/dist/reportEvidence.js +16 -0
  31. package/dist/retention.entity.js +11 -0
  32. package/dist/rpc.d.ts +295 -0
  33. package/dist/rpc.js +267 -0
  34. package/dist/rpcRoutes.js +97 -0
  35. package/dist/server.d.ts +200 -0
  36. package/dist/server.js +7 -0
  37. package/dist/service.js +1412 -0
  38. package/dist/strategies.d.ts +148 -0
  39. package/dist/strategies.js +90 -0
  40. package/dist/testing.d.ts +139 -0
  41. package/dist/testing.js +111 -0
  42. package/dist/typing.js +88 -0
  43. package/dist/typingInvalidation.js +86 -0
  44. package/dist/typingLease.js +59 -0
  45. package/dist/typingPublisher.js +59 -0
  46. package/dist/useChatTypingPublisher.js +33 -0
  47. package/dist/useTyping.js +24 -0
  48. package/dist/web.d.ts +354 -0
  49. package/dist/web.js +19 -0
  50. package/package.json +98 -4
@@ -0,0 +1,148 @@
1
+ import { Effect } from 'effect';
2
+ import { RedisStoreOptions } from '@voltro/plugin-ratelimit';
3
+ import { Schema } from 'effect';
4
+ import { Subject } from '@voltro/protocol';
5
+ import { YieldableError } from 'effect/Cause';
6
+
7
+ export declare const channelChatRetention: (channel: ChatChannel) => number | null;
8
+
9
+ declare type ChatChannel = typeof ChatChannelSchema.Type;
10
+
11
+ declare const ChatChannelSchema: Schema.Struct<{
12
+ readonly id: Schema.String;
13
+ readonly kind: Schema.Literals<readonly ["global", "lobby", "party", "guild", "match", "dm", "custom"]>;
14
+ readonly refId: Schema.String;
15
+ readonly name: Schema.String;
16
+ readonly slowModeMs: Schema.Number;
17
+ readonly retentionMs: Schema.NullOr<Schema.Number>;
18
+ readonly maxMembers: Schema.Number;
19
+ readonly frozen: Schema.Boolean;
20
+ readonly createdAt: Schema.Number;
21
+ readonly metadata: Schema.$Record<Schema.String, Schema.Unknown>;
22
+ }>;
23
+
24
+ declare class ChatError extends ChatError_base {
25
+ }
26
+
27
+ declare const ChatError_base: Schema.Class<ChatError, Schema.TaggedStruct<"ChatError", {
28
+ readonly code: Schema.Literals<readonly ["identity_required", "invalid_input", "not_found", "forbidden", "blocked", "sanctioned", "muted", "slow_mode", "rate_limited", "content_rejected", "conflict", "frozen", "member_limit", "unavailable", "export_limit"]>;
29
+ }>, YieldableError>;
30
+
31
+ export declare interface ChatMediaInput {
32
+ readonly subject: Subject;
33
+ /** Facts read from plugin-storage for a ref the sender owns in this tenant. */
34
+ readonly ref: {
35
+ readonly id: string;
36
+ readonly contentType: string;
37
+ readonly size: number;
38
+ readonly width: number | null;
39
+ readonly height: number | null;
40
+ };
41
+ readonly contentTypes: ReadonlyArray<string>;
42
+ readonly maxBytes: number;
43
+ }
44
+
45
+ export declare interface ChatMediaVerdict {
46
+ readonly allowed: boolean;
47
+ readonly flagged: boolean;
48
+ }
49
+
50
+ export declare interface ChatMentionInput {
51
+ readonly body: string;
52
+ readonly subject: Subject;
53
+ readonly maxMentions?: number;
54
+ }
55
+
56
+ export declare interface ChatModerationInput {
57
+ readonly body: string;
58
+ readonly denyWords: ReadonlyArray<string>;
59
+ readonly action: 'block' | 'flag';
60
+ }
61
+
62
+ export declare interface ChatModerationResult {
63
+ readonly body: string;
64
+ readonly flagged: boolean;
65
+ readonly allowed: boolean;
66
+ }
67
+
68
+ export declare interface ChatOptions {
69
+ readonly maxBodyLength?: number;
70
+ readonly pageSize?: number;
71
+ readonly maxMembers?: number;
72
+ readonly maxChannelsPerSubject?: number;
73
+ readonly retentionMs?: number | null;
74
+ readonly evidenceRetentionMs?: number;
75
+ readonly perChannelRate?: number;
76
+ readonly globalRate?: number;
77
+ readonly maxMentions?: number;
78
+ readonly maxExportRows?: number;
79
+ readonly denyWords?: ReadonlyArray<string>;
80
+ readonly moderationAction?: 'block' | 'flag';
81
+ readonly strategies?: Partial<ChatStrategies>;
82
+ readonly mediaContentTypes?: ReadonlyArray<string>;
83
+ readonly maxMediaBytes?: number;
84
+ readonly mediaUrlTtlSec?: number;
85
+ readonly rateLimit?: RedisStoreOptions;
86
+ readonly now?: () => number;
87
+ }
88
+
89
+ export declare interface ChatRateInput {
90
+ readonly channel: ChatChannel;
91
+ readonly perChannel: number;
92
+ readonly global: number;
93
+ }
94
+
95
+ export declare interface ChatStrategies {
96
+ readonly moderate: typeof keywordChatModeration;
97
+ readonly rateLimit: typeof fixedChatRateBudgets;
98
+ readonly mentionResolver: typeof playerChatMentions;
99
+ readonly retention: typeof channelChatRetention;
100
+ readonly moderateMedia: typeof typedChatMedia;
101
+ }
102
+
103
+ export declare const defaultChatStrategies: ChatStrategies;
104
+
105
+ export declare const fixedChatRateBudgets: (input: ChatRateInput) => {
106
+ readonly perChannel: number;
107
+ readonly global: number;
108
+ };
109
+
110
+ /** Composition over the existing moderation provider, not another classifier. */
111
+ export declare const keywordChatModeration: (input: ChatModerationInput) => Effect.Effect<ChatModerationResult, ChatError>;
112
+
113
+ /** Exact Name#Tag claims from Players; Chat still admits every result against its channel. */
114
+ export declare const playerChatMentions: (input: ChatMentionInput) => Effect.Effect<ReadonlyArray<string>, ChatError>;
115
+
116
+ export declare const resolveChatOptions: (options?: ChatOptions) => Readonly<{
117
+ maxBodyLength: number;
118
+ pageSize: number;
119
+ maxMembers: number;
120
+ evidenceRetentionMs: number;
121
+ maxChannelsPerSubject: number;
122
+ retentionMs: number | null;
123
+ perChannelRate: number;
124
+ globalRate: number;
125
+ maxMentions: number;
126
+ maxExportRows: number;
127
+ denyWords: readonly string[];
128
+ moderationAction: "flag" | "block";
129
+ mediaContentTypes: readonly string[];
130
+ maxMediaBytes: number;
131
+ mediaUrlTtlSec: number;
132
+ strategies: Readonly<{
133
+ moderate: typeof keywordChatModeration;
134
+ rateLimit: typeof fixedChatRateBudgets;
135
+ mentionResolver: typeof playerChatMentions;
136
+ retention: typeof channelChatRetention;
137
+ moderateMedia: typeof typedChatMedia;
138
+ }>;
139
+ rateLimit: RedisStoreOptions | undefined;
140
+ now: () => number;
141
+ }>;
142
+
143
+ export declare type ResolvedChatOptions = ReturnType<typeof resolveChatOptions>;
144
+
145
+ /** Declared type and size bounds only. Image classification is an integration: replace this slot to call a provider. */
146
+ export declare const typedChatMedia: (input: ChatMediaInput) => Effect.Effect<ChatMediaVerdict, ChatError>;
147
+
148
+ export { }
@@ -0,0 +1,90 @@
1
+ import { chatError as e } from "./errors.js";
2
+ import { Effect as t } from "effect";
3
+ import { PlayersPort as n } from "@voltro/plugin-players/port";
4
+ import { keywordProvider as r } from "@voltro/plugin-moderation";
5
+ //#region src/strategies.ts
6
+ var i = (e) => r(e.denyWords)(e.body).pipe(t.map((t) => ({
7
+ body: e.body,
8
+ flagged: t.flagged,
9
+ allowed: !t.flagged || e.action === "flag"
10
+ }))), a = (e) => ({
11
+ perChannel: e.perChannel,
12
+ global: e.global
13
+ }), o = (r) => t.gen(function* () {
14
+ let i = r.maxMentions ?? 20;
15
+ if (!Number.isSafeInteger(i) || i < 1 || i > 100 || typeof r.body != "string" || r.body.length > 4e4) return yield* t.fail(e("invalid_input"));
16
+ let a = [], o = /* @__PURE__ */ new Set();
17
+ for (let n of r.body.matchAll(/(?:^|[\s(\[{])@([^@#\r\n]{1,256})#([0-9]{4})(?![\p{L}\p{N}_])/gu)) {
18
+ let r = n[1] + "#" + n[2];
19
+ if (!o.has(r)) {
20
+ if (a.length === i) return yield* t.fail(e("invalid_input"));
21
+ o.add(r), a.push({
22
+ name: n[1],
23
+ tag: n[2]
24
+ });
25
+ }
26
+ }
27
+ let s = yield* t.serviceOption(n);
28
+ if (s._tag === "None") return [];
29
+ let c = /* @__PURE__ */ new Set();
30
+ for (let n of a) {
31
+ let i = yield* t.suspend(() => s.value.resolveName(r.subject, n.name, n.tag)).pipe(t.catch((n) => n.code === "invalid_name" || n.code === "reserved_name" || n.code === "name_rejected" ? t.succeed(null) : t.fail(e("unavailable"))), t.catchDefect(() => t.fail(e("unavailable"))));
32
+ i !== null && c.add(i.subjectId);
33
+ }
34
+ return [...c];
35
+ }), s = (e) => e.retentionMs, c = (e) => t.succeed({
36
+ allowed: e.contentTypes.includes(e.ref.contentType) && e.ref.size <= e.maxBytes,
37
+ flagged: !1
38
+ }), l = Object.freeze({
39
+ moderate: i,
40
+ rateLimit: a,
41
+ mentionResolver: o,
42
+ retention: s,
43
+ moderateMedia: c
44
+ }), u = (t = {}) => {
45
+ let n = {
46
+ maxBodyLength: t.maxBodyLength ?? 2e3,
47
+ pageSize: t.pageSize ?? 50,
48
+ maxMembers: t.maxMembers ?? 500,
49
+ evidenceRetentionMs: t.evidenceRetentionMs ?? 2592e6,
50
+ maxChannelsPerSubject: t.maxChannelsPerSubject ?? 100,
51
+ retentionMs: t.retentionMs === void 0 ? 6048e5 : t.retentionMs,
52
+ perChannelRate: t.perChannelRate ?? 2,
53
+ globalRate: t.globalRate ?? 10,
54
+ maxMentions: t.maxMentions ?? 20,
55
+ maxExportRows: t.maxExportRows ?? 1e4,
56
+ denyWords: Object.freeze([...t.denyWords ?? []]),
57
+ moderationAction: t.moderationAction ?? "block",
58
+ mediaContentTypes: Object.freeze([...t.mediaContentTypes ?? [
59
+ "image/png",
60
+ "image/jpeg",
61
+ "image/webp",
62
+ "image/gif"
63
+ ]]),
64
+ maxMediaBytes: t.maxMediaBytes ?? 10485760,
65
+ mediaUrlTtlSec: t.mediaUrlTtlSec ?? 300,
66
+ strategies: Object.freeze({
67
+ ...l,
68
+ ...t.strategies
69
+ }),
70
+ rateLimit: t.rateLimit,
71
+ now: t.now ?? Date.now
72
+ };
73
+ for (let [t, r] of [
74
+ [n.evidenceRetentionMs, 31536e6],
75
+ [n.maxBodyLength, 2e4],
76
+ [n.pageSize, 200],
77
+ [n.maxMembers, 1e4],
78
+ [n.maxChannelsPerSubject, 1e4],
79
+ [n.perChannelRate, 1e3],
80
+ [n.globalRate, 1e4],
81
+ [n.maxMentions, 100],
82
+ [n.maxExportRows, 1e5],
83
+ [n.maxMediaBytes, 104857600],
84
+ [n.mediaUrlTtlSec, 3600]
85
+ ]) if (!Number.isSafeInteger(t) || t < 1 || t > r) throw e("invalid_input");
86
+ if (n.retentionMs !== null && (!Number.isSafeInteger(n.retentionMs) || n.retentionMs < 1 || n.retentionMs > 31536e7) || n.denyWords.length > 1e3 || !["block", "flag"].includes(n.moderationAction) || n.denyWords.some((e) => typeof e != "string" || !e.trim() || e.length > 256) || n.mediaContentTypes.length > 64 || n.mediaContentTypes.some((e) => typeof e != "string" || !/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(e)) || Object.values(n.strategies).some((e) => typeof e != "function") || typeof n.now != "function") throw e("invalid_input");
87
+ return Object.freeze(n);
88
+ };
89
+ //#endregion
90
+ export { s as channelChatRetention, l as defaultChatStrategies, a as fixedChatRateBudgets, i as keywordChatModeration, o as playerChatMentions, u as resolveChatOptions, c as typedChatMedia };
@@ -0,0 +1,139 @@
1
+ import { Effect } from 'effect';
2
+ import { Schema } from 'effect';
3
+ import { Subject } from '@voltro/protocol';
4
+ import { YieldableError } from 'effect/Cause';
5
+
6
+ declare type ChatChannel = typeof ChatChannelSchema.Type;
7
+
8
+ declare const ChatChannelSchema: Schema.Struct<{
9
+ readonly id: Schema.String;
10
+ readonly kind: Schema.Literals<readonly ["global", "lobby", "party", "guild", "match", "dm", "custom"]>;
11
+ readonly refId: Schema.String;
12
+ readonly name: Schema.String;
13
+ readonly slowModeMs: Schema.Number;
14
+ readonly retentionMs: Schema.NullOr<Schema.Number>;
15
+ readonly maxMembers: Schema.Number;
16
+ readonly frozen: Schema.Boolean;
17
+ readonly createdAt: Schema.Number;
18
+ readonly metadata: Schema.$Record<Schema.String, Schema.Unknown>;
19
+ }>;
20
+
21
+ declare class ChatError extends ChatError_base {
22
+ }
23
+
24
+ declare const ChatError_base: Schema.Class<ChatError, Schema.TaggedStruct<"ChatError", {
25
+ readonly code: Schema.Literals<readonly ["identity_required", "invalid_input", "not_found", "forbidden", "blocked", "sanctioned", "muted", "slow_mode", "rate_limited", "content_rejected", "conflict", "frozen", "member_limit", "unavailable", "export_limit"]>;
26
+ }>, YieldableError>;
27
+
28
+ declare interface ChatMediaInput {
29
+ readonly subject: Subject;
30
+ /** Facts read from plugin-storage for a ref the sender owns in this tenant. */
31
+ readonly ref: {
32
+ readonly id: string;
33
+ readonly contentType: string;
34
+ readonly size: number;
35
+ readonly width: number | null;
36
+ readonly height: number | null;
37
+ };
38
+ readonly contentTypes: ReadonlyArray<string>;
39
+ readonly maxBytes: number;
40
+ }
41
+
42
+ export declare const chatMediaModerationFixture: {
43
+ kind: "slot";
44
+ defaultImplementation: (input: ChatMediaInput) => Effect.Effect< ChatMediaVerdict, ChatError>;
45
+ defaultProbe: {
46
+ run: () => Effect.Effect<boolean, ChatError, never>;
47
+ expected: boolean;
48
+ };
49
+ overrideProbe: {
50
+ run: () => Effect.Effect<boolean, ChatError, never>;
51
+ expected: boolean;
52
+ };
53
+ };
54
+
55
+ declare interface ChatMediaVerdict {
56
+ readonly allowed: boolean;
57
+ readonly flagged: boolean;
58
+ }
59
+
60
+ declare interface ChatMentionInput {
61
+ readonly body: string;
62
+ readonly subject: Subject;
63
+ readonly maxMentions?: number;
64
+ }
65
+
66
+ export declare const chatMentionResolverFixture: {
67
+ kind: "slot";
68
+ defaultImplementation: (input: ChatMentionInput) => Effect.Effect<ReadonlyArray<string>, ChatError>;
69
+ defaultProbe: {
70
+ run: () => Effect.Effect<readonly string[], ChatError, never>;
71
+ expected: never[];
72
+ };
73
+ overrideProbe: {
74
+ run: () => Effect.Effect<readonly string[], ChatError, never>;
75
+ expected: string[];
76
+ };
77
+ };
78
+
79
+ export declare const chatModerationFixture: {
80
+ kind: "slot";
81
+ defaultImplementation: (input: ChatModerationInput) => Effect.Effect< ChatModerationResult, ChatError>;
82
+ defaultProbe: {
83
+ run: () => Effect.Effect<boolean, ChatError, never>;
84
+ expected: boolean;
85
+ };
86
+ overrideProbe: {
87
+ run: () => Effect.Effect<boolean, ChatError, never>;
88
+ expected: boolean;
89
+ };
90
+ };
91
+
92
+ declare interface ChatModerationInput {
93
+ readonly body: string;
94
+ readonly denyWords: ReadonlyArray<string>;
95
+ readonly action: 'block' | 'flag';
96
+ }
97
+
98
+ declare interface ChatModerationResult {
99
+ readonly body: string;
100
+ readonly flagged: boolean;
101
+ readonly allowed: boolean;
102
+ }
103
+
104
+ declare interface ChatRateInput {
105
+ readonly channel: ChatChannel;
106
+ readonly perChannel: number;
107
+ readonly global: number;
108
+ }
109
+
110
+ export declare const chatRateLimitFixture: {
111
+ kind: "slot";
112
+ defaultImplementation: (input: ChatRateInput) => {
113
+ readonly perChannel: number;
114
+ readonly global: number;
115
+ };
116
+ defaultProbe: {
117
+ run: () => Effect.Effect<number, never, never>;
118
+ expected: number;
119
+ };
120
+ overrideProbe: {
121
+ run: () => Effect.Effect<number, never, never>;
122
+ expected: number;
123
+ };
124
+ };
125
+
126
+ export declare const chatRetentionFixture: {
127
+ kind: "slot";
128
+ defaultImplementation: (channel: ChatChannel) => number | null;
129
+ defaultProbe: {
130
+ run: () => Effect.Effect<number | null, never, never>;
131
+ expected: number;
132
+ };
133
+ overrideProbe: {
134
+ run: () => Effect.Effect<number | null, never, never>;
135
+ expected: number;
136
+ };
137
+ };
138
+
139
+ export { }
@@ -0,0 +1,111 @@
1
+ import { channelChatRetention as e, fixedChatRateBudgets as t, keywordChatModeration as n, playerChatMentions as r, resolveChatOptions as i, typedChatMedia as a } from "./strategies.js";
2
+ import { Effect as o } from "effect";
3
+ //#region src/testing.ts
4
+ var s = {
5
+ id: "channel",
6
+ kind: "custom",
7
+ refId: "fixture",
8
+ name: "Fixture",
9
+ slowModeMs: 0,
10
+ retentionMs: 6048e5,
11
+ maxMembers: 2,
12
+ frozen: !1,
13
+ createdAt: 1,
14
+ metadata: {}
15
+ }, c = {
16
+ body: "blocked",
17
+ denyWords: ["blocked"],
18
+ action: "block"
19
+ }, l = {
20
+ kind: "slot",
21
+ defaultImplementation: n,
22
+ defaultProbe: {
23
+ run: () => n(c).pipe(o.map((e) => e.allowed)),
24
+ expected: !1
25
+ },
26
+ overrideProbe: {
27
+ run: () => i({ strategies: { moderate: (e) => n({
28
+ ...e,
29
+ action: "flag"
30
+ }) } }).strategies.moderate(c).pipe(o.map((e) => e.allowed)),
31
+ expected: !0
32
+ }
33
+ }, u = {
34
+ channel: s,
35
+ perChannel: 2,
36
+ global: 10
37
+ }, d = {
38
+ kind: "slot",
39
+ defaultImplementation: t,
40
+ defaultProbe: {
41
+ run: () => o.sync(() => t(u).perChannel),
42
+ expected: 2
43
+ },
44
+ overrideProbe: {
45
+ run: () => o.sync(() => i({ strategies: { rateLimit: (e) => ({
46
+ ...t(e),
47
+ perChannel: 1
48
+ }) } }).strategies.rateLimit(u).perChannel),
49
+ expected: 1
50
+ }
51
+ }, f = {
52
+ body: "@Player#0001",
53
+ subject: {
54
+ type: "user",
55
+ id: "one",
56
+ tenantId: "tenant"
57
+ }
58
+ }, p = {
59
+ kind: "slot",
60
+ defaultImplementation: r,
61
+ defaultProbe: {
62
+ run: () => r(f),
63
+ expected: []
64
+ },
65
+ overrideProbe: {
66
+ run: () => i({ strategies: { mentionResolver: () => o.succeed(["trusted-resolved-id"]) } }).strategies.mentionResolver(f),
67
+ expected: ["trusted-resolved-id"]
68
+ }
69
+ }, m = {
70
+ kind: "slot",
71
+ defaultImplementation: e,
72
+ defaultProbe: {
73
+ run: () => o.sync(() => e(s)),
74
+ expected: 6048e5
75
+ },
76
+ overrideProbe: {
77
+ run: () => o.sync(() => i({ strategies: { retention: () => 864e5 } }).strategies.retention(s)),
78
+ expected: 864e5
79
+ }
80
+ }, h = {
81
+ subject: {
82
+ type: "user",
83
+ id: "one",
84
+ tenantId: "tenant"
85
+ },
86
+ ref: {
87
+ id: "file_fixture",
88
+ contentType: "image/png",
89
+ size: 1024,
90
+ width: 8,
91
+ height: 8
92
+ },
93
+ contentTypes: ["image/png"],
94
+ maxBytes: 2048
95
+ }, g = {
96
+ kind: "slot",
97
+ defaultImplementation: a,
98
+ defaultProbe: {
99
+ run: () => a(h).pipe(o.map((e) => e.allowed)),
100
+ expected: !0
101
+ },
102
+ overrideProbe: {
103
+ run: () => i({ strategies: { moderateMedia: () => o.succeed({
104
+ allowed: !1,
105
+ flagged: !0
106
+ }) } }).strategies.moderateMedia(h).pipe(o.map((e) => e.allowed)),
107
+ expected: !1
108
+ }
109
+ };
110
+ //#endregion
111
+ export { g as chatMediaModerationFixture, p as chatMentionResolverFixture, l as chatModerationFixture, d as chatRateLimitFixture, m as chatRetentionFixture };
package/dist/typing.js ADDED
@@ -0,0 +1,88 @@
1
+ import { chatError as e } from "./errors.js";
2
+ import { CHAT_TYPING_TTL_MS as t, ChatTypingSchema as n } from "./contract.js";
3
+ import { createTypingInvalidation as r } from "./typingInvalidation.js";
4
+ import { typing as i } from "./events.js";
5
+ import { Effect as a, Schema as o } from "effect";
6
+ import { createHash as s } from "node:crypto";
7
+ //#region src/typing.ts
8
+ var c = (c) => {
9
+ let l = (t) => {
10
+ if (!t || typeof t != "object" || Array.isArray(t)) throw e("invalid_input");
11
+ let n = Object.getOwnPropertyDescriptor(t, "channelId");
12
+ if (!n || !("value" in n) || typeof n.value != "string" || !n.value.trim() || n.value.length > 256 || n.value.includes("\0") || n.value === "~") throw e("invalid_input");
13
+ return n.value;
14
+ }, u = (t) => a.try({
15
+ try: t,
16
+ catch: () => e("invalid_input")
17
+ }), d = (n, r) => {
18
+ let i, o, d;
19
+ try {
20
+ i = structuredClone(n), o = l(r);
21
+ let t = Object.getOwnPropertyDescriptor(r, "active");
22
+ if (!t || !("value" in t) || typeof t.value != "boolean") throw e("invalid_input");
23
+ d = t.value;
24
+ } catch {
25
+ return a.fail(e("invalid_input"));
26
+ }
27
+ return a.gen(function* () {
28
+ let n = yield* c.readAccess(i, o), r = yield* u(c.now), l = yield* c.social;
29
+ if (n.channel.kind === "dm" && !l) return yield* a.fail(e("forbidden"));
30
+ if (d) {
31
+ if (n.channel.frozen) return yield* a.fail(e("frozen"));
32
+ if (n.member.mutedUntil !== null && n.member.mutedUntil > r) return yield* a.fail(e("muted"));
33
+ yield* c.sanctioned(i, o);
34
+ }
35
+ let f = c.options.strategies.rateLimit({
36
+ channel: n.channel,
37
+ perChannel: c.options.perChannelRate,
38
+ global: c.options.globalRate
39
+ });
40
+ if (![f.perChannel, f.global].every((e) => Number.isSafeInteger(e) && e > 0 && e <= 1e4)) return yield* a.fail(e("unavailable"));
41
+ for (let [t, l] of [[[
42
+ i.tenantId,
43
+ n.member.subjectId,
44
+ o
45
+ ], f.perChannel], [[i.tenantId, n.member.subjectId], f.global]]) {
46
+ let n = s("sha256").update(JSON.stringify(t)).digest("hex"), i = yield* c.limiter.consume("chat:typing:" + n, {
47
+ limit: l,
48
+ windowMs: 1e3,
49
+ algorithm: "fixed-window",
50
+ burst: l
51
+ }, r).pipe(a.mapError(() => e("unavailable")), a.catchDefect(() => a.fail(e("unavailable"))));
52
+ if (i.failOpenReason !== void 0) return yield* a.fail(e("unavailable"));
53
+ if (!i.allowed) return yield* a.fail(e("rate_limited"));
54
+ }
55
+ let p = d ? r + t : r;
56
+ return !Number.isSafeInteger(p) || p > 864e13 ? yield* a.fail(e("unavailable")) : Object.freeze({
57
+ subjectId: n.member.subjectId,
58
+ active: d,
59
+ expiresAt: p
60
+ });
61
+ }).pipe(a.catchDefect(() => a.fail(e("unavailable"))));
62
+ }, f = (e) => a.gen(function* () {
63
+ if (e.event !== i.name) return !1;
64
+ let t = yield* u(() => l(e.key));
65
+ return yield* c.readAccess(e.subject, t), !0;
66
+ }).pipe(a.catch(() => a.succeed(!1)), a.catchDefect(() => a.succeed(!1))), p = (t) => a.gen(function* () {
67
+ if (t.event !== i.name) return "deny";
68
+ let r = yield* u(() => l(t.key)), s = yield* o.decodeUnknownEffect(n)(t.payload).pipe(a.mapError(() => e("invalid_input"))), d = yield* c.readAccess(t.subject, r, s.subjectId), f = yield* u(c.now);
69
+ if (s.subjectId === d.member.subjectId || d.peer === null || !Number.isSafeInteger(s.expiresAt) || s.expiresAt > f + 5e3 || s.expiresAt < f - 5e3 || s.active && s.expiresAt <= f || s.active && (d.channel.frozen || d.peer.mutedUntil !== null && d.peer.mutedUntil > f)) return "drop";
70
+ let p = yield* c.social;
71
+ return d.channel.kind === "dm" && !p ? "deny" : p && (yield* p.isBlocked(t.subject, s.subjectId).pipe(a.mapError(() => e("unavailable")))) ? "drop" : "allow";
72
+ }).pipe(a.catch(() => a.succeed("deny")), a.catchDefect(() => a.succeed("deny"))), m = r(c.store);
73
+ return {
74
+ prepare: d,
75
+ policy: {
76
+ id: i.clientPolicy,
77
+ admit: f,
78
+ deliver: p,
79
+ monitor: (t) => u(() => {
80
+ if (t.event !== i.name) throw e("forbidden");
81
+ return l(t.key);
82
+ }).pipe(a.flatMap((e) => m(structuredClone(t.subject), e)))
83
+ },
84
+ close: m.close
85
+ };
86
+ };
87
+ //#endregion
88
+ export { c as createChatTyping };
@@ -0,0 +1,86 @@
1
+ import { chatError as e } from "./errors.js";
2
+ import "./contract.js";
3
+ import { Deferred as t, Effect as n } from "effect";
4
+ import { socialRelationsTable as r } from "@voltro/plugin-social";
5
+ //#region src/typingInvalidation.ts
6
+ var i = (e, t) => e instanceof Date && t instanceof Date ? e.getTime() === t.getTime() : e === t, a = (e, t, n) => e !== null && t !== null && n.every((n) => i(e[n], t[n])), o = (i) => {
7
+ let o = /* @__PURE__ */ new Set(), s = !1, c, l, u = (e, t) => {
8
+ if (![
9
+ "_voltro_chat_channels",
10
+ "_voltro_chat_members",
11
+ r.tableName
12
+ ].includes(e.table)) return !1;
13
+ if (e.oversized !== void 0 || e.netChange || !e.old && !e.new) return !0;
14
+ let n = [e.old, e.new].filter((e) => e !== null);
15
+ return n.some((e) => e.tenantId === t.tenant || e.tenantId === void 0) ? e.table === "_voltro_chat_channels" ? !a(e.old, e.new, [
16
+ "id",
17
+ "tenantId",
18
+ "kind",
19
+ "frozen"
20
+ ]) && n.some((e) => e.id === t.channel || e.id === void 0) : e.table === "_voltro_chat_members" ? !a(e.old, e.new, [
21
+ "id",
22
+ "tenantId",
23
+ "channelId",
24
+ "subjectId",
25
+ "role",
26
+ "mutedUntil"
27
+ ]) && n.some((e) => e.channelId === t.channel || e.channelId === void 0) : n.some((e) => (e.kind === "block" || e.kind === void 0) && (e.fromId === t.subject || e.toId === t.subject || e.fromId === void 0 || e.toId === void 0)) : !1;
28
+ }, d = (e) => {
29
+ if (o.delete(e), o.size === 0) {
30
+ let e = c, t = l;
31
+ c = void 0, l = void 0;
32
+ try {
33
+ e?.();
34
+ } finally {
35
+ t?.();
36
+ }
37
+ }
38
+ }, f = (e) => {
39
+ try {
40
+ d(e);
41
+ } finally {
42
+ e.revoke();
43
+ }
44
+ };
45
+ return Object.assign((r, a) => n.gen(function* () {
46
+ if (r.type !== "user") return yield* n.fail(e("identity_required"));
47
+ let p = yield* t.make();
48
+ return yield* n.acquireRelease(n.try({
49
+ try: () => {
50
+ if (s || o.size >= 4096) throw e("unavailable");
51
+ let m = {
52
+ tenant: r.tenantId,
53
+ subject: r.id,
54
+ channel: a,
55
+ revoke: () => {
56
+ n.runSync(t.succeed(p, void 0));
57
+ }
58
+ };
59
+ o.add(m);
60
+ try {
61
+ c ??= i.onChange((e) => {
62
+ for (let t of [...o]) u(e, t) && f(t);
63
+ }), i.onChangeStreamGap !== void 0 && l === void 0 && (l = i.onChangeStreamGap(() => {
64
+ for (let e of [...o]) f(e);
65
+ })), o.size === 0 && d(m);
66
+ } catch {
67
+ throw d(m), e("unavailable");
68
+ }
69
+ return m;
70
+ },
71
+ catch: () => e("unavailable")
72
+ }), (e) => n.sync(() => d(e))), t.await(p);
73
+ }), { close: () => {
74
+ if (s) return;
75
+ s = !0;
76
+ let t = !1;
77
+ for (let e of [...o]) try {
78
+ f(e);
79
+ } catch {
80
+ t = !0;
81
+ }
82
+ if (t) throw e("unavailable");
83
+ } });
84
+ };
85
+ //#endregion
86
+ export { o as createTypingInvalidation };