@helyx/bot 1.5.2 → 1.5.4
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/CHANGELOG.md +27 -0
- package/command-root-catalogue.json +452 -0
- package/dist/cached-message-deletion-authority.d.ts +27 -0
- package/dist/cached-message-deletion-authority.js +288 -0
- package/dist/channel-audit-configuration.d.ts +11 -1
- package/dist/channel-audit-configuration.js +28 -6
- package/dist/channel-audit-control.js +42 -2
- package/dist/channel-audit-permissions.js +76 -3
- package/dist/cloud-command-policy-sync.d.ts +26 -0
- package/dist/cloud-command-policy-sync.js +198 -0
- package/dist/cloud-connector.d.ts +6 -1
- package/dist/cloud-connector.js +224 -53
- package/dist/cloud-control-dispatcher.js +18 -0
- package/dist/command-policy-runtime.d.ts +23 -0
- package/dist/command-policy-runtime.js +53 -0
- package/dist/command-root-catalogue.d.ts +5 -0
- package/dist/command-root-catalogue.js +16 -0
- package/dist/control-error-mapping.d.ts +8 -0
- package/dist/control-error-mapping.js +60 -0
- package/dist/control-errors.d.ts +3 -1
- package/dist/control-errors.js +4 -1
- package/dist/control-health-routes.d.ts +10 -0
- package/dist/control-health-routes.js +28 -0
- package/dist/control-server-schemas.d.ts +7 -0
- package/dist/control-server-schemas.js +9 -0
- package/dist/control-server.d.ts +27 -6
- package/dist/control-server.js +103 -122
- package/dist/dashboard-feature-control.d.ts +2 -1
- package/dist/dashboard-feature-control.js +1 -0
- package/dist/dashboard-logging.d.ts +20 -0
- package/dist/dashboard-logging.js +39 -0
- package/dist/dashboard-module-projection.d.ts +1 -1
- package/dist/dashboard-module-projection.js +5 -2
- package/dist/dashboard-preview-import-routes.js +8 -0
- package/dist/database-bootstrap.d.ts +1 -1
- package/dist/database-bootstrap.js +4 -3
- package/dist/discord-channel-audit.js +1 -0
- package/dist/discord-user-identities.d.ts +14 -0
- package/dist/discord-user-identities.js +64 -0
- package/dist/emergency-deletion-dispatcher.d.ts +68 -0
- package/dist/emergency-deletion-dispatcher.js +502 -0
- package/dist/emergency-deletion-rest-transport.d.ts +24 -0
- package/dist/emergency-deletion-rest-transport.js +253 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +53 -10
- package/dist/guarded-discord-dispatcher.d.ts +29 -0
- package/dist/guarded-discord-dispatcher.js +204 -0
- package/dist/guild-command-discord-client.d.ts +2 -0
- package/dist/guild-command-discord-client.js +7 -0
- package/dist/guild-command-projection.d.ts +4 -1
- package/dist/guild-command-projection.js +25 -0
- package/dist/guild-command-read-service.d.ts +10 -1
- package/dist/guild-command-read-service.js +37 -0
- package/dist/guild-command-worker.d.ts +19 -4
- package/dist/guild-command-worker.js +356 -26
- package/dist/guild-message-protection-author-roles.d.ts +7 -0
- package/dist/guild-message-protection-author-roles.js +93 -0
- package/dist/guild-message-protection-deletion.d.ts +21 -0
- package/dist/guild-message-protection-deletion.js +138 -0
- package/dist/guild-message-protection-delivery.d.ts +41 -0
- package/dist/guild-message-protection-delivery.js +223 -0
- package/dist/guild-message-update-metadata.d.ts +32 -0
- package/dist/guild-message-update-metadata.js +255 -0
- package/dist/guilds.d.ts +2 -1
- package/dist/guilds.js +11 -1
- package/dist/hosted-server-moderation.d.ts +2 -2
- package/dist/index.js +68 -4
- package/dist/interactions.d.ts +3 -1
- package/dist/interactions.js +77 -12
- package/dist/invite-rollup-maintenance.d.ts +9 -0
- package/dist/invite-rollup-maintenance.js +12 -0
- package/dist/managed-resource-authorization.d.ts +15 -0
- package/dist/managed-resource-authorization.js +37 -0
- package/dist/managed-resource-create-defaults.d.ts +21 -0
- package/dist/managed-resource-create-defaults.js +83 -0
- package/dist/managed-resource-query.d.ts +6 -0
- package/dist/managed-resource-query.js +15 -0
- package/dist/member-role-ids.d.ts +17 -0
- package/dist/member-role-ids.js +66 -0
- package/dist/message-protection-admission.d.ts +60 -0
- package/dist/message-protection-admission.js +279 -0
- package/dist/message-protection-cold-data.d.ts +42 -0
- package/dist/message-protection-cold-data.js +180 -0
- package/dist/message-protection-coordinator.d.ts +73 -0
- package/dist/message-protection-coordinator.js +622 -0
- package/dist/message-protection-settings.d.ts +40 -0
- package/dist/message-protection-settings.js +275 -0
- package/dist/message-protection-summary.d.ts +8 -0
- package/dist/message-protection-summary.js +98 -0
- package/dist/module-availability-control.js +20 -12
- package/dist/module-logging.d.ts +3 -2
- package/dist/module-logging.js +6 -1
- package/dist/modules.js +56 -1
- package/dist/mysql-runtime-persistence.d.ts +1 -0
- package/dist/mysql-runtime-persistence.js +4 -1
- package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
- package/dist/nexus-command-recovery-control-routes.js +48 -0
- package/dist/nexus-command-references.d.ts +6 -0
- package/dist/nexus-command-references.js +33 -0
- package/dist/scheduled-task-runner.js +23 -4
- package/dist/shard-command-startup.d.ts +13 -0
- package/dist/shard-command-startup.js +27 -0
- package/dist/shard-contracts.d.ts +84 -0
- package/dist/shard-contracts.js +33 -0
- package/dist/shard-core-readiness.d.ts +5 -0
- package/dist/shard-core-readiness.js +19 -0
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +10 -1
- package/dist/shard-supervisor.d.ts +1 -1
- package/dist/shard-supervisor.js +124 -43
- package/dist/shard-worker.js +25 -15
- package/documentation/README.md +1 -0
- package/documentation/examples/.env.example +0 -45
- package/documentation/guides/self-hosting.md +54 -0
- package/package.json +18 -17
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { captureCachedMessageDeletionAuthority, } from "./cached-message-deletion-authority.js";
|
|
2
|
+
/** Separate lexical scope: queued grants never capture an execute/Message environment. */
|
|
3
|
+
export function createProtectionDeliveryLifetime() {
|
|
4
|
+
let stopped = false;
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
isStopped: () => stopped,
|
|
7
|
+
stop: () => {
|
|
8
|
+
stopped = true;
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/** Explicit private composition only; never derive configuration from mutable client identity. */
|
|
13
|
+
export function createMessageProtectionAuthorityFactory(client, expectedApplicationId, eligibility, deletion) {
|
|
14
|
+
if (expectedApplicationId === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (typeof expectedApplicationId !== "string" ||
|
|
17
|
+
!/^\d{17,20}$/u.test(expectedApplicationId))
|
|
18
|
+
throw new TypeError("Invalid protection application identity");
|
|
19
|
+
if (!eligibility || !deletion)
|
|
20
|
+
return undefined;
|
|
21
|
+
return (source, isStopped) => captureCachedMessageDeletionAuthority({
|
|
22
|
+
client,
|
|
23
|
+
expectedApplicationId,
|
|
24
|
+
eligibility,
|
|
25
|
+
isStopped,
|
|
26
|
+
source,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const expired = Promise.resolve(Object.freeze({ outcome: "expired-event", issued: false, attempts: 0 }));
|
|
30
|
+
const unavailable = Promise.resolve(Object.freeze({ outcome: "unavailable", issued: false, attempts: 0 }));
|
|
31
|
+
const unknownFailure = Promise.resolve(Object.freeze({ outcome: "failed", issued: "unknown", attempts: null }));
|
|
32
|
+
/** Handler-local owner; only its content-free grant is handed to the queue. */
|
|
33
|
+
export function createProtectionDeletionOperation(input, deletion, strict, captured, isActive) {
|
|
34
|
+
let owned = captured ? ownOnce(captured) : undefined;
|
|
35
|
+
let result;
|
|
36
|
+
let entering = false;
|
|
37
|
+
let inspecting = false;
|
|
38
|
+
const releaseUnused = () => {
|
|
39
|
+
const unused = owned;
|
|
40
|
+
owned = undefined;
|
|
41
|
+
unused?.stop();
|
|
42
|
+
};
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
releaseUnused,
|
|
45
|
+
invoke: () => {
|
|
46
|
+
if (inspecting)
|
|
47
|
+
return unavailable;
|
|
48
|
+
let active = false;
|
|
49
|
+
try {
|
|
50
|
+
inspecting = true;
|
|
51
|
+
active = isActive() === true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* Unknown event admission fails closed. */
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
inspecting = false;
|
|
58
|
+
}
|
|
59
|
+
if (!active) {
|
|
60
|
+
releaseUnused();
|
|
61
|
+
return expired;
|
|
62
|
+
}
|
|
63
|
+
if (result)
|
|
64
|
+
return result;
|
|
65
|
+
if (entering)
|
|
66
|
+
return unavailable;
|
|
67
|
+
if (!deletion || (strict && !owned)) {
|
|
68
|
+
releaseUnused();
|
|
69
|
+
return (result = unavailable);
|
|
70
|
+
}
|
|
71
|
+
const provided = owned;
|
|
72
|
+
owned = undefined;
|
|
73
|
+
entering = true;
|
|
74
|
+
try {
|
|
75
|
+
const handle = deletion.enqueueCaptured(input, provided);
|
|
76
|
+
// Store before releasing unused authority: stop may synchronously reenter.
|
|
77
|
+
result = handle.result;
|
|
78
|
+
if (!handle.retainedProvidedAuthority)
|
|
79
|
+
provided?.stop();
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// The actual queue returns a safely fulfilled shared result; this is a sync port failure.
|
|
84
|
+
result = unknownFailure;
|
|
85
|
+
provided?.stop();
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
entering = false;
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function ownOnce(captured) {
|
|
95
|
+
try {
|
|
96
|
+
const original = captured.scope;
|
|
97
|
+
const scope = Object.freeze({
|
|
98
|
+
guildId: original.guildId,
|
|
99
|
+
moduleId: original.moduleId,
|
|
100
|
+
channelId: original.channelId,
|
|
101
|
+
messageId: original.messageId,
|
|
102
|
+
admissionToken: original.admissionToken,
|
|
103
|
+
});
|
|
104
|
+
const current = captured.isCurrent, stop = captured.stop;
|
|
105
|
+
if (typeof current !== "function" || typeof stop !== "function")
|
|
106
|
+
return undefined;
|
|
107
|
+
let released = false;
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
scope,
|
|
110
|
+
isCurrent: () => {
|
|
111
|
+
if (released)
|
|
112
|
+
return false;
|
|
113
|
+
try {
|
|
114
|
+
const valid = current();
|
|
115
|
+
return !released && valid === true;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
stop: () => {
|
|
122
|
+
if (released)
|
|
123
|
+
return;
|
|
124
|
+
released = true;
|
|
125
|
+
try {
|
|
126
|
+
stop();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
/* Cleanup cannot replace an actual outcome. */
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=guild-message-protection-deletion.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type GuildMessageCreateContribution, type ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
import type { Message } from "discord.js";
|
|
3
|
+
import type { EmergencyDeletionDispatcher } from "./emergency-deletion-dispatcher.js";
|
|
4
|
+
import { type MessageProtectionAuthorityFactory } from "./guild-message-protection-deletion.js";
|
|
5
|
+
/** Trusted runtime memory only. No SQL, Redis or network fallback is permitted. */
|
|
6
|
+
export interface MessageProtectionEligibility {
|
|
7
|
+
read(this: void, guildId: string, moduleId: string): {
|
|
8
|
+
state: "enabled";
|
|
9
|
+
generation: string;
|
|
10
|
+
} | {
|
|
11
|
+
state: "disabled" | "unavailable";
|
|
12
|
+
};
|
|
13
|
+
isCurrent(this: void, generation: string): boolean;
|
|
14
|
+
}
|
|
15
|
+
interface Recipient {
|
|
16
|
+
moduleId: string;
|
|
17
|
+
classificationRequested: boolean;
|
|
18
|
+
deletionAllowed: boolean;
|
|
19
|
+
contribution: GuildMessageCreateContribution;
|
|
20
|
+
}
|
|
21
|
+
interface Admission {
|
|
22
|
+
recipient: Recipient;
|
|
23
|
+
generation: string;
|
|
24
|
+
}
|
|
25
|
+
/** Owns the exact-event lifetime; never exposes the private source object. */
|
|
26
|
+
export declare class GuildMessageProtectionDelivery {
|
|
27
|
+
#private;
|
|
28
|
+
private readonly eligibility;
|
|
29
|
+
private readonly services;
|
|
30
|
+
private readonly contentIntent;
|
|
31
|
+
private readonly ownUserId;
|
|
32
|
+
private readonly ownApplicationId;
|
|
33
|
+
private readonly deletion?;
|
|
34
|
+
private readonly authorityFactory?;
|
|
35
|
+
constructor(eligibility: MessageProtectionEligibility | undefined, services: ServiceAccess, contentIntent: boolean, ownUserId: () => string | undefined, ownApplicationId: () => string | undefined, deletion?: EmergencyDeletionDispatcher | undefined, authorityFactory?: MessageProtectionAuthorityFactory | undefined);
|
|
36
|
+
select(message: Message, recipients: readonly Recipient[]): Admission[];
|
|
37
|
+
execute(message: Message, admissions: readonly Admission[], onError: (moduleId: string, error: unknown) => void): Promise<void>;
|
|
38
|
+
stop(): void;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
41
|
+
//# sourceMappingURL=guild-message-protection-delivery.d.ts.map
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { HELYX_SERVICE_NAMES, } from "@helyx/sdk";
|
|
2
|
+
import { snapshotProtectionAuthorRoles } from "./guild-message-protection-author-roles.js";
|
|
3
|
+
import { createProtectionDeletionOperation, createProtectionDeliveryLifetime, } from "./guild-message-protection-deletion.js";
|
|
4
|
+
import { summarizeProtectionMessage, unavailableProtectionSnapshot, } from "./message-protection-summary.js";
|
|
5
|
+
/** Owns the exact-event lifetime; never exposes the private source object. */
|
|
6
|
+
export class GuildMessageProtectionDelivery {
|
|
7
|
+
eligibility;
|
|
8
|
+
services;
|
|
9
|
+
contentIntent;
|
|
10
|
+
ownUserId;
|
|
11
|
+
ownApplicationId;
|
|
12
|
+
deletion;
|
|
13
|
+
authorityFactory;
|
|
14
|
+
#lifetime = createProtectionDeliveryLifetime();
|
|
15
|
+
#revoke = new Set();
|
|
16
|
+
constructor(eligibility, services, contentIntent, ownUserId, ownApplicationId, deletion, authorityFactory) {
|
|
17
|
+
this.eligibility = eligibility;
|
|
18
|
+
this.services = services;
|
|
19
|
+
this.contentIntent = contentIntent;
|
|
20
|
+
this.ownUserId = ownUserId;
|
|
21
|
+
this.ownApplicationId = ownApplicationId;
|
|
22
|
+
this.deletion = deletion;
|
|
23
|
+
this.authorityFactory = authorityFactory;
|
|
24
|
+
}
|
|
25
|
+
select(message, recipients) {
|
|
26
|
+
if (!this.#safeSource(message))
|
|
27
|
+
return [];
|
|
28
|
+
return recipients.flatMap((recipient) => {
|
|
29
|
+
const options = recipient.contribution.protection;
|
|
30
|
+
if (!options ||
|
|
31
|
+
(message.webhookId
|
|
32
|
+
? !options.webhooks
|
|
33
|
+
: message.author.bot && !options.otherBots))
|
|
34
|
+
return [];
|
|
35
|
+
const generation = this.#generation(message.guildId, recipient.moduleId);
|
|
36
|
+
return generation ? [{ recipient, generation }] : [];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async execute(message, admissions, onError) {
|
|
40
|
+
const guildId = message.guildId;
|
|
41
|
+
let snapshot;
|
|
42
|
+
for (const { recipient, generation } of admissions) {
|
|
43
|
+
const current = () => !this.#lifetime.isStopped() &&
|
|
44
|
+
Boolean(guildId) &&
|
|
45
|
+
this.#generation(guildId, recipient.moduleId) === generation;
|
|
46
|
+
if (!this.#safeSource(message) || !current())
|
|
47
|
+
continue;
|
|
48
|
+
const { moduleId, contribution, classificationRequested } = recipient;
|
|
49
|
+
let source = message;
|
|
50
|
+
let active = true;
|
|
51
|
+
let deletionOperation;
|
|
52
|
+
const revoke = () => {
|
|
53
|
+
active = false;
|
|
54
|
+
source = undefined;
|
|
55
|
+
deletionOperation?.releaseUnused();
|
|
56
|
+
};
|
|
57
|
+
this.#revoke.add(revoke);
|
|
58
|
+
try {
|
|
59
|
+
const { channelId, id: messageId } = message;
|
|
60
|
+
const authorId = message.author.id;
|
|
61
|
+
const webhook = Boolean(message.webhookId);
|
|
62
|
+
const authorRoles = snapshotProtectionAuthorRoles(message, webhook);
|
|
63
|
+
const thread = message.channel.isThread();
|
|
64
|
+
const parentChannelId = thread
|
|
65
|
+
? (message.channel.parentId ?? undefined)
|
|
66
|
+
: undefined;
|
|
67
|
+
const categoryId = thread
|
|
68
|
+
? (message.channel.parent?.parentId ?? undefined)
|
|
69
|
+
: "parentId" in message.channel
|
|
70
|
+
? (message.channel.parentId ?? undefined)
|
|
71
|
+
: undefined;
|
|
72
|
+
const context = {
|
|
73
|
+
serverId: guildId,
|
|
74
|
+
channelId,
|
|
75
|
+
messageId,
|
|
76
|
+
...(parentChannelId ? { parentChannelId } : {}),
|
|
77
|
+
...(categoryId ? { categoryId } : {}),
|
|
78
|
+
isReply: Boolean(message.reference?.messageId),
|
|
79
|
+
authorId,
|
|
80
|
+
authorDisplayName: (message.member?.displayName ??
|
|
81
|
+
message.author.globalName ??
|
|
82
|
+
message.author.username).slice(0, 128),
|
|
83
|
+
...authorRoles,
|
|
84
|
+
authorIsBot: message.author.bot,
|
|
85
|
+
authorIsWebhook: webhook,
|
|
86
|
+
authorIsSystem: false,
|
|
87
|
+
mentionedUserIds: Object.freeze(boundedIds(message.mentions.users.keys(), 25)),
|
|
88
|
+
classification: Object.freeze({
|
|
89
|
+
available: false,
|
|
90
|
+
kinds: [],
|
|
91
|
+
}),
|
|
92
|
+
createdAt: new Date(message.createdAt.getTime()),
|
|
93
|
+
services: this.services,
|
|
94
|
+
addAuthorRole: async (roleId) => {
|
|
95
|
+
await this.services
|
|
96
|
+
.get(HELYX_SERVICE_NAMES.memberRoles)
|
|
97
|
+
.addApprovedRole({
|
|
98
|
+
guildId: guildId,
|
|
99
|
+
memberUserId: authorId,
|
|
100
|
+
roleId,
|
|
101
|
+
moduleId,
|
|
102
|
+
reason: "Guild message contribution author role addition",
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
removeAuthorRole: async (roleId) => {
|
|
106
|
+
await this.services
|
|
107
|
+
.get(HELYX_SERVICE_NAMES.memberRoles)
|
|
108
|
+
.removeApprovedRole({
|
|
109
|
+
guildId: guildId,
|
|
110
|
+
memberUserId: authorId,
|
|
111
|
+
roleId,
|
|
112
|
+
moduleId,
|
|
113
|
+
reason: "Guild message contribution author role removal",
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
sendToChannel: (targetChannelId, response) => this.services
|
|
117
|
+
.get(HELYX_SERVICE_NAMES.discordMessages)
|
|
118
|
+
.send({
|
|
119
|
+
guildId: guildId,
|
|
120
|
+
channelId: targetChannelId,
|
|
121
|
+
response,
|
|
122
|
+
}),
|
|
123
|
+
};
|
|
124
|
+
if (contribution.protection?.summary)
|
|
125
|
+
context.getProtectionSnapshot = () => {
|
|
126
|
+
if (!active || !source || !current()) {
|
|
127
|
+
revoke();
|
|
128
|
+
return unavailableProtectionSnapshot("expired-event");
|
|
129
|
+
}
|
|
130
|
+
if (!classificationRequested || !this.contentIntent)
|
|
131
|
+
return unavailableProtectionSnapshot("capability-unavailable");
|
|
132
|
+
snapshot ??= summarizeProtectionMessage(source);
|
|
133
|
+
return snapshot;
|
|
134
|
+
};
|
|
135
|
+
if (contribution.protection?.deleteMessage &&
|
|
136
|
+
recipient.deletionAllowed) {
|
|
137
|
+
const input = Object.freeze({
|
|
138
|
+
guildId: guildId,
|
|
139
|
+
moduleId,
|
|
140
|
+
channelId,
|
|
141
|
+
messageId,
|
|
142
|
+
admissionToken: generation,
|
|
143
|
+
});
|
|
144
|
+
let authority;
|
|
145
|
+
if (this.authorityFactory && this.deletion) {
|
|
146
|
+
try {
|
|
147
|
+
if (message.inGuild())
|
|
148
|
+
authority = this.authorityFactory(Object.freeze({ ...input, channel: message.channel }), this.#lifetime.isStopped);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* Strict capture failure never falls back to primitive-only enqueue. */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
deletionOperation = createProtectionDeletionOperation(input, this.deletion, this.authorityFactory !== undefined, authority, () => active && current());
|
|
155
|
+
context.deleteMessage = deletionOperation.invoke;
|
|
156
|
+
}
|
|
157
|
+
if (!current())
|
|
158
|
+
continue;
|
|
159
|
+
await contribution.execute(Object.freeze(context));
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
onError(moduleId, error);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
revoke();
|
|
166
|
+
this.#revoke.delete(revoke);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
stop() {
|
|
171
|
+
this.#lifetime.stop();
|
|
172
|
+
for (const revoke of this.#revoke)
|
|
173
|
+
revoke();
|
|
174
|
+
this.#revoke.clear();
|
|
175
|
+
}
|
|
176
|
+
#generation(guildId, moduleId) {
|
|
177
|
+
try {
|
|
178
|
+
if (this.#lifetime.isStopped())
|
|
179
|
+
return undefined;
|
|
180
|
+
const admission = this.eligibility?.read(guildId, moduleId);
|
|
181
|
+
if (this.#lifetime.isStopped() || admission?.state !== "enabled")
|
|
182
|
+
return undefined;
|
|
183
|
+
const generation = admission.generation;
|
|
184
|
+
const current = generation && this.eligibility?.isCurrent(generation);
|
|
185
|
+
return !this.#lifetime.isStopped() && current === true
|
|
186
|
+
? generation
|
|
187
|
+
: undefined;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
#safeSource(message) {
|
|
194
|
+
const ownId = this.ownUserId();
|
|
195
|
+
const applicationId = this.ownApplicationId();
|
|
196
|
+
return Boolean(!this.#lifetime.isStopped() &&
|
|
197
|
+
ownId &&
|
|
198
|
+
applicationId &&
|
|
199
|
+
message.guildId &&
|
|
200
|
+
message.author?.id &&
|
|
201
|
+
typeof message.author.bot === "boolean" &&
|
|
202
|
+
typeof message.author.system === "boolean" &&
|
|
203
|
+
typeof message.system === "boolean" &&
|
|
204
|
+
(message.webhookId === null || typeof message.webhookId === "string") &&
|
|
205
|
+
(message.applicationId === null ||
|
|
206
|
+
typeof message.applicationId === "string") &&
|
|
207
|
+
!message.author.system &&
|
|
208
|
+
!message.system &&
|
|
209
|
+
message.author.id !== ownId &&
|
|
210
|
+
message.applicationId !== applicationId &&
|
|
211
|
+
message.webhookId !== applicationId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function boundedIds(ids, limit) {
|
|
215
|
+
const result = [];
|
|
216
|
+
for (const id of ids) {
|
|
217
|
+
if (result.length === limit)
|
|
218
|
+
break;
|
|
219
|
+
result.push(id);
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=guild-message-protection-delivery.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { GuildMessageUpdateMetadataContext, GuildMessageUpdateMetadataContribution } from "@helyx/sdk";
|
|
2
|
+
import type { Client, Message, PartialMessage } from "discord.js";
|
|
3
|
+
import type { MessageProtectionEligibility } from "./guild-message-protection-delivery.js";
|
|
4
|
+
interface Recipient {
|
|
5
|
+
readonly moduleId: string;
|
|
6
|
+
readonly contribution: GuildMessageUpdateMetadataContribution;
|
|
7
|
+
}
|
|
8
|
+
interface Admission extends Recipient {
|
|
9
|
+
readonly generation: string;
|
|
10
|
+
}
|
|
11
|
+
interface Capture {
|
|
12
|
+
readonly context: Readonly<GuildMessageUpdateMetadataContext>;
|
|
13
|
+
current(): boolean;
|
|
14
|
+
}
|
|
15
|
+
/** Dormant metadata only. Queued tasks never retain the mutable source Message. */
|
|
16
|
+
export declare class GuildMessageUpdateMetadataDelivery {
|
|
17
|
+
#private;
|
|
18
|
+
private readonly client;
|
|
19
|
+
private readonly expectedApplicationId;
|
|
20
|
+
private readonly eligibility;
|
|
21
|
+
constructor(client: Client, expectedApplicationId: string | undefined, eligibility: MessageProtectionEligibility | undefined);
|
|
22
|
+
prepare(message: Message | PartialMessage, recipients: readonly Recipient[], onError: (moduleId: string, error: unknown) => void): {
|
|
23
|
+
guildId: string;
|
|
24
|
+
run(this: void): Promise<void>;
|
|
25
|
+
} | undefined;
|
|
26
|
+
execute(capture: Capture, admissions: readonly Admission[], onError: (moduleId: string, error: unknown) => void): Promise<void>;
|
|
27
|
+
stop(): void;
|
|
28
|
+
private generation;
|
|
29
|
+
private capture;
|
|
30
|
+
}
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=guild-message-update-metadata.d.ts.map
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { ChannelType } from "discord.js";
|
|
2
|
+
import { snapshotProtectionAuthorRoles } from "./guild-message-protection-author-roles.js";
|
|
3
|
+
const snowflake = /^\d{17,20}$/u;
|
|
4
|
+
/** Dormant metadata only. Queued tasks never retain the mutable source Message. */
|
|
5
|
+
export class GuildMessageUpdateMetadataDelivery {
|
|
6
|
+
client;
|
|
7
|
+
expectedApplicationId;
|
|
8
|
+
eligibility;
|
|
9
|
+
#stopped = false;
|
|
10
|
+
constructor(client, expectedApplicationId, eligibility) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
this.expectedApplicationId = expectedApplicationId;
|
|
13
|
+
this.eligibility = eligibility;
|
|
14
|
+
}
|
|
15
|
+
prepare(message, recipients, onError) {
|
|
16
|
+
if (this.#stopped || !recipients.length)
|
|
17
|
+
return undefined;
|
|
18
|
+
const capture = this.capture(message);
|
|
19
|
+
if (!capture)
|
|
20
|
+
return undefined;
|
|
21
|
+
const admissions = [];
|
|
22
|
+
for (const recipient of recipients) {
|
|
23
|
+
const metadata = recipient.contribution.metadata;
|
|
24
|
+
if (metadata?.version !== 1)
|
|
25
|
+
continue;
|
|
26
|
+
if (capture.context.authorIsWebhook
|
|
27
|
+
? metadata.webhooks !== true
|
|
28
|
+
: capture.context.authorIsBot && metadata.otherBots !== true)
|
|
29
|
+
continue;
|
|
30
|
+
const generation = this.generation(capture.context.serverId, recipient.moduleId);
|
|
31
|
+
if (generation)
|
|
32
|
+
admissions.push({ ...recipient, generation });
|
|
33
|
+
}
|
|
34
|
+
return admissions.length
|
|
35
|
+
? task(this, capture, admissions, onError)
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
async execute(capture, admissions, onError) {
|
|
39
|
+
for (const { moduleId, contribution, generation } of admissions) {
|
|
40
|
+
if (this.generation(capture.context.serverId, moduleId) !== generation ||
|
|
41
|
+
!capture.current())
|
|
42
|
+
continue;
|
|
43
|
+
try {
|
|
44
|
+
await contribution.execute(capture.context);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
onError(moduleId, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
stop() {
|
|
52
|
+
this.#stopped = true;
|
|
53
|
+
}
|
|
54
|
+
generation(guildId, moduleId) {
|
|
55
|
+
try {
|
|
56
|
+
if (this.#stopped)
|
|
57
|
+
return undefined;
|
|
58
|
+
const observed = this.eligibility?.read(guildId, moduleId);
|
|
59
|
+
if (this.#stopped ||
|
|
60
|
+
observed?.state !== "enabled" ||
|
|
61
|
+
typeof observed.generation !== "string" ||
|
|
62
|
+
!observed.generation.length ||
|
|
63
|
+
observed.generation.length > 256)
|
|
64
|
+
return undefined;
|
|
65
|
+
const current = this.eligibility?.isCurrent(observed.generation);
|
|
66
|
+
return !this.#stopped && current === true
|
|
67
|
+
? observed.generation
|
|
68
|
+
: undefined;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
capture(message) {
|
|
75
|
+
try {
|
|
76
|
+
const expected = this.expectedApplicationId;
|
|
77
|
+
const ownUserId = this.client.user?.id;
|
|
78
|
+
// Pinned Discord.js partial getter reads content. Check its data-property
|
|
79
|
+
// shape without invoking content accessors or inspecting/retaining text.
|
|
80
|
+
const contentShape = Object.getOwnPropertyDescriptor(message, "content");
|
|
81
|
+
if (!expected ||
|
|
82
|
+
!snowflake.test(expected) ||
|
|
83
|
+
!ownUserId ||
|
|
84
|
+
!snowflake.test(ownUserId) ||
|
|
85
|
+
!contentShape ||
|
|
86
|
+
!("value" in contentShape) ||
|
|
87
|
+
typeof contentShape.value !== "string" ||
|
|
88
|
+
message.client !== this.client ||
|
|
89
|
+
!message.guildId ||
|
|
90
|
+
!snowflake.test(message.guildId) ||
|
|
91
|
+
!snowflake.test(message.channelId) ||
|
|
92
|
+
!snowflake.test(message.id))
|
|
93
|
+
return undefined;
|
|
94
|
+
const guild = message.guild, channel = message.channel, author = message.author;
|
|
95
|
+
if (!guild ||
|
|
96
|
+
!channel ||
|
|
97
|
+
channel.isDMBased() ||
|
|
98
|
+
!author ||
|
|
99
|
+
!snowflake.test(author.id) ||
|
|
100
|
+
typeof author.bot !== "boolean" ||
|
|
101
|
+
author.system !== false ||
|
|
102
|
+
message.system !== false ||
|
|
103
|
+
(message.webhookId !== null &&
|
|
104
|
+
(typeof message.webhookId !== "string" ||
|
|
105
|
+
!snowflake.test(message.webhookId))) ||
|
|
106
|
+
(message.applicationId !== null &&
|
|
107
|
+
(typeof message.applicationId !== "string" ||
|
|
108
|
+
!snowflake.test(message.applicationId))) ||
|
|
109
|
+
author.id === ownUserId ||
|
|
110
|
+
message.applicationId === expected ||
|
|
111
|
+
message.webhookId === expected)
|
|
112
|
+
return undefined;
|
|
113
|
+
const roles = snapshotProtectionAuthorRoles(message, Boolean(message.webhookId));
|
|
114
|
+
if (roles.authorRolesResolved !== true)
|
|
115
|
+
return undefined;
|
|
116
|
+
const created = timestamp(message.createdTimestamp), updated = timestamp(message.editedTimestamp);
|
|
117
|
+
if (!created || !updated || updated < created)
|
|
118
|
+
return undefined;
|
|
119
|
+
const parent = channel.isThread() ? channel.parent : undefined;
|
|
120
|
+
if (channel.isThread() && !parent)
|
|
121
|
+
return undefined;
|
|
122
|
+
const categoryId = parent ? parent.parentId : channel.parentId;
|
|
123
|
+
const category = categoryId
|
|
124
|
+
? guild.channels.cache.get(categoryId)
|
|
125
|
+
: undefined;
|
|
126
|
+
if (categoryId &&
|
|
127
|
+
(!snowflake.test(categoryId) ||
|
|
128
|
+
!category ||
|
|
129
|
+
category.type !== ChannelType.GuildCategory))
|
|
130
|
+
return undefined;
|
|
131
|
+
const current = combineWitnesses(relationships(this.client, expected, ownUserId, guild, channel, parent, category), roleWitness(this.client, guild, author.id, Boolean(message.webhookId), roles.authorRoleIds));
|
|
132
|
+
if (!current() ||
|
|
133
|
+
message.guildId !== guild.id ||
|
|
134
|
+
message.channelId !== channel.id)
|
|
135
|
+
return undefined;
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
context: Object.freeze({
|
|
138
|
+
serverId: guild.id,
|
|
139
|
+
channelId: channel.id,
|
|
140
|
+
messageId: message.id,
|
|
141
|
+
...(parent ? { parentChannelId: parent.id } : {}),
|
|
142
|
+
...(categoryId ? { categoryId } : {}),
|
|
143
|
+
authorId: author.id,
|
|
144
|
+
authorRoleIds: roles.authorRoleIds,
|
|
145
|
+
authorRolesResolved: true,
|
|
146
|
+
authorIsBot: author.bot,
|
|
147
|
+
authorIsWebhook: Boolean(message.webhookId),
|
|
148
|
+
authorIsSystem: false,
|
|
149
|
+
partial: false,
|
|
150
|
+
createdAt: created,
|
|
151
|
+
updatedAt: updated,
|
|
152
|
+
}),
|
|
153
|
+
current,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function timestamp(value) {
|
|
162
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
|
|
163
|
+
return undefined;
|
|
164
|
+
const date = new Date(value);
|
|
165
|
+
if (!Number.isFinite(date.getTime()))
|
|
166
|
+
return undefined;
|
|
167
|
+
const result = date.toISOString();
|
|
168
|
+
return result.length === 24 ? result : undefined;
|
|
169
|
+
}
|
|
170
|
+
function combineWitnesses(...witnesses) {
|
|
171
|
+
return () => witnesses.every((current) => current());
|
|
172
|
+
}
|
|
173
|
+
/** Current cached assignment proof. This closure never captures Message. */
|
|
174
|
+
function roleWitness(client, guild, authorId, webhook, ids) {
|
|
175
|
+
if (webhook)
|
|
176
|
+
return () => true;
|
|
177
|
+
const member = guild.members.cache.get(authorId);
|
|
178
|
+
if (!member)
|
|
179
|
+
return () => false;
|
|
180
|
+
const assigned = Reflect.get(member, "_roles");
|
|
181
|
+
if (!Array.isArray(assigned) || assigned.length > 250)
|
|
182
|
+
return () => false;
|
|
183
|
+
const snapshot = [];
|
|
184
|
+
for (let index = 0, length = assigned.length; index < length; index++)
|
|
185
|
+
snapshot.push(assigned[index]);
|
|
186
|
+
const roles = ids.map((id) => ({ id, role: guild.roles.cache.get(id) }));
|
|
187
|
+
return () => {
|
|
188
|
+
try {
|
|
189
|
+
return (guild.members.cache.get(authorId) === member &&
|
|
190
|
+
member.guild === guild &&
|
|
191
|
+
member.client === client &&
|
|
192
|
+
member.id === authorId &&
|
|
193
|
+
member.user.id === authorId &&
|
|
194
|
+
member.partial === false &&
|
|
195
|
+
Reflect.get(member, "_roles") === assigned &&
|
|
196
|
+
assigned.length === snapshot.length &&
|
|
197
|
+
snapshot.every((id, index) => assigned[index] === id) &&
|
|
198
|
+
roles.every(({ id, role }) => Boolean(role &&
|
|
199
|
+
guild.roles.cache.get(id) === role &&
|
|
200
|
+
role.id === id &&
|
|
201
|
+
role.guild === guild &&
|
|
202
|
+
role.client === client)));
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Own lexical scope contains cached relationship witnesses, never Message. */
|
|
210
|
+
function relationships(client, expected, ownUserId, guild, channel, parent, category) {
|
|
211
|
+
const channelId = channel.id, guildId = guild.id, parentId = parent?.id, categoryId = category?.id;
|
|
212
|
+
return () => {
|
|
213
|
+
try {
|
|
214
|
+
return Boolean(client.isReady() === true &&
|
|
215
|
+
client.user?.id === ownUserId &&
|
|
216
|
+
client.application?.id === expected &&
|
|
217
|
+
client.guilds.cache.get(guildId) === guild &&
|
|
218
|
+
guild.id === guildId &&
|
|
219
|
+
guild.client === client &&
|
|
220
|
+
guild.available === true &&
|
|
221
|
+
channel.guild === guild &&
|
|
222
|
+
channel.client === client &&
|
|
223
|
+
channel.id === channelId &&
|
|
224
|
+
guild.channels.cache.get(channelId) === channel &&
|
|
225
|
+
client.channels.cache.get(channelId) === channel &&
|
|
226
|
+
(!parent ||
|
|
227
|
+
(parent.guild === guild &&
|
|
228
|
+
parent.client === client &&
|
|
229
|
+
parent.id === parentId &&
|
|
230
|
+
guild.channels.cache.get(parentId) === parent)) &&
|
|
231
|
+
(!category ||
|
|
232
|
+
(category.guild === guild &&
|
|
233
|
+
category.client === client &&
|
|
234
|
+
category.id === categoryId &&
|
|
235
|
+
guild.channels.cache.get(categoryId) === category)) &&
|
|
236
|
+
(channel.isThread()
|
|
237
|
+
? channel.parentId === parentId &&
|
|
238
|
+
parent &&
|
|
239
|
+
"parentId" in parent &&
|
|
240
|
+
parent.parentId === (categoryId ?? null)
|
|
241
|
+
: channel.parentId === (categoryId ?? null)));
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** Queue closure is created outside the capture scope, with no source Message. */
|
|
249
|
+
function task(delivery, capture, admissions, onError) {
|
|
250
|
+
return Object.freeze({
|
|
251
|
+
guildId: capture.context.serverId,
|
|
252
|
+
run: () => delivery.execute(capture, admissions, onError),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=guild-message-update-metadata.js.map
|