Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions external_plugins/discord/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,26 +390,37 @@ function chunk(text: string, limit: number, mode: 'length' | 'newline'): string[
}

async function fetchTextChannel(id: string) {
const ch = await client.channels.fetch(id)
let ch = await client.channels.fetch(id)
if (!ch || !ch.isTextBased()) {
throw new Error(`channel ${id} not found or not text-based`)
}
if (ch.partial) {
ch = await ch.fetch()
}
return ch
}

// Outbound gate — tools can only target chats the inbound gate would deliver
// from. DM channel ID ≠ user ID, so we inspect the fetched channel's type.
// Thread → parent lookup mirrors the inbound gate.
async function fetchAllowedChannel(id: string) {
const ch = await fetchTextChannel(id)
// Force bypass Discord.js cache to avoid stale partial objects where
// recipientId is null (DMs) or type/properties are wrong (groups).
// See: https://github.com/anthropics/claude-plugins-official/issues/983
let ch = await client.channels.fetch(id, { force: true })
if (!ch || !ch.isTextBased()) {
throw new Error(`channel ${id} not found or not text-based`)
}
if (ch.partial) ch = await ch.fetch()

const access = loadAccess()
if (ch.type === ChannelType.DM) {
if (access.allowFrom.includes(ch.recipientId)) return ch
} else {
const key = ch.isThread() ? ch.parentId ?? ch.id : ch.id
if (key in access.groups) return ch
}
throw new Error(`channel ${id} is not allowlisted — add via /discord:access`)
throw new Error(`channel ${id} is not allowlisted — add via /discord:access`;
}

async function downloadAttachment(att: Attachment): Promise<string> {
Expand Down
Loading