// The pure decision: does this MESSAGE_CREATE get a turn? No I/O, no clock. // Every drop has one reason string, which the connector journals as a drop // line. The order is cheapest and most conservative first. // // channelInfo: a lookup for channels that are not listed directly, used to // find a thread's parent. It is `(id) => {type, parentId} | undefined`. The // connector fills it from GUILD_CREATE and THREAD_* events and, on a miss, // one REST call. authorize never calls the network itself. export const CHANNEL_TYPE = Object.freeze({ GUILD_TEXT: 0, ANNOUNCEMENT_THREAD: 10, PUBLIC_THREAD: 11, PRIVATE_THREAD: 12, }); const THREAD_TYPES = new Set([CHANNEL_TYPE.ANNOUNCEMENT_THREAD, CHANNEL_TYPE.PUBLIC_THREAD, CHANNEL_TYPE.PRIVATE_THREAD]); export function isThreadType(type) { return THREAD_TYPES.has(type); } export const DROP = Object.freeze({ NOT_OBJECT: "not-an-object", NO_ID: "no-message-id", GUILD: "wrong-guild", BOT: "author-is-bot", WEBHOOK: "webhook", SELF: "author-is-self", USER: "user-unlisted", CHANNEL: "channel-unlisted", THREAD_PARENT: "thread-parent-unlisted", MENTION: "no-mention", }); function mentionsBot(message, botUserId) { if (!Array.isArray(message.mentions)) return false; return message.mentions.some((m) => m && typeof m === "object" && m.id === botUserId); } // Returns {ok: true, channel, thread, oversize} or {ok: false, reason}. export function authorize(binding, message, channelInfo = () => undefined) { if (!message || typeof message !== "object") return { ok: false, reason: DROP.NOT_OBJECT }; if (typeof message.id !== "string" || message.id.length === 0) return { ok: false, reason: DROP.NO_ID }; if (message.guild_id !== binding.guildId) return { ok: false, reason: DROP.GUILD }; const author = message.author && typeof message.author === "object" ? message.author : null; if (!author || typeof author.id !== "string") return { ok: false, reason: DROP.USER }; if (author.id === binding.botUserId) return { ok: false, reason: DROP.SELF }; if (message.webhook_id !== undefined && message.webhook_id !== null) return { ok: false, reason: DROP.WEBHOOK }; if (author.bot === true || author.system === true) return { ok: false, reason: DROP.BOT }; if (!binding.users.some((u) => u.id === author.id)) return { ok: false, reason: DROP.USER }; let channel = binding.channels.find((c) => c.id === message.channel_id); let thread = null; if (!channel) { const info = channelInfo(message.channel_id); if (!info || !isThreadType(info.type)) return { ok: false, reason: DROP.CHANNEL }; if (info.guildId !== undefined && info.guildId !== binding.guildId) return { ok: false, reason: DROP.GUILD }; channel = binding.channels.find((c) => c.id === info.parentId); if (!channel) return { ok: false, reason: DROP.THREAD_PARENT }; thread = { id: message.channel_id, name: typeof info.name === "string" ? info.name : null }; } // A thread inherits the parent's mode (Q4). @everyone is not a mention of // the bot; only an entry in `mentions` with the bot's id counts. if (channel.mode === "mention" && !mentionsBot(message, binding.botUserId)) return { ok: false, reason: DROP.MENTION }; const content = typeof message.content === "string" ? message.content : ""; return { ok: true, channel, thread, oversize: content.length > binding.limits.inboundMaxChars }; }