@zhin.js/adapter-discord 1.1.26 → 1.1.28
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 +175 -0
- package/LICENSE +21 -0
- package/PERMITS.md +15 -14
- package/README.md +7 -2
- package/adapters/discord/index.js +3 -0
- package/adapters/discord/index.ts +3 -0
- package/agents/discord/conversation.js +70 -0
- package/agents/discord/conversation.ts +77 -0
- package/agents/discord/messages.js +18 -2
- package/agents/discord/messages.ts +18 -2
- package/agents/discord/skills/discord/SKILL.md +1 -1
- package/agents/discord/skills/discord/tools/react/index.js +7 -6
- package/agents/discord/skills/discord/tools/react/index.ts +8 -7
- package/agents/discord/skills/discord/tools/send_embed/index.js +6 -11
- package/agents/discord/skills/discord/tools/send_embed/index.ts +7 -12
- package/agents/discord/skills/discord-guild/tools/add_role/index.js +11 -9
- package/agents/discord/skills/discord-guild/tools/add_role/index.ts +11 -10
- package/agents/discord/skills/discord-guild/tools/create_thread/index.js +5 -5
- package/agents/discord/skills/discord-guild/tools/create_thread/index.ts +6 -6
- package/agents/discord/skills/discord-guild/tools/forum_post/index.js +7 -3
- package/agents/discord/skills/discord-guild/tools/forum_post/index.ts +7 -4
- package/agents/discord/skills/discord-guild/tools/list_roles/index.js +7 -8
- package/agents/discord/skills/discord-guild/tools/list_roles/index.ts +8 -9
- package/agents/discord/skills/discord-guild/tools/remove_role/index.js +11 -9
- package/agents/discord/skills/discord-guild/tools/remove_role/index.ts +11 -10
- package/lib/channel-visibility.d.ts +9 -0
- package/lib/channel-visibility.js +44 -0
- package/lib/endpoint.d.ts +1 -0
- package/lib/endpoint.js +76 -12
- package/lib/gateway.d.ts +1 -0
- package/lib/gateway.js +21 -8
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/platform-permit.d.ts +4 -11
- package/lib/platform-permit.js +7 -40
- package/lib/protocol.d.ts +18 -0
- package/lib/protocol.js +32 -3
- package/lib/sender-standing.d.ts +18 -0
- package/lib/sender-standing.js +91 -0
- package/lib/webhook.d.ts +2 -6
- package/lib/webhook.js +23 -55
- package/package.json +23 -25
- package/plugin.js +0 -6
- package/schema.json +0 -56
- package/src/channel-visibility.ts +44 -0
- package/src/endpoint.ts +77 -11
- package/src/gateway.ts +22 -5
- package/src/index.ts +1 -6
- package/src/platform-permit.ts +5 -52
- package/src/protocol.ts +43 -3
- package/src/sender-standing.ts +91 -0
- package/src/webhook.ts +21 -58
package/lib/endpoint.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { createDiscordSelfProfile, createDiscordRestSelfProfile } from './self-profile.js';
|
|
2
2
|
import { DiscordRestRequestError } from './rest-request-error.js';
|
|
3
|
+
import { isDiscordChannelObfuscated } from './channel-visibility.js';
|
|
3
4
|
/**
|
|
4
5
|
* DiscordEndpoint — lifecycle, outbound, admit, gateway / interactions modes, agent tool surface.
|
|
5
6
|
*/
|
|
6
7
|
import { Client, ChannelType } from 'discord.js';
|
|
7
8
|
import { createEndpointSpecification, endpointSpec, createEndpointLifecycle, createEndpointOperation, } from 'zhin.js/adapter';
|
|
8
9
|
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
9
|
-
import { connectDiscordGatewayClient, discordClientOptions, DEFAULT_INTENTS,
|
|
10
|
-
import { discordInboundConversation, formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
|
|
10
|
+
import { connectDiscordGatewayClient, discordClientOptions, DEFAULT_INTENTS, toMessageCreateOptions, } from './gateway.js';
|
|
11
|
+
import { discordChannelOf, discordInboundConversation, formatButtonContent, formatButtonSegments, formatInboundContent, formatInboundSegments, formatOutboundBody, senderDisplayName, } from './protocol.js';
|
|
11
12
|
import { registerDiscordInteractionRoutes } from './webhook.js';
|
|
12
13
|
import { receiveDiscordGuildMemberSideEvent } from './side-event-dispatch.js';
|
|
14
|
+
import { discordGuildPermissionNamesFromChannelBitfield, discordPermissionNames, discordSenderStanding, discordStandingFromPermissions, } from './sender-standing.js';
|
|
13
15
|
const DISCORD_API = 'https://discord.com/api/v10';
|
|
14
16
|
/** 出站 HTTP 调用统一 30s 超时。 */
|
|
15
17
|
const OUTBOUND_TIMEOUT_MS = 30_000;
|
|
@@ -137,11 +139,11 @@ export class DiscordGatewayEndpoint extends Client {
|
|
|
137
139
|
}
|
|
138
140
|
async #sendFramework({ conversation, payload, signal }) {
|
|
139
141
|
const body = formatOutboundBody(payload);
|
|
140
|
-
const snowflake = await this.#sendBody({ channelId: conversation
|
|
142
|
+
const snowflake = await this.#sendBody({ channelId: discordChannelOf(conversation), body, signal });
|
|
141
143
|
this.#logger.debug(formatCompact({
|
|
142
144
|
op: 'discord_send',
|
|
143
145
|
endpoint: this.#options.config.id,
|
|
144
|
-
target: conversation
|
|
146
|
+
target: discordChannelOf(conversation),
|
|
145
147
|
messageId: snowflake,
|
|
146
148
|
}));
|
|
147
149
|
return snowflake;
|
|
@@ -150,7 +152,7 @@ export class DiscordGatewayEndpoint extends Client {
|
|
|
150
152
|
if (!message.id)
|
|
151
153
|
return;
|
|
152
154
|
const operation = createEndpointOperation({ signal });
|
|
153
|
-
const channel = await operation.prepare(() => this.#requireClient().channels.fetch(message.conversation
|
|
155
|
+
const channel = await operation.prepare(() => this.#requireClient().channels.fetch(discordChannelOf(message.conversation)));
|
|
154
156
|
if (!channel?.isTextBased() || !channel.messages)
|
|
155
157
|
return;
|
|
156
158
|
const msg = await operation.prepare(() => channel.messages.fetch(message.id));
|
|
@@ -163,6 +165,7 @@ export class DiscordGatewayEndpoint extends Client {
|
|
|
163
165
|
if (msg.authorBot)
|
|
164
166
|
return;
|
|
165
167
|
const conversation = discordInboundConversation(String(this.#options.id), msg);
|
|
168
|
+
const platform = discordSenderStanding(msg);
|
|
166
169
|
void this[endpointSpec].events.emit('message.receive', {
|
|
167
170
|
conversation,
|
|
168
171
|
event: msg.event,
|
|
@@ -173,7 +176,7 @@ export class DiscordGatewayEndpoint extends Client {
|
|
|
173
176
|
sender: {
|
|
174
177
|
id: msg.authorId,
|
|
175
178
|
name: senderDisplayName(msg) || undefined,
|
|
176
|
-
...(
|
|
179
|
+
...(platform ? { platform } : {}),
|
|
177
180
|
},
|
|
178
181
|
endpointId: this.#options.config.id,
|
|
179
182
|
...(msg.mentionedBot ? { mentioned: true } : {}),
|
|
@@ -183,7 +186,7 @@ export class DiscordGatewayEndpoint extends Client {
|
|
|
183
186
|
userId: msg.authorId,
|
|
184
187
|
guildId: msg.guildId,
|
|
185
188
|
permissions: msg.permissionTokens,
|
|
186
|
-
role:
|
|
189
|
+
role: platform?.standing,
|
|
187
190
|
}),
|
|
188
191
|
}).catch((err) => {
|
|
189
192
|
this.#logger.warn(formatCompact({
|
|
@@ -221,7 +224,7 @@ export class DiscordGatewayEndpoint extends Client {
|
|
|
221
224
|
});
|
|
222
225
|
}
|
|
223
226
|
async #addReaction({ message, emoji, hint, signal }) {
|
|
224
|
-
const channelId = hint?.channelId ?? message.conversation
|
|
227
|
+
const channelId = hint?.channelId ?? discordChannelOf(message.conversation);
|
|
225
228
|
if (!channelId || !message.id)
|
|
226
229
|
return null;
|
|
227
230
|
const operation = createEndpointOperation({ signal });
|
|
@@ -325,6 +328,8 @@ export class DiscordRestClient {
|
|
|
325
328
|
return this.request({ method: 'DELETE', path: `/channels/${channelId}/messages/${messageId}`, signal });
|
|
326
329
|
}
|
|
327
330
|
}
|
|
331
|
+
const APP_PERMISSIONS_TTL_MS = 10 * 60_000;
|
|
332
|
+
const APP_PERMISSIONS_MAX = 1_000;
|
|
328
333
|
export class DiscordInteractionsEndpoint extends DiscordRestClient {
|
|
329
334
|
[endpointSpec];
|
|
330
335
|
#logger;
|
|
@@ -333,6 +338,8 @@ export class DiscordInteractionsEndpoint extends DiscordRestClient {
|
|
|
333
338
|
#routeReleases = [];
|
|
334
339
|
#open = false;
|
|
335
340
|
#started = false;
|
|
341
|
+
/** Latest interaction `app_permissions` per guild (guild-level names), for the Bot's own standing. */
|
|
342
|
+
#appPermissions = new Map();
|
|
336
343
|
#control = Object.freeze({
|
|
337
344
|
recall: request => this.#recallMessage(request),
|
|
338
345
|
});
|
|
@@ -347,6 +354,12 @@ export class DiscordInteractionsEndpoint extends DiscordRestClient {
|
|
|
347
354
|
this[endpointSpec] = createEndpointSpecification(this, {
|
|
348
355
|
start: () => this.#startFramework(), open: () => this.#openFramework(), close: () => this.#closeFramework(),
|
|
349
356
|
stop: () => this.#stopFramework(), send: request => this.#sendFramework(request), name: options.config.id,
|
|
357
|
+
// Interactions mode has no gateway session to read the bot's guild member: its own standing
|
|
358
|
+
// comes from the latest interaction's `app_permissions` in that guild (ADR 0056 §1), reduced
|
|
359
|
+
// to guild-level names like senders; without a recent interaction it stays unknown.
|
|
360
|
+
management: Object.freeze({
|
|
361
|
+
inspectSelfStanding: async ({ conversation }) => this.#selfStandingFromInteractions(conversation),
|
|
362
|
+
}),
|
|
350
363
|
control: this.#control, content: this.#content,
|
|
351
364
|
profile: createDiscordRestSelfProfile(this, id => this[endpointSpec].reportSelfUserId(id)),
|
|
352
365
|
});
|
|
@@ -384,18 +397,41 @@ export class DiscordInteractionsEndpoint extends DiscordRestClient {
|
|
|
384
397
|
}
|
|
385
398
|
async #sendFramework({ conversation, payload, signal }) {
|
|
386
399
|
const body = formatOutboundBody(payload);
|
|
387
|
-
const data = await this.createMessage({ channelId: conversation
|
|
400
|
+
const data = await this.createMessage({ channelId: discordChannelOf(conversation), body, signal });
|
|
388
401
|
return data.id ?? '';
|
|
389
402
|
}
|
|
390
403
|
async #recallMessage({ message, signal }) {
|
|
391
404
|
if (!message.id)
|
|
392
405
|
return;
|
|
393
|
-
await this.deleteMessage({ channelId: message.conversation
|
|
406
|
+
await this.deleteMessage({ channelId: discordChannelOf(message.conversation), messageId: message.id, signal });
|
|
407
|
+
}
|
|
408
|
+
#selfStandingFromInteractions(conversation) {
|
|
409
|
+
const guildId = conversation.kind === 'channel' && conversation.parent?.kind === 'channel' ? conversation.parent.id : undefined;
|
|
410
|
+
const entry = guildId ? this.#appPermissions.get(guildId) : undefined;
|
|
411
|
+
if (!entry || Date.now() - entry.at > APP_PERMISSIONS_TTL_MS)
|
|
412
|
+
return undefined;
|
|
413
|
+
// The guild owner is not visible to an interaction; `administrator` still reads as `admin`.
|
|
414
|
+
return { standing: discordStandingFromPermissions(entry.names), ...(entry.names.length ? { permissions: entry.names } : {}) };
|
|
415
|
+
}
|
|
416
|
+
#recordAppPermissions(msg) {
|
|
417
|
+
const appPermissions = msg.event?.app_permissions;
|
|
418
|
+
if (!msg.guildId || appPermissions === undefined)
|
|
419
|
+
return;
|
|
420
|
+
this.#appPermissions.delete(msg.guildId);
|
|
421
|
+
this.#appPermissions.set(msg.guildId, { names: discordGuildPermissionNamesFromChannelBitfield(appPermissions), at: Date.now() });
|
|
422
|
+
while (this.#appPermissions.size > APP_PERMISSIONS_MAX) {
|
|
423
|
+
const oldest = this.#appPermissions.keys().next().value;
|
|
424
|
+
if (oldest === undefined)
|
|
425
|
+
break;
|
|
426
|
+
this.#appPermissions.delete(oldest);
|
|
427
|
+
}
|
|
394
428
|
}
|
|
395
429
|
admit(msg) {
|
|
396
430
|
if (!this.#open)
|
|
397
431
|
return;
|
|
432
|
+
this.#recordAppPermissions(msg);
|
|
398
433
|
const conversation = discordInboundConversation(String(this.#options.id), msg);
|
|
434
|
+
const platform = discordSenderStanding(msg);
|
|
399
435
|
void this[endpointSpec].events.emit('message.receive', {
|
|
400
436
|
conversation,
|
|
401
437
|
event: msg.event,
|
|
@@ -406,7 +442,7 @@ export class DiscordInteractionsEndpoint extends DiscordRestClient {
|
|
|
406
442
|
sender: {
|
|
407
443
|
id: msg.authorId,
|
|
408
444
|
name: senderDisplayName(msg) || undefined,
|
|
409
|
-
...(
|
|
445
|
+
...(platform ? { platform } : {}),
|
|
410
446
|
},
|
|
411
447
|
endpointId: this.#options.config.id,
|
|
412
448
|
metadata: Object.freeze({
|
|
@@ -474,7 +510,7 @@ async function resolveDiscordContent(fetch, token, reference, context) {
|
|
|
474
510
|
}
|
|
475
511
|
try {
|
|
476
512
|
context.signal.throwIfAborted();
|
|
477
|
-
const response = await fetch(`${DISCORD_API}/channels/${reference.message.conversation
|
|
513
|
+
const response = await fetch(`${DISCORD_API}/channels/${discordChannelOf(reference.message.conversation)}/messages/${reference.message.id}`, { headers: { Authorization: `Bot ${token}` }, signal: context.signal });
|
|
478
514
|
if (response.status === 404)
|
|
479
515
|
return Object.freeze({ status: 'not_found', code: 'discord_message_not_found' });
|
|
480
516
|
if (response.status === 403)
|
|
@@ -515,9 +551,34 @@ async function resolveDiscordContent(fetch, token, reference, context) {
|
|
|
515
551
|
/**
|
|
516
552
|
* DiscordGatewayEndpoint 的 EndpointManagement 语义端口(参照 qq 的工厂模式)。
|
|
517
553
|
* 数据源为 discord.js SDK 缓存:guilds.cache / guild.channels.cache / guild.members。
|
|
554
|
+
* listChannels 会过滤带 CHANNEL_OBFUSCATED flag 的混淆频道(见 channel-visibility.ts)。
|
|
518
555
|
*/
|
|
519
556
|
export function createDiscordEndpointManagement(requireClient) {
|
|
520
557
|
return Object.freeze({
|
|
558
|
+
/**
|
|
559
|
+
* ADR 0056 §1: the Bot's own guild member in the conversation's guild — guild owner → `owner`;
|
|
560
|
+
* `administrator` / `moderate_members` → `admin`; roles and **guild-level** permission names
|
|
561
|
+
* (`GuildMember.permissions`, as for senders) so `bot-permission:*` and `platform-permission:*`
|
|
562
|
+
* read the same scope. Channel overwrites are left to the platform at execution time.
|
|
563
|
+
*/
|
|
564
|
+
async inspectSelfStanding({ conversation, signal }) {
|
|
565
|
+
if (conversation.kind === 'private')
|
|
566
|
+
return undefined;
|
|
567
|
+
const operation = createEndpointOperation({ signal });
|
|
568
|
+
const client = requireClient();
|
|
569
|
+
const channel = await operation.run(() => client.channels.fetch(conversation.threadId ?? conversation.id));
|
|
570
|
+
if (!channel || !('guild' in channel) || !client.user)
|
|
571
|
+
return undefined;
|
|
572
|
+
const guild = channel.guild;
|
|
573
|
+
const me = guild.members.me ?? await operation.run(() => guild.members.fetchMe());
|
|
574
|
+
const permissions = me.permissions;
|
|
575
|
+
const names = permissions ? discordPermissionNames(bit => permissions.has(bit)) : [];
|
|
576
|
+
const roles = [...me.roles.cache.values()]
|
|
577
|
+
.filter(role => role.id !== guild.id)
|
|
578
|
+
.map(role => (role.name ? { id: String(role.id), name: String(role.name) } : { id: String(role.id) }));
|
|
579
|
+
const standing = discordStandingFromPermissions(names, guild.ownerId === client.user.id);
|
|
580
|
+
return { standing, ...(roles.length ? { roles } : {}), ...(names.length ? { permissions: names } : {}) };
|
|
581
|
+
},
|
|
521
582
|
async listGroups(request = {}) {
|
|
522
583
|
return createEndpointOperation(request).run(() => {
|
|
523
584
|
const groups = [];
|
|
@@ -545,6 +606,9 @@ export function createDiscordEndpointManagement(requireClient) {
|
|
|
545
606
|
continue;
|
|
546
607
|
if (channel.type !== ChannelType.GuildText)
|
|
547
608
|
continue;
|
|
609
|
+
// 机器人无 VIEW_CHANNEL 的频道会被 Gateway 混淆下发(HTTP 列表已不返回),默认过滤以保持两条路径一致。
|
|
610
|
+
if (isDiscordChannelObfuscated(channel))
|
|
611
|
+
continue;
|
|
548
612
|
channels.push({
|
|
549
613
|
id: String(channel.id),
|
|
550
614
|
name: channel.name ? String(channel.name) : undefined,
|
package/lib/gateway.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const DEFAULT_INTENTS: GatewayIntentBits[];
|
|
|
5
5
|
/** Native SDK surface; tests inject transport below the Client rather than replacing its identity. */
|
|
6
6
|
export type DiscordClientTransport = Client;
|
|
7
7
|
export declare function discordClientOptions(intents: readonly number[], gatewayFaultProxyUrl?: string, restOptions?: Partial<RESTOptions>): ClientOptions;
|
|
8
|
+
/** Normalized standing word (`owner` / `admin` / `member`); `undefined` outside a guild. */
|
|
8
9
|
export declare function resolveSenderRole(msg: DiscordInboundMessage): string | undefined;
|
|
9
10
|
export declare function normalizeDiscordMessage(raw: unknown): DiscordInboundMessage | null;
|
|
10
11
|
export declare function toMessageCreateOptions(body: DiscordOutboundBody): Promise<MessageCreateOptions>;
|
package/lib/gateway.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { Partials, GatewayIntentBits, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, REST, Routes, PermissionFlagsBits, } from 'discord.js';
|
|
5
5
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
6
|
import { activityTypeCode, resolveChannelKind, } from './protocol.js';
|
|
7
|
+
import { discordPermissionNames, discordSenderStanding } from './sender-standing.js';
|
|
7
8
|
const logger = getLogger('discord');
|
|
8
9
|
export const DEFAULT_INTENTS = [
|
|
9
10
|
GatewayIntentBits.Guilds,
|
|
@@ -16,15 +17,14 @@ export const DEFAULT_INTENTS = [
|
|
|
16
17
|
export function discordClientOptions(intents, gatewayFaultProxyUrl, restOptions = {}) {
|
|
17
18
|
return { rest: { ...restOptions, retries: 0, rejectOnRateLimit: () => true }, intents: [...intents], partials: [Partials.Channel], ...(gatewayFaultProxyUrl ? { ws: { buildStrategy: manager => new DiscordFaultProxyStrategy(manager, gatewayFaultProxyUrl) } } : {}) };
|
|
18
19
|
}
|
|
20
|
+
/** Normalized standing word (`owner` / `admin` / `member`); `undefined` outside a guild. */
|
|
19
21
|
export function resolveSenderRole(msg) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return 'member';
|
|
27
|
-
return undefined;
|
|
22
|
+
return discordSenderStanding(msg)?.standing;
|
|
23
|
+
}
|
|
24
|
+
/** A discord.js thread channel's parent channel id (threads share their parent's conversation key, ADR 0056 §2). */
|
|
25
|
+
function threadParentOf(channel) {
|
|
26
|
+
const thread = channel;
|
|
27
|
+
return thread && typeof thread.isThread === 'function' && thread.isThread() && thread.parentId ? thread.parentId : undefined;
|
|
28
28
|
}
|
|
29
29
|
export function normalizeDiscordMessage(raw) {
|
|
30
30
|
if (!raw || typeof raw !== 'object')
|
|
@@ -33,6 +33,8 @@ export function normalizeDiscordMessage(raw) {
|
|
|
33
33
|
if (!msg.author || !msg.channel)
|
|
34
34
|
return null;
|
|
35
35
|
const permissionTokens = [];
|
|
36
|
+
let platformPermissions = [];
|
|
37
|
+
let authorRoles = [];
|
|
36
38
|
let isGuildOwner = false;
|
|
37
39
|
const member = msg.member;
|
|
38
40
|
const guild = msg.guild;
|
|
@@ -48,6 +50,13 @@ export function normalizeDiscordMessage(raw) {
|
|
|
48
50
|
if (member.permissions.has(bit))
|
|
49
51
|
permissionTokens.push(name);
|
|
50
52
|
}
|
|
53
|
+
platformPermissions = discordPermissionNames(bit => member.permissions.has(bit));
|
|
54
|
+
const cache = member.roles?.cache;
|
|
55
|
+
if (cache && typeof cache.values === 'function') {
|
|
56
|
+
authorRoles = [...cache.values()]
|
|
57
|
+
.filter((role) => role && role.id && role.id !== guild.id)
|
|
58
|
+
.map((role) => (role.name ? { id: String(role.id), name: String(role.name) } : { id: String(role.id) }));
|
|
59
|
+
}
|
|
51
60
|
if (guild.ownerId === msg.author.id) {
|
|
52
61
|
isGuildOwner = true;
|
|
53
62
|
permissionTokens.push('guild_owner', 'ADMINISTRATOR');
|
|
@@ -60,6 +69,7 @@ export function normalizeDiscordMessage(raw) {
|
|
|
60
69
|
content: msg.content ?? '',
|
|
61
70
|
channelId: msg.channel.id,
|
|
62
71
|
channelKind: resolveChannelKind(msg.channel.type),
|
|
72
|
+
...(threadParentOf(msg.channel) ? { threadParentId: threadParentOf(msg.channel) } : {}),
|
|
63
73
|
authorId: msg.author.id,
|
|
64
74
|
authorName: member?.displayName || msg.author.displayName || msg.author.username,
|
|
65
75
|
authorBot: msg.author.bot,
|
|
@@ -67,6 +77,8 @@ export function normalizeDiscordMessage(raw) {
|
|
|
67
77
|
guildId: guild?.id,
|
|
68
78
|
isGuildOwner,
|
|
69
79
|
permissionTokens,
|
|
80
|
+
...(platformPermissions.length ? { platformPermissions } : {}),
|
|
81
|
+
...(authorRoles.length ? { authorRoles } : {}),
|
|
70
82
|
attachments: [...msg.attachments.values()].map((a) => ({
|
|
71
83
|
id: a.id,
|
|
72
84
|
name: a.name ?? undefined,
|
|
@@ -256,6 +268,7 @@ export async function connectDiscordGatewayClient(client, config, handlers, sign
|
|
|
256
268
|
customId: interaction.customId,
|
|
257
269
|
channelId,
|
|
258
270
|
channelKind: interaction.channel ? resolveChannelKind(interaction.channel.type) : interaction.guildId ? 'channel' : 'private',
|
|
271
|
+
...(threadParentOf(interaction.channel) ? { threadParentId: threadParentOf(interaction.channel) } : {}),
|
|
259
272
|
...(interaction.guildId ? { guildId: interaction.guildId } : {}),
|
|
260
273
|
userId: interaction.user.id,
|
|
261
274
|
userName: interaction.user.username || interaction.user.displayName || interaction.user.id,
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, type DiscordActivity, type DiscordEndpointConfig, type DiscordButtonInbound, type DiscordInboundAttachment, type DiscordInboundMessage, type DiscordOutboundBody, type DiscordWireSegment, type ResolvedDiscordConfig, type ResolvedDiscordGatewayConfig, type ResolvedDiscordInteractionsConfig, } from './protocol.js';
|
|
2
2
|
export { requireDiscordGatewayClient, type DiscordClient, type DiscordClientEventMap, } from './client.js';
|
|
3
|
-
export {
|
|
3
|
+
export { platformPermit } from './platform-permit.js';
|
|
4
4
|
export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, type DiscordClientTransport, type DiscordEndpointOptions, type DiscordInteractionsEndpointOptions, } from './endpoint.js';
|
|
5
5
|
export { connectDiscordGatewayClient, discordClientOptions, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, type DiscordGatewayConnectHandlers, } from './gateway.js';
|
|
6
6
|
export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, type DiscordInteractionsHandler, } from './webhook.js';
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { activityTypeCode, formatButtonContent, formatInboundContent, formatOutboundBody, resolveChannelKind, resolveDiscordConfig, senderDisplayName, } from './protocol.js';
|
|
2
2
|
export { requireDiscordGatewayClient, } from './client.js';
|
|
3
|
-
export {
|
|
3
|
+
export { platformPermit } from './platform-permit.js';
|
|
4
4
|
export { DiscordGatewayEndpoint, DiscordInteractionsEndpoint, } from './endpoint.js';
|
|
5
5
|
export { connectDiscordGatewayClient, discordClientOptions, normalizeDiscordMessage, resolveSenderRole, toMessageCreateOptions, } from './gateway.js';
|
|
6
6
|
export { handleDiscordInteractionRequest, registerDiscordInteractionRoutes, } from './webhook.js';
|
package/lib/platform-permit.d.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Discord platform permit
|
|
2
|
+
* Discord platform permit helper(ADR 0056 §5)。
|
|
3
|
+
*
|
|
4
|
+
* 平台身份由适配器上报到 `sender.platform`,permit 由内置评估器(`@zhin.js/permission`)判断;
|
|
5
|
+
* 适配器不再导出自有的 permit 解析器或检查函数。
|
|
3
6
|
*/
|
|
4
|
-
import type { PermissionSubject } from '@zhin.js/permission';
|
|
5
7
|
export declare function platformPermit(perm: string): string;
|
|
6
|
-
export declare function discordGroupPermitResolver(logicalPerm: string): string;
|
|
7
|
-
export declare function normalizeDiscordSenderForPermit(input: {
|
|
8
|
-
isOwner?: boolean;
|
|
9
|
-
permissions?: string[];
|
|
10
|
-
}): {
|
|
11
|
-
role?: string;
|
|
12
|
-
permissions?: string[];
|
|
13
|
-
};
|
|
14
|
-
export declare function checkDiscordPlatformPermit(perm: string, subject: PermissionSubject): boolean;
|
package/lib/platform-permit.js
CHANGED
|
@@ -1,42 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Discord platform permit helper(ADR 0056 §5)。
|
|
3
|
+
*
|
|
4
|
+
* 平台身份由适配器上报到 `sender.platform`,permit 由内置评估器(`@zhin.js/permission`)判断;
|
|
5
|
+
* 适配器不再导出自有的 permit 解析器或检查函数。
|
|
6
|
+
*/
|
|
2
7
|
export function platformPermit(perm) {
|
|
3
|
-
return `platform
|
|
4
|
-
}
|
|
5
|
-
const FACTORY_PERM_MAP = {
|
|
6
|
-
scene_admin: 'moderate_members',
|
|
7
|
-
scene_owner: 'guild_owner',
|
|
8
|
-
};
|
|
9
|
-
export function discordGroupPermitResolver(logicalPerm) {
|
|
10
|
-
return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
|
|
11
|
-
}
|
|
12
|
-
export function normalizeDiscordSenderForPermit(input) {
|
|
13
|
-
const permissions = [...(input.permissions ?? [])];
|
|
14
|
-
if (input.isOwner) {
|
|
15
|
-
permissions.push('guild_owner', 'ADMINISTRATOR');
|
|
16
|
-
return { role: 'owner', permissions };
|
|
17
|
-
}
|
|
18
|
-
if (permissions.includes('ADMINISTRATOR')) {
|
|
19
|
-
return { role: 'admin', permissions };
|
|
20
|
-
}
|
|
21
|
-
if (permissions.includes('MODERATE_MEMBERS') || permissions.includes('MANAGE_GUILD')) {
|
|
22
|
-
return { role: 'admin', permissions };
|
|
23
|
-
}
|
|
24
|
-
return { role: 'member', permissions };
|
|
25
|
-
}
|
|
26
|
-
export function checkDiscordPlatformPermit(perm, subject) {
|
|
27
|
-
const role = subject.sender?.role?.[0];
|
|
28
|
-
const permissions = subject.sender?.permissions ?? [];
|
|
29
|
-
const has = (t) => permissions.includes(t);
|
|
30
|
-
switch (perm) {
|
|
31
|
-
case 'guild_owner':
|
|
32
|
-
return role === 'owner' || has('guild_owner');
|
|
33
|
-
case 'moderate_members':
|
|
34
|
-
return role === 'owner' || role === 'admin' || has('ADMINISTRATOR') || has('MODERATE_MEMBERS');
|
|
35
|
-
case 'manage_roles':
|
|
36
|
-
return role === 'owner' || has('ADMINISTRATOR') || has('MANAGE_ROLES');
|
|
37
|
-
case 'manage_channels':
|
|
38
|
-
return role === 'owner' || has('ADMINISTRATOR') || has('MANAGE_CHANNELS');
|
|
39
|
-
default:
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
8
|
+
return `platform-permission:${perm}`;
|
|
42
9
|
}
|
package/lib/protocol.d.ts
CHANGED
|
@@ -68,8 +68,17 @@ export interface DiscordInboundMessage {
|
|
|
68
68
|
readonly authorBot?: boolean;
|
|
69
69
|
readonly createdTimestamp: number;
|
|
70
70
|
readonly guildId?: string;
|
|
71
|
+
/** The message was sent in a thread: its parent channel (the thread itself is `channelId`). */
|
|
72
|
+
readonly threadParentId?: string;
|
|
71
73
|
readonly isGuildOwner?: boolean;
|
|
72
74
|
readonly permissionTokens?: readonly string[];
|
|
75
|
+
/** ADR 0056 §1: guild permission flags as snake_case names (`ban_members`, …). */
|
|
76
|
+
readonly platformPermissions?: readonly string[];
|
|
77
|
+
/** ADR 0056 §1: the member's guild roles (`@everyone` excluded). */
|
|
78
|
+
readonly authorRoles?: readonly {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly name?: string;
|
|
81
|
+
}[];
|
|
73
82
|
readonly attachments?: readonly DiscordInboundAttachment[];
|
|
74
83
|
readonly embedTitles?: readonly string[];
|
|
75
84
|
readonly stickerNames?: readonly string[];
|
|
@@ -85,6 +94,8 @@ export interface DiscordButtonInbound {
|
|
|
85
94
|
readonly customId: string;
|
|
86
95
|
readonly channelId: string;
|
|
87
96
|
readonly channelKind: 'private' | 'group' | 'channel';
|
|
97
|
+
/** Pressed inside a thread: its parent channel. */
|
|
98
|
+
readonly threadParentId?: string;
|
|
88
99
|
readonly userId: string;
|
|
89
100
|
readonly userName: string;
|
|
90
101
|
readonly sourceMessageId?: string;
|
|
@@ -125,13 +136,18 @@ export declare function resolveChannelKind(channelType: number | string | undefi
|
|
|
125
136
|
/**
|
|
126
137
|
* 入站归一化 → ConversationRef:DM → 'private';GroupDM → 'group';
|
|
127
138
|
* guild 频道 → 'channel' + guild 容器进 parent(guild 是频道容器,kind 'channel')。
|
|
139
|
+
* 线程(thread):会话 id 是父频道、`threadId` 是线程频道(ConversationRef 的子话题约定),
|
|
140
|
+
* 所以父频道上的角色 / 功能配置在线程里同样生效(ADR 0056 §2);出站用 {@link discordChannelOf}。
|
|
128
141
|
* endpoint.adapter = owner PluginId(CapabilityId 以 \0 分隔)。
|
|
129
142
|
*/
|
|
130
143
|
export declare function discordInboundConversation(endpointKey: string, msg: {
|
|
131
144
|
readonly channelId: string;
|
|
132
145
|
readonly channelKind: 'private' | 'group' | 'channel';
|
|
133
146
|
readonly guildId?: string;
|
|
147
|
+
readonly threadParentId?: string;
|
|
134
148
|
}): ConversationRef;
|
|
149
|
+
/** The Discord channel a conversation's messages live in: the thread when there is one. */
|
|
150
|
+
export declare function discordChannelOf(conversation: Pick<ConversationRef, 'id' | 'threadId'>): string;
|
|
135
151
|
export declare function senderDisplayName(msg: DiscordInboundMessage): string;
|
|
136
152
|
/** Build inbound text for OutboundMessageService.receive (gateway owns reply routing). */
|
|
137
153
|
export declare function formatInboundContent(msg: DiscordInboundMessage): string;
|
|
@@ -158,4 +174,6 @@ export declare function activityTypeCode(type: DiscordActivity['type']): number;
|
|
|
158
174
|
export declare const DISCORD_SIGNATURE_MAX_AGE_MS: number;
|
|
159
175
|
export declare function verifyDiscordInteractionSignature(publicKeyHex: string, body: string, signature: string, timestamp: string, nowMs?: number): boolean;
|
|
160
176
|
export declare function formatSlashCommandContent(interaction: Record<string, unknown>): string;
|
|
177
|
+
/** Parent channel of a raw API channel object when it is a thread. */
|
|
178
|
+
export declare function discordThreadParentOf(channel: unknown): string | undefined;
|
|
161
179
|
export declare function interactionToInboundMessage(interaction: Record<string, unknown>): DiscordInboundMessage;
|
package/lib/protocol.js
CHANGED
|
@@ -2,6 +2,7 @@ import { validateDiscordGatewayFaultProxy } from './gateway-fault-proxy.js';
|
|
|
2
2
|
/** Discord Gateway protocol helpers. Canonicalization is owned by Core. */
|
|
3
3
|
import { createPublicKey, verify as cryptoVerify } from 'node:crypto';
|
|
4
4
|
import { isMediaRef } from '@zhin.js/im-contract';
|
|
5
|
+
import { discordGuildPermissionNamesFromChannelBitfield } from './sender-standing.js';
|
|
5
6
|
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
7
|
const logger = getLogger('discord');
|
|
7
8
|
export function resolveDiscordConfig(config) {
|
|
@@ -56,18 +57,26 @@ export function resolveChannelKind(channelType) {
|
|
|
56
57
|
/**
|
|
57
58
|
* 入站归一化 → ConversationRef:DM → 'private';GroupDM → 'group';
|
|
58
59
|
* guild 频道 → 'channel' + guild 容器进 parent(guild 是频道容器,kind 'channel')。
|
|
60
|
+
* 线程(thread):会话 id 是父频道、`threadId` 是线程频道(ConversationRef 的子话题约定),
|
|
61
|
+
* 所以父频道上的角色 / 功能配置在线程里同样生效(ADR 0056 §2);出站用 {@link discordChannelOf}。
|
|
59
62
|
* endpoint.adapter = owner PluginId(CapabilityId 以 \0 分隔)。
|
|
60
63
|
*/
|
|
61
64
|
export function discordInboundConversation(endpointKey, msg) {
|
|
65
|
+
const thread = msg.channelKind === 'channel' && msg.threadParentId && msg.threadParentId !== msg.channelId;
|
|
62
66
|
return {
|
|
63
67
|
endpoint: { id: endpointKey, adapter: endpointKey.split('\0')[0] ?? endpointKey },
|
|
64
68
|
kind: msg.channelKind,
|
|
65
|
-
id: msg.channelId,
|
|
69
|
+
id: thread ? msg.threadParentId : msg.channelId,
|
|
70
|
+
...(thread ? { threadId: msg.channelId } : {}),
|
|
66
71
|
...(msg.guildId && msg.channelKind === 'channel'
|
|
67
72
|
? { parent: { kind: 'channel', id: msg.guildId } }
|
|
68
73
|
: {}),
|
|
69
74
|
};
|
|
70
75
|
}
|
|
76
|
+
/** The Discord channel a conversation's messages live in: the thread when there is one. */
|
|
77
|
+
export function discordChannelOf(conversation) {
|
|
78
|
+
return conversation.threadId ?? conversation.id;
|
|
79
|
+
}
|
|
71
80
|
export function senderDisplayName(msg) {
|
|
72
81
|
return msg.authorName || msg.authorId;
|
|
73
82
|
}
|
|
@@ -253,6 +262,7 @@ export function formatOutboundBody(payload) {
|
|
|
253
262
|
embeds.push({ title: String(data.title ?? ''), url: String(data.url ?? ''), description: String(data.description ?? data.content ?? ''), ...(data.image ? { image: { url: String(data.image) } } : {}) });
|
|
254
263
|
break;
|
|
255
264
|
case 'embed':
|
|
265
|
+
case 'discord:embed':
|
|
256
266
|
embeds.push({ ...data });
|
|
257
267
|
break;
|
|
258
268
|
case 'keyboard': {
|
|
@@ -338,18 +348,37 @@ export function formatSlashCommandContent(interaction) {
|
|
|
338
348
|
}
|
|
339
349
|
return parts.join(' ');
|
|
340
350
|
}
|
|
351
|
+
/** Thread channel types (announcement / public / private thread) carry `parent_id`, the channel they belong to. */
|
|
352
|
+
const THREAD_CHANNEL_TYPES = new Set([10, 11, 12]);
|
|
353
|
+
/** Parent channel of a raw API channel object when it is a thread. */
|
|
354
|
+
export function discordThreadParentOf(channel) {
|
|
355
|
+
if (!channel || typeof channel !== 'object')
|
|
356
|
+
return undefined;
|
|
357
|
+
const raw = channel;
|
|
358
|
+
return THREAD_CHANNEL_TYPES.has(Number(raw.type)) && typeof raw.parent_id === 'string' && raw.parent_id ? raw.parent_id : undefined;
|
|
359
|
+
}
|
|
341
360
|
export function interactionToInboundMessage(interaction) {
|
|
342
|
-
const
|
|
361
|
+
const member = interaction.member;
|
|
362
|
+
const user = member?.user
|
|
343
363
|
?? interaction.user;
|
|
364
|
+
const guildId = interaction.guild_id != null ? String(interaction.guild_id) : undefined;
|
|
365
|
+
// ADR 0056 §1: guild-level names only (the interaction bitfield includes channel overwrites).
|
|
366
|
+
const platformPermissions = guildId ? discordGuildPermissionNamesFromChannelBitfield(member?.permissions) : [];
|
|
367
|
+
const authorRoles = guildId && Array.isArray(member?.roles)
|
|
368
|
+
? member.roles.map(String).filter((id) => id && id !== guildId).map((id) => ({ id }))
|
|
369
|
+
: [];
|
|
344
370
|
return {
|
|
345
371
|
event: interaction,
|
|
346
372
|
id: String(interaction.id),
|
|
347
373
|
content: formatSlashCommandContent(interaction),
|
|
348
374
|
channelId: String(interaction.channel_id ?? ''),
|
|
349
375
|
channelKind: interaction.guild_id ? 'channel' : 'private',
|
|
376
|
+
...(discordThreadParentOf(interaction.channel) ? { threadParentId: discordThreadParentOf(interaction.channel) } : {}),
|
|
350
377
|
authorId: String(user?.id ?? ''),
|
|
351
378
|
authorName: String(user?.username ?? user?.id ?? ''),
|
|
352
379
|
createdTimestamp: Date.now(),
|
|
353
|
-
guildId
|
|
380
|
+
guildId,
|
|
381
|
+
...(platformPermissions.length ? { platformPermissions } : {}),
|
|
382
|
+
...(authorRoles.length ? { authorRoles } : {}),
|
|
354
383
|
};
|
|
355
384
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { PlatformStanding } from '@zhin.js/im-contract';
|
|
2
|
+
import type { DiscordInboundMessage } from './protocol.js';
|
|
3
|
+
/** `ManageRoles` → `manage_roles`, `SendTTSMessages` → `send_tts_messages`. */
|
|
4
|
+
export declare function discordPermissionName(flag: string): string;
|
|
5
|
+
/** All permission flag names the predicate grants. */
|
|
6
|
+
export declare function discordPermissionNames(has: (bit: bigint) => boolean): string[];
|
|
7
|
+
/** Permission names from an interaction's decimal bitfield string (`member.permissions`). */
|
|
8
|
+
export declare function discordPermissionNamesFromBitfield(bitfield: unknown): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Guild-level permission names from a channel-level bitfield (interaction `member.permissions` /
|
|
11
|
+
* `app_permissions`): an administrator holds every flag server-wide; otherwise only the flags that
|
|
12
|
+
* channel overwrites cannot change are reported, so a channel overwrite (e.g. "Manage Permissions" in
|
|
13
|
+
* one channel) never reads as a server-wide `manage_roles`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function discordGuildPermissionNamesFromChannelBitfield(bitfield: unknown): string[];
|
|
16
|
+
/** Normalized standing from guild-level permission names (the guild owner is reported separately). */
|
|
17
|
+
export declare function discordStandingFromPermissions(permissions: readonly string[], isOwner?: boolean): PlatformStanding['standing'];
|
|
18
|
+
export declare function discordSenderStanding(msg: DiscordInboundMessage): PlatformStanding | undefined;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR 0056 §1: Discord guild member → normalized `sender.platform`.
|
|
3
|
+
*
|
|
4
|
+
* - `standing`: guild owner → `owner`; `Administrator` / `ModerateMembers` → `admin`; other members →
|
|
5
|
+
* `member`. Direct messages report nothing.
|
|
6
|
+
* - `roles`: the member's guild roles (`@everyone` excluded), with names when the event carries them.
|
|
7
|
+
* - `permissions`: the member's **guild-level** permission flags as snake_case names (`ban_members`,
|
|
8
|
+
* `manage_roles`, …). Gateway mode reads `GuildMember.permissions` (server-wide, no channel overwrites)
|
|
9
|
+
* for senders and the Bot alike. Interactions only carry channel-level bitfields (overwrites applied),
|
|
10
|
+
* so they report just the flags that cannot be overwritten per channel (identical at both levels) —
|
|
11
|
+
* all flags when the member is an administrator.
|
|
12
|
+
*/
|
|
13
|
+
import { PermissionFlagsBits } from 'discord.js';
|
|
14
|
+
const FLAG_ENTRIES = Object.entries(PermissionFlagsBits);
|
|
15
|
+
/** `ManageRoles` → `manage_roles`, `SendTTSMessages` → `send_tts_messages`. */
|
|
16
|
+
export function discordPermissionName(flag) {
|
|
17
|
+
return flag.replace(/([A-Z]+)([A-Z][a-z])/gu, '$1_$2').replace(/([a-z0-9])([A-Z])/gu, '$1_$2').toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
/** All permission flag names the predicate grants. */
|
|
20
|
+
export function discordPermissionNames(has) {
|
|
21
|
+
const names = [];
|
|
22
|
+
for (const [flag, bit] of FLAG_ENTRIES) {
|
|
23
|
+
try {
|
|
24
|
+
if (has(bit))
|
|
25
|
+
names.push(discordPermissionName(flag));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// A partial permission view cannot answer this bit; leave it out.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return names;
|
|
32
|
+
}
|
|
33
|
+
/** Permission names from an interaction's decimal bitfield string (`member.permissions`). */
|
|
34
|
+
export function discordPermissionNamesFromBitfield(bitfield) {
|
|
35
|
+
const value = parseBitfield(bitfield);
|
|
36
|
+
return value === undefined ? [] : discordPermissionNames(bit => (value & bit) === bit);
|
|
37
|
+
}
|
|
38
|
+
function parseBitfield(bitfield) {
|
|
39
|
+
if (typeof bitfield !== 'string' && typeof bitfield !== 'number' && typeof bitfield !== 'bigint')
|
|
40
|
+
return undefined;
|
|
41
|
+
try {
|
|
42
|
+
return BigInt(bitfield);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Flags Discord does not allow in channel overwrites: a channel-level bitfield shows their guild value. */
|
|
49
|
+
const GUILD_ONLY_FLAGS = [
|
|
50
|
+
PermissionFlagsBits.Administrator, PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers,
|
|
51
|
+
PermissionFlagsBits.ManageGuild, PermissionFlagsBits.ViewAuditLog, PermissionFlagsBits.ViewGuildInsights,
|
|
52
|
+
PermissionFlagsBits.ChangeNickname, PermissionFlagsBits.ManageNicknames, PermissionFlagsBits.ManageGuildExpressions,
|
|
53
|
+
PermissionFlagsBits.ModerateMembers, PermissionFlagsBits.ViewCreatorMonetizationAnalytics,
|
|
54
|
+
PermissionFlagsBits.CreateGuildExpressions,
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Guild-level permission names from a channel-level bitfield (interaction `member.permissions` /
|
|
58
|
+
* `app_permissions`): an administrator holds every flag server-wide; otherwise only the flags that
|
|
59
|
+
* channel overwrites cannot change are reported, so a channel overwrite (e.g. "Manage Permissions" in
|
|
60
|
+
* one channel) never reads as a server-wide `manage_roles`.
|
|
61
|
+
*/
|
|
62
|
+
export function discordGuildPermissionNamesFromChannelBitfield(bitfield) {
|
|
63
|
+
const value = parseBitfield(bitfield);
|
|
64
|
+
if (value === undefined)
|
|
65
|
+
return [];
|
|
66
|
+
if ((value & PermissionFlagsBits.Administrator) === PermissionFlagsBits.Administrator)
|
|
67
|
+
return discordPermissionNames(() => true);
|
|
68
|
+
return discordPermissionNames(bit => GUILD_ONLY_FLAGS.includes(bit) && (value & bit) === bit);
|
|
69
|
+
}
|
|
70
|
+
/** Normalized standing from guild-level permission names (the guild owner is reported separately). */
|
|
71
|
+
export function discordStandingFromPermissions(permissions, isOwner = false) {
|
|
72
|
+
return isOwner ? 'owner'
|
|
73
|
+
: permissions.includes('administrator') || permissions.includes('moderate_members') ? 'admin' : 'member';
|
|
74
|
+
}
|
|
75
|
+
export function discordSenderStanding(msg) {
|
|
76
|
+
if (!msg.guildId)
|
|
77
|
+
return undefined;
|
|
78
|
+
const permissions = msg.platformPermissions ?? [];
|
|
79
|
+
const legacy = msg.permissionTokens ?? [];
|
|
80
|
+
const standing = msg.isGuildOwner
|
|
81
|
+
? 'owner'
|
|
82
|
+
: permissions.includes('administrator') || permissions.includes('moderate_members')
|
|
83
|
+
|| legacy.includes('ADMINISTRATOR') || legacy.includes('MODERATE_MEMBERS')
|
|
84
|
+
? 'admin'
|
|
85
|
+
: 'member';
|
|
86
|
+
return {
|
|
87
|
+
standing,
|
|
88
|
+
...(msg.authorRoles?.length ? { roles: msg.authorRoles } : {}),
|
|
89
|
+
...(permissions.length ? { permissions } : {}),
|
|
90
|
+
};
|
|
91
|
+
}
|