@open-mercato/channel-discord 0.6.8-develop.7090.1.4dbca7b350
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/.turbo/turbo-build.log +2 -0
- package/build.mjs +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.js +86 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.js +39 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.js +97 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.js +65 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.js +110 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.js +103 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.js +93 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.js +52 -0
- package/dist/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/helpers/discordSignature.js +46 -0
- package/dist/modules/channel_discord/__integration__/helpers/discordSignature.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/helpers/eventsQueue.js +16 -0
- package/dist/modules/channel_discord/__integration__/helpers/eventsQueue.js.map +7 -0
- package/dist/modules/channel_discord/__integration__/meta.js +10 -0
- package/dist/modules/channel_discord/__integration__/meta.js.map +7 -0
- package/dist/modules/channel_discord/acl.js +15 -0
- package/dist/modules/channel_discord/acl.js.map +7 -0
- package/dist/modules/channel_discord/ai-agents.js +97 -0
- package/dist/modules/channel_discord/ai-agents.js.map +7 -0
- package/dist/modules/channel_discord/api/ai-auto-reply/channels/route.js +73 -0
- package/dist/modules/channel_discord/api/ai-auto-reply/channels/route.js.map +7 -0
- package/dist/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.js +222 -0
- package/dist/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.js.map +7 -0
- package/dist/modules/channel_discord/api/interactions/route.js +173 -0
- package/dist/modules/channel_discord/api/interactions/route.js.map +7 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.js +196 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.js.map +7 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.js +14 -0
- package/dist/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.js.map +7 -0
- package/dist/modules/channel_discord/cli.js +142 -0
- package/dist/modules/channel_discord/cli.js.map +7 -0
- package/dist/modules/channel_discord/commands/ai-reply-proposal.js +116 -0
- package/dist/modules/channel_discord/commands/ai-reply-proposal.js.map +7 -0
- package/dist/modules/channel_discord/commands/update-ai-auto-reply.js +104 -0
- package/dist/modules/channel_discord/commands/update-ai-auto-reply.js.map +7 -0
- package/dist/modules/channel_discord/di.js +23 -0
- package/dist/modules/channel_discord/di.js.map +7 -0
- package/dist/modules/channel_discord/index.js +9 -0
- package/dist/modules/channel_discord/index.js.map +7 -0
- package/dist/modules/channel_discord/integration.js +96 -0
- package/dist/modules/channel_discord/integration.js.map +7 -0
- package/dist/modules/channel_discord/lib/adapter.js +248 -0
- package/dist/modules/channel_discord/lib/adapter.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-agent-directory.js +45 -0
- package/dist/modules/channel_discord/lib/ai-agent-directory.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-features.js +9 -0
- package/dist/modules/channel_discord/lib/ai-features.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-proposal-contract.js +9 -0
- package/dist/modules/channel_discord/lib/ai-proposal-contract.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-proposal.js +109 -0
- package/dist/modules/channel_discord/lib/ai-proposal.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-reply.js +96 -0
- package/dist/modules/channel_discord/lib/ai-reply.js.map +7 -0
- package/dist/modules/channel_discord/lib/ai-service-principal.js +70 -0
- package/dist/modules/channel_discord/lib/ai-service-principal.js.map +7 -0
- package/dist/modules/channel_discord/lib/capabilities.js +42 -0
- package/dist/modules/channel_discord/lib/capabilities.js.map +7 -0
- package/dist/modules/channel_discord/lib/channel-access.js +62 -0
- package/dist/modules/channel_discord/lib/channel-access.js.map +7 -0
- package/dist/modules/channel_discord/lib/channel-state-store.js +103 -0
- package/dist/modules/channel_discord/lib/channel-state-store.js.map +7 -0
- package/dist/modules/channel_discord/lib/convert-outbound.js +38 -0
- package/dist/modules/channel_discord/lib/convert-outbound.js.map +7 -0
- package/dist/modules/channel_discord/lib/credentials.js +61 -0
- package/dist/modules/channel_discord/lib/credentials.js.map +7 -0
- package/dist/modules/channel_discord/lib/discord-gateway-client.js +289 -0
- package/dist/modules/channel_discord/lib/discord-gateway-client.js.map +7 -0
- package/dist/modules/channel_discord/lib/discord-rest.js +203 -0
- package/dist/modules/channel_discord/lib/discord-rest.js.map +7 -0
- package/dist/modules/channel_discord/lib/failure-reason.js +30 -0
- package/dist/modules/channel_discord/lib/failure-reason.js.map +7 -0
- package/dist/modules/channel_discord/lib/gateway-bridge.js +42 -0
- package/dist/modules/channel_discord/lib/gateway-bridge.js.map +7 -0
- package/dist/modules/channel_discord/lib/health.js +37 -0
- package/dist/modules/channel_discord/lib/health.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-dispatch.js +164 -0
- package/dist/modules/channel_discord/lib/interactions-dispatch.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-handler.js +115 -0
- package/dist/modules/channel_discord/lib/interactions-handler.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-queue.js +17 -0
- package/dist/modules/channel_discord/lib/interactions-queue.js.map +7 -0
- package/dist/modules/channel_discord/lib/interactions-verify.js +84 -0
- package/dist/modules/channel_discord/lib/interactions-verify.js.map +7 -0
- package/dist/modules/channel_discord/lib/normalize-inbound.js +59 -0
- package/dist/modules/channel_discord/lib/normalize-inbound.js.map +7 -0
- package/dist/modules/channel_discord/lib/preset.js +63 -0
- package/dist/modules/channel_discord/lib/preset.js.map +7 -0
- package/dist/modules/channel_discord/lib/queue-strategy.js +12 -0
- package/dist/modules/channel_discord/lib/queue-strategy.js.map +7 -0
- package/dist/modules/channel_discord/lib/slash-commands.js +42 -0
- package/dist/modules/channel_discord/lib/slash-commands.js.map +7 -0
- package/dist/modules/channel_discord/message-types.js +64 -0
- package/dist/modules/channel_discord/message-types.js.map +7 -0
- package/dist/modules/channel_discord/setup.js +62 -0
- package/dist/modules/channel_discord/setup.js.map +7 -0
- package/dist/modules/channel_discord/subscribers/ai-auto-reply.js +185 -0
- package/dist/modules/channel_discord/subscribers/ai-auto-reply.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.js +79 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.js +20 -0
- package/dist/modules/channel_discord/widgets/injection/ai-auto-reply/widget.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.client.js +220 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.client.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.js +17 -0
- package/dist/modules/channel_discord/widgets/injection/connect/widget.js.map +7 -0
- package/dist/modules/channel_discord/widgets/injection-table.js +26 -0
- package/dist/modules/channel_discord/widgets/injection-table.js.map +7 -0
- package/dist/modules/channel_discord/workers/discord-gateway.js +270 -0
- package/dist/modules/channel_discord/workers/discord-gateway.js.map +7 -0
- package/dist/modules/channel_discord/workers/discord-interactions.js +113 -0
- package/dist/modules/channel_discord/workers/discord-interactions.js.map +7 -0
- package/jest.config.cjs +38 -0
- package/package.json +104 -0
- package/src/index.ts +1 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-001.spec.ts +109 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-002.spec.ts +58 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-003.spec.ts +175 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-004.spec.ts +83 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-005.spec.ts +156 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-006.spec.ts +144 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-007.spec.ts +119 -0
- package/src/modules/channel_discord/__integration__/TC-CHANNEL-DISCORD-008.spec.ts +75 -0
- package/src/modules/channel_discord/__integration__/helpers/discordSignature.ts +77 -0
- package/src/modules/channel_discord/__integration__/helpers/eventsQueue.ts +26 -0
- package/src/modules/channel_discord/__integration__/meta.ts +6 -0
- package/src/modules/channel_discord/__tests__/ai-auto-reply.policy.integration.test.ts +286 -0
- package/src/modules/channel_discord/acl.ts +19 -0
- package/src/modules/channel_discord/ai-agents.ts +157 -0
- package/src/modules/channel_discord/api/ai-auto-reply/channels/__tests__/route.test.ts +175 -0
- package/src/modules/channel_discord/api/ai-auto-reply/channels/route.ts +99 -0
- package/src/modules/channel_discord/api/channels/[id]/ai-auto-reply/__tests__/route.test.ts +311 -0
- package/src/modules/channel_discord/api/channels/[id]/ai-auto-reply/route.ts +299 -0
- package/src/modules/channel_discord/api/interactions/route.ts +242 -0
- package/src/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.meta.ts +18 -0
- package/src/modules/channel_discord/backend/channel_discord/channels/[id]/ai-auto-reply/page.tsx +288 -0
- package/src/modules/channel_discord/cli.ts +234 -0
- package/src/modules/channel_discord/commands/__tests__/ai-reply-proposal.test.ts +155 -0
- package/src/modules/channel_discord/commands/__tests__/update-ai-auto-reply.test.ts +185 -0
- package/src/modules/channel_discord/commands/ai-reply-proposal.ts +162 -0
- package/src/modules/channel_discord/commands/update-ai-auto-reply.ts +157 -0
- package/src/modules/channel_discord/di.ts +26 -0
- package/src/modules/channel_discord/i18n/de.json +57 -0
- package/src/modules/channel_discord/i18n/en.json +57 -0
- package/src/modules/channel_discord/i18n/es.json +57 -0
- package/src/modules/channel_discord/i18n/ko.json +57 -0
- package/src/modules/channel_discord/i18n/pl.json +57 -0
- package/src/modules/channel_discord/index.ts +6 -0
- package/src/modules/channel_discord/integration.ts +98 -0
- package/src/modules/channel_discord/lib/__tests__/adapter.test.ts +206 -0
- package/src/modules/channel_discord/lib/__tests__/ai-agent-directory.test.ts +61 -0
- package/src/modules/channel_discord/lib/__tests__/ai-reply.test.ts +115 -0
- package/src/modules/channel_discord/lib/__tests__/capabilities.test.ts +170 -0
- package/src/modules/channel_discord/lib/__tests__/channel-quarantine.test.ts +89 -0
- package/src/modules/channel_discord/lib/__tests__/channel-state-store.auto-reply-marker.test.ts +145 -0
- package/src/modules/channel_discord/lib/__tests__/channel-state-store.test.ts +78 -0
- package/src/modules/channel_discord/lib/__tests__/convert-outbound.test.ts +39 -0
- package/src/modules/channel_discord/lib/__tests__/discord-gateway-client.test.ts +243 -0
- package/src/modules/channel_discord/lib/__tests__/failure-reason.test.ts +68 -0
- package/src/modules/channel_discord/lib/__tests__/gateway-bridge.test.ts +62 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-dispatch.test.ts +223 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-handler.test.ts +469 -0
- package/src/modules/channel_discord/lib/__tests__/interactions-verify.test.ts +126 -0
- package/src/modules/channel_discord/lib/__tests__/normalize-inbound.identity.test.ts +64 -0
- package/src/modules/channel_discord/lib/__tests__/normalize-inbound.test.ts +67 -0
- package/src/modules/channel_discord/lib/__tests__/preset.test.ts +110 -0
- package/src/modules/channel_discord/lib/__tests__/queue-strategy.test.ts +33 -0
- package/src/modules/channel_discord/lib/adapter.ts +359 -0
- package/src/modules/channel_discord/lib/ai-agent-directory.ts +124 -0
- package/src/modules/channel_discord/lib/ai-features.ts +23 -0
- package/src/modules/channel_discord/lib/ai-proposal-contract.ts +24 -0
- package/src/modules/channel_discord/lib/ai-proposal.ts +179 -0
- package/src/modules/channel_discord/lib/ai-reply.ts +179 -0
- package/src/modules/channel_discord/lib/ai-service-principal.ts +143 -0
- package/src/modules/channel_discord/lib/capabilities.ts +104 -0
- package/src/modules/channel_discord/lib/channel-access.ts +110 -0
- package/src/modules/channel_discord/lib/channel-state-store.ts +217 -0
- package/src/modules/channel_discord/lib/convert-outbound.ts +88 -0
- package/src/modules/channel_discord/lib/credentials.ts +88 -0
- package/src/modules/channel_discord/lib/discord-gateway-client.ts +432 -0
- package/src/modules/channel_discord/lib/discord-rest.ts +393 -0
- package/src/modules/channel_discord/lib/failure-reason.ts +49 -0
- package/src/modules/channel_discord/lib/gateway-bridge.ts +79 -0
- package/src/modules/channel_discord/lib/health.ts +55 -0
- package/src/modules/channel_discord/lib/interactions-dispatch.ts +288 -0
- package/src/modules/channel_discord/lib/interactions-handler.ts +267 -0
- package/src/modules/channel_discord/lib/interactions-queue.ts +52 -0
- package/src/modules/channel_discord/lib/interactions-verify.ts +174 -0
- package/src/modules/channel_discord/lib/normalize-inbound.ts +89 -0
- package/src/modules/channel_discord/lib/preset.ts +109 -0
- package/src/modules/channel_discord/lib/queue-strategy.ts +29 -0
- package/src/modules/channel_discord/lib/slash-commands.ts +57 -0
- package/src/modules/channel_discord/message-types.ts +75 -0
- package/src/modules/channel_discord/setup.ts +78 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.armed.test.ts +149 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.failure-marker.test.ts +133 -0
- package/src/modules/channel_discord/subscribers/__tests__/ai-auto-reply.test.ts +273 -0
- package/src/modules/channel_discord/subscribers/ai-auto-reply.ts +325 -0
- package/src/modules/channel_discord/widgets/injection/ai-auto-reply/widget.client.tsx +143 -0
- package/src/modules/channel_discord/widgets/injection/ai-auto-reply/widget.ts +19 -0
- package/src/modules/channel_discord/widgets/injection/connect/widget.client.tsx +237 -0
- package/src/modules/channel_discord/widgets/injection/connect/widget.ts +16 -0
- package/src/modules/channel_discord/widgets/injection-table.ts +24 -0
- package/src/modules/channel_discord/workers/__tests__/discord-gateway.single-identify.test.ts +88 -0
- package/src/modules/channel_discord/workers/__tests__/discord-gateway.test.ts +179 -0
- package/src/modules/channel_discord/workers/__tests__/discord-interactions.test.ts +152 -0
- package/src/modules/channel_discord/workers/discord-gateway.ts +458 -0
- package/src/modules/channel_discord/workers/discord-interactions.ts +162 -0
- package/tsconfig.json +9 -0
- package/watch.mjs +7 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin Discord REST API client. Same trade-off as the Gmail provider's
|
|
3
|
+
* `gmail-client.ts`: we call `fetch` directly (no `discord.js`) so the adapter
|
|
4
|
+
* ships zero heavyweight runtime dependencies and stays swappable in tests via
|
|
5
|
+
* `setDiscordRestClient(...)`.
|
|
6
|
+
*
|
|
7
|
+
* Only the endpoints the adapter actually uses are exposed:
|
|
8
|
+
* - createMessage → POST /channels/{id}/messages
|
|
9
|
+
* - editMessage → PATCH /channels/{id}/messages/{mid}
|
|
10
|
+
* - deleteMessage → DELETE /channels/{id}/messages/{mid}
|
|
11
|
+
* - addReaction → PUT /channels/{id}/messages/{mid}/reactions/{emoji}/@me
|
|
12
|
+
* - removeReaction → DELETE /channels/{id}/messages/{mid}/reactions/{emoji}/@me
|
|
13
|
+
* - listMessages → GET /channels/{id}/messages?before=
|
|
14
|
+
* - getCurrentUser → GET /users/@me
|
|
15
|
+
* - getGatewayBot → GET /gateway/bot
|
|
16
|
+
* - registerGuildCommands → PUT /applications/{app}/guilds/{guild}/commands
|
|
17
|
+
* - editOriginalInteractionResponse → PATCH /webhooks/{app}/{token}/messages/@original
|
|
18
|
+
* - createInteractionFollowUp → POST /webhooks/{app}/{token}
|
|
19
|
+
*/
|
|
20
|
+
import { fetchWithTimeout, FetchTimeoutError } from '@open-mercato/shared/lib/http/fetchWithTimeout'
|
|
21
|
+
|
|
22
|
+
export const DISCORD_API_BASE = 'https://discord.com/api/v10'
|
|
23
|
+
|
|
24
|
+
export interface DiscordAuth {
|
|
25
|
+
botToken: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DiscordUser {
|
|
29
|
+
id: string
|
|
30
|
+
username: string
|
|
31
|
+
global_name?: string | null
|
|
32
|
+
bot?: boolean
|
|
33
|
+
discriminator?: string
|
|
34
|
+
avatar?: string | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Open Mercato marker carried by a message object that was SYNTHESIZED from a
|
|
39
|
+
* Discord interaction rather than received from Discord. Discord never returns
|
|
40
|
+
* this key; it exists so `normalizeInboundDiscordMessage` can tell a hub
|
|
41
|
+
* consumer that the record came from a slash command or a component press
|
|
42
|
+
* instead of someone typing in the channel.
|
|
43
|
+
*/
|
|
44
|
+
export interface DiscordInteractionOrigin {
|
|
45
|
+
id: string
|
|
46
|
+
type: number
|
|
47
|
+
applicationId: string
|
|
48
|
+
commandName?: string
|
|
49
|
+
customId?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface DiscordMessageObject {
|
|
53
|
+
id: string
|
|
54
|
+
channel_id: string
|
|
55
|
+
guild_id?: string
|
|
56
|
+
content: string
|
|
57
|
+
author: DiscordUser
|
|
58
|
+
timestamp: string
|
|
59
|
+
attachments?: Array<{ id: string; url: string; filename: string; content_type?: string; size?: number }>
|
|
60
|
+
message_reference?: { message_id?: string; channel_id?: string; guild_id?: string }
|
|
61
|
+
/** Present only on interaction-derived messages — see `DiscordInteractionOrigin`. */
|
|
62
|
+
discord_interaction?: DiscordInteractionOrigin
|
|
63
|
+
[key: string]: unknown
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CreateMessageInput {
|
|
67
|
+
channelId: string
|
|
68
|
+
content: string
|
|
69
|
+
messageReferenceId?: string
|
|
70
|
+
allowedMentions?: Record<string, unknown>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Body of a message that replaces (or follows) a deferred interaction response.
|
|
75
|
+
*
|
|
76
|
+
* `ephemeral` maps to Discord's `flags: 64` — only the invoking user sees the
|
|
77
|
+
* message. It is the right default for an operational acknowledgement that would
|
|
78
|
+
* otherwise be noise for everyone else in the channel.
|
|
79
|
+
*/
|
|
80
|
+
export interface InteractionResponseInput {
|
|
81
|
+
content: string
|
|
82
|
+
ephemeral?: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Discord message flag: visible only to the user who triggered the interaction. */
|
|
86
|
+
export const DISCORD_MESSAGE_FLAG_EPHEMERAL = 64
|
|
87
|
+
|
|
88
|
+
export interface GatewayBotResponse {
|
|
89
|
+
url: string
|
|
90
|
+
shards: number
|
|
91
|
+
session_start_limit?: { total: number; remaining: number; reset_after: number; max_concurrency: number }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DiscordRestClient {
|
|
95
|
+
createMessage(auth: DiscordAuth, input: CreateMessageInput): Promise<DiscordMessageObject>
|
|
96
|
+
editMessage(auth: DiscordAuth, channelId: string, messageId: string, content: string): Promise<DiscordMessageObject>
|
|
97
|
+
deleteMessage(auth: DiscordAuth, channelId: string, messageId: string): Promise<void>
|
|
98
|
+
addReaction(auth: DiscordAuth, channelId: string, messageId: string, emoji: string): Promise<void>
|
|
99
|
+
removeReaction(auth: DiscordAuth, channelId: string, messageId: string, emoji: string): Promise<void>
|
|
100
|
+
listMessages(
|
|
101
|
+
auth: DiscordAuth,
|
|
102
|
+
channelId: string,
|
|
103
|
+
input: { before?: string; limit?: number },
|
|
104
|
+
): Promise<DiscordMessageObject[]>
|
|
105
|
+
getCurrentUser(auth: DiscordAuth): Promise<DiscordUser>
|
|
106
|
+
getGatewayBot(auth: DiscordAuth): Promise<GatewayBotResponse>
|
|
107
|
+
registerGuildCommands(
|
|
108
|
+
auth: DiscordAuth,
|
|
109
|
+
applicationId: string,
|
|
110
|
+
guildId: string,
|
|
111
|
+
commands: Array<Record<string, unknown>>,
|
|
112
|
+
): Promise<void>
|
|
113
|
+
/**
|
|
114
|
+
* Replace the "thinking…" placeholder a deferred interaction response left
|
|
115
|
+
* behind. This is the call that ends the pending state, so it is the one the
|
|
116
|
+
* dispatch worker makes first.
|
|
117
|
+
*
|
|
118
|
+
* Takes NO bot token: the interaction token in the URL is the credential, and
|
|
119
|
+
* Discord documents these webhook endpoints as requiring no authorization. The
|
|
120
|
+
* token is single-interaction and expires after 15 minutes.
|
|
121
|
+
*/
|
|
122
|
+
editOriginalInteractionResponse(
|
|
123
|
+
applicationId: string,
|
|
124
|
+
interactionToken: string,
|
|
125
|
+
input: InteractionResponseInput,
|
|
126
|
+
): Promise<void>
|
|
127
|
+
/**
|
|
128
|
+
* Post an ADDITIONAL message against the same interaction token. Used as the
|
|
129
|
+
* fallback when the original response can no longer be edited.
|
|
130
|
+
*/
|
|
131
|
+
createInteractionFollowUp(
|
|
132
|
+
applicationId: string,
|
|
133
|
+
interactionToken: string,
|
|
134
|
+
input: InteractionResponseInput,
|
|
135
|
+
): Promise<void>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class DiscordApiError extends Error {
|
|
139
|
+
readonly status: number
|
|
140
|
+
readonly detail: string
|
|
141
|
+
constructor(message: string, status: number, detail: string) {
|
|
142
|
+
super(message)
|
|
143
|
+
this.name = 'DiscordApiError'
|
|
144
|
+
this.status = status
|
|
145
|
+
this.detail = detail
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const DISCORD_MAX_RETRIES = 3
|
|
150
|
+
const DISCORD_BACKOFF_BASE_MS = 500
|
|
151
|
+
const DISCORD_BACKOFF_CAP_MS = 8_000
|
|
152
|
+
const DISCORD_DEFAULT_REQUEST_TIMEOUT_MS = 15_000
|
|
153
|
+
|
|
154
|
+
function resolveRequestTimeoutMs(): number {
|
|
155
|
+
const fromEnv = Number.parseInt(process.env.OM_CHANNEL_DISCORD_REQUEST_TIMEOUT_MS ?? '', 10)
|
|
156
|
+
return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DISCORD_DEFAULT_REQUEST_TIMEOUT_MS
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Discord emoji path encoding: unicode emoji is URL-encoded as-is; a custom
|
|
161
|
+
* emoji uses the `name:id` form. We pass through the caller's value and only
|
|
162
|
+
* `encodeURIComponent` it so both forms survive.
|
|
163
|
+
*/
|
|
164
|
+
function encodeEmoji(emoji: string): string {
|
|
165
|
+
return encodeURIComponent(emoji)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class FetchDiscordRestClient implements DiscordRestClient {
|
|
169
|
+
async createMessage(auth: DiscordAuth, input: CreateMessageInput): Promise<DiscordMessageObject> {
|
|
170
|
+
const body: Record<string, unknown> = {
|
|
171
|
+
content: input.content,
|
|
172
|
+
allowed_mentions: input.allowedMentions ?? { parse: [] },
|
|
173
|
+
}
|
|
174
|
+
if (input.messageReferenceId) {
|
|
175
|
+
body.message_reference = { message_id: input.messageReferenceId }
|
|
176
|
+
}
|
|
177
|
+
return this.request<DiscordMessageObject>(
|
|
178
|
+
auth,
|
|
179
|
+
'POST',
|
|
180
|
+
`/channels/${encodeURIComponent(input.channelId)}/messages`,
|
|
181
|
+
body,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async editMessage(
|
|
186
|
+
auth: DiscordAuth,
|
|
187
|
+
channelId: string,
|
|
188
|
+
messageId: string,
|
|
189
|
+
content: string,
|
|
190
|
+
): Promise<DiscordMessageObject> {
|
|
191
|
+
return this.request<DiscordMessageObject>(
|
|
192
|
+
auth,
|
|
193
|
+
'PATCH',
|
|
194
|
+
`/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}`,
|
|
195
|
+
{ content },
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async deleteMessage(auth: DiscordAuth, channelId: string, messageId: string): Promise<void> {
|
|
200
|
+
await this.request<void>(
|
|
201
|
+
auth,
|
|
202
|
+
'DELETE',
|
|
203
|
+
`/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}`,
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async addReaction(auth: DiscordAuth, channelId: string, messageId: string, emoji: string): Promise<void> {
|
|
208
|
+
await this.request<void>(
|
|
209
|
+
auth,
|
|
210
|
+
'PUT',
|
|
211
|
+
`/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/reactions/${encodeEmoji(emoji)}/@me`,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async removeReaction(auth: DiscordAuth, channelId: string, messageId: string, emoji: string): Promise<void> {
|
|
216
|
+
await this.request<void>(
|
|
217
|
+
auth,
|
|
218
|
+
'DELETE',
|
|
219
|
+
`/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(messageId)}/reactions/${encodeEmoji(emoji)}/@me`,
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async listMessages(
|
|
224
|
+
auth: DiscordAuth,
|
|
225
|
+
channelId: string,
|
|
226
|
+
input: { before?: string; limit?: number },
|
|
227
|
+
): Promise<DiscordMessageObject[]> {
|
|
228
|
+
const search = new URLSearchParams()
|
|
229
|
+
if (input.before) search.set('before', input.before)
|
|
230
|
+
search.set('limit', String(Math.min(Math.max(input.limit ?? 50, 1), 100)))
|
|
231
|
+
const query = search.toString()
|
|
232
|
+
return this.request<DiscordMessageObject[]>(
|
|
233
|
+
auth,
|
|
234
|
+
'GET',
|
|
235
|
+
`/channels/${encodeURIComponent(channelId)}/messages?${query}`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async getCurrentUser(auth: DiscordAuth): Promise<DiscordUser> {
|
|
240
|
+
return this.request<DiscordUser>(auth, 'GET', '/users/@me')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async getGatewayBot(auth: DiscordAuth): Promise<GatewayBotResponse> {
|
|
244
|
+
return this.request<GatewayBotResponse>(auth, 'GET', '/gateway/bot')
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async registerGuildCommands(
|
|
248
|
+
auth: DiscordAuth,
|
|
249
|
+
applicationId: string,
|
|
250
|
+
guildId: string,
|
|
251
|
+
commands: Array<Record<string, unknown>>,
|
|
252
|
+
): Promise<void> {
|
|
253
|
+
await this.request<unknown>(
|
|
254
|
+
auth,
|
|
255
|
+
'PUT',
|
|
256
|
+
`/applications/${encodeURIComponent(applicationId)}/guilds/${encodeURIComponent(guildId)}/commands`,
|
|
257
|
+
commands,
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async editOriginalInteractionResponse(
|
|
262
|
+
applicationId: string,
|
|
263
|
+
interactionToken: string,
|
|
264
|
+
input: InteractionResponseInput,
|
|
265
|
+
): Promise<void> {
|
|
266
|
+
await this.request<unknown>(
|
|
267
|
+
null,
|
|
268
|
+
'PATCH',
|
|
269
|
+
`/webhooks/${encodeURIComponent(applicationId)}/${encodeURIComponent(interactionToken)}/messages/@original`,
|
|
270
|
+
interactionResponseBody(input),
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async createInteractionFollowUp(
|
|
275
|
+
applicationId: string,
|
|
276
|
+
interactionToken: string,
|
|
277
|
+
input: InteractionResponseInput,
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
await this.request<unknown>(
|
|
280
|
+
null,
|
|
281
|
+
'POST',
|
|
282
|
+
`/webhooks/${encodeURIComponent(applicationId)}/${encodeURIComponent(interactionToken)}`,
|
|
283
|
+
interactionResponseBody(input),
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private async request<T>(
|
|
288
|
+
auth: DiscordAuth | null,
|
|
289
|
+
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
|
290
|
+
path: string,
|
|
291
|
+
body?: unknown,
|
|
292
|
+
): Promise<T> {
|
|
293
|
+
const url = `${DISCORD_API_BASE}${path}`
|
|
294
|
+
// `auth === null` is the interaction-webhook family: the token in the path is
|
|
295
|
+
// the credential and Discord rejects nothing for the missing header, so
|
|
296
|
+
// sending the bot token there would leak it for no benefit.
|
|
297
|
+
const headers: Record<string, string> = auth ? { Authorization: `Bot ${auth.botToken}` } : {}
|
|
298
|
+
let payload: BodyInit | undefined
|
|
299
|
+
if (body !== undefined) {
|
|
300
|
+
headers['Content-Type'] = 'application/json'
|
|
301
|
+
payload = JSON.stringify(body)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let attempt = 0
|
|
305
|
+
let lastError: DiscordApiError | null = null
|
|
306
|
+
while (attempt <= DISCORD_MAX_RETRIES) {
|
|
307
|
+
let res: Response
|
|
308
|
+
try {
|
|
309
|
+
res = await fetchWithTimeout(url, { method, headers, body: payload, timeoutMs: resolveRequestTimeoutMs() })
|
|
310
|
+
} catch (err) {
|
|
311
|
+
const errName = (err as { name?: unknown } | null)?.name
|
|
312
|
+
const aborted = err instanceof FetchTimeoutError || errName === 'TimeoutError' || errName === 'AbortError'
|
|
313
|
+
if (!aborted) throw err
|
|
314
|
+
const timeoutError = new DiscordApiError(`Discord API ${method} ${path} timed out`, 599, 'request timed out')
|
|
315
|
+
if (attempt === DISCORD_MAX_RETRIES) throw timeoutError
|
|
316
|
+
lastError = timeoutError
|
|
317
|
+
await sleep(computeBackoff(attempt))
|
|
318
|
+
attempt += 1
|
|
319
|
+
continue
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const text = await res.text()
|
|
323
|
+
if (res.ok) {
|
|
324
|
+
if (!text) return undefined as unknown as T
|
|
325
|
+
return JSON.parse(text) as T
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const detail = parseErrorMessage(text) ?? `${res.status} ${res.statusText}`
|
|
329
|
+
const apiError = new DiscordApiError(`Discord API ${method} ${path} failed: ${detail}`, res.status, detail)
|
|
330
|
+
const transient = res.status === 429 || (res.status >= 500 && res.status < 600)
|
|
331
|
+
if (!transient || attempt === DISCORD_MAX_RETRIES) {
|
|
332
|
+
throw apiError
|
|
333
|
+
}
|
|
334
|
+
lastError = apiError
|
|
335
|
+
const waitMs = parseRetryAfter(res.headers.get('retry-after')) ?? computeBackoff(attempt)
|
|
336
|
+
await sleep(waitMs)
|
|
337
|
+
attempt += 1
|
|
338
|
+
}
|
|
339
|
+
throw lastError ?? new DiscordApiError(`Discord API ${method} ${path} exhausted retries`, 599, 'retries exhausted')
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function interactionResponseBody(input: InteractionResponseInput): Record<string, unknown> {
|
|
344
|
+
return {
|
|
345
|
+
content: input.content,
|
|
346
|
+
// Interaction replies quote whatever the user typed, so mentions inside it
|
|
347
|
+
// must not become live pings — same guard `createMessage` applies.
|
|
348
|
+
allowed_mentions: { parse: [] },
|
|
349
|
+
...(input.ephemeral ? { flags: DISCORD_MESSAGE_FLAG_EPHEMERAL } : {}),
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function parseRetryAfter(value: string | null): number | null {
|
|
354
|
+
if (!value) return null
|
|
355
|
+
const asNumber = Number(value)
|
|
356
|
+
if (Number.isFinite(asNumber) && asNumber >= 0) {
|
|
357
|
+
return Math.min(asNumber * 1000, DISCORD_BACKOFF_CAP_MS)
|
|
358
|
+
}
|
|
359
|
+
return null
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function computeBackoff(attempt: number): number {
|
|
363
|
+
const raw = DISCORD_BACKOFF_BASE_MS * Math.pow(2, attempt)
|
|
364
|
+
const jitter = Math.floor(Math.random() * 100)
|
|
365
|
+
return Math.min(raw + jitter, DISCORD_BACKOFF_CAP_MS)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function sleep(ms: number): Promise<void> {
|
|
369
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function parseErrorMessage(text: string): string | null {
|
|
373
|
+
if (!text) return null
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(text) as { message?: string; error?: string }
|
|
376
|
+
if (typeof parsed?.message === 'string') return parsed.message
|
|
377
|
+
if (typeof parsed?.error === 'string') return parsed.error
|
|
378
|
+
} catch {
|
|
379
|
+
/* fall through */
|
|
380
|
+
}
|
|
381
|
+
return text.length > 200 ? text.slice(0, 200) : text
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let cachedClient: DiscordRestClient | null = null
|
|
385
|
+
|
|
386
|
+
export function getDiscordRestClient(): DiscordRestClient {
|
|
387
|
+
if (!cachedClient) cachedClient = new FetchDiscordRestClient()
|
|
388
|
+
return cachedClient
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function setDiscordRestClient(client: DiscordRestClient | null): void {
|
|
392
|
+
cachedClient = client
|
|
393
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn an arbitrary thrown value into a one-line reason that is safe to persist
|
|
3
|
+
* and render.
|
|
4
|
+
*
|
|
5
|
+
* The AI auto-reply marker stores this on `channelState` and the settings page
|
|
6
|
+
* renders it, so the string leaves the log and enters the product. An upstream
|
|
7
|
+
* provider error is not a curated message: an HTTP client that echoes the failing
|
|
8
|
+
* URL, or an SDK that quotes the Authorization header, would put a live
|
|
9
|
+
* credential in a JSONB column and on an operator's screen. Redacting is cheap;
|
|
10
|
+
* discovering later that a bot token is sitting in `channel_state` is not.
|
|
11
|
+
*
|
|
12
|
+
* The patterns are deliberately broad-but-anchored — a false positive costs one
|
|
13
|
+
* unreadable fragment of a diagnostic, a false negative costs a leaked secret.
|
|
14
|
+
*/
|
|
15
|
+
const SECRET_PATTERNS: readonly RegExp[] = [
|
|
16
|
+
// `Authorization: Bot <token>` / `Bearer <token>`, however it is spelled.
|
|
17
|
+
/\b(?:Bot|Bearer|Basic|token|api[_-]?key|secret|password)\b\s*[:=]?\s*['"]?[A-Za-z0-9._~+/=-]{8,}['"]?/gi,
|
|
18
|
+
// Discord bot tokens and JWT-shaped values: three dot-separated base64url runs.
|
|
19
|
+
/\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{20,}\b/g,
|
|
20
|
+
// Provider API keys that announce themselves with a prefix.
|
|
21
|
+
/\b(?:sk|pk|rk)-[A-Za-z0-9_-]{16,}\b/g,
|
|
22
|
+
// Anything carried in a query string, which is where URLs hide credentials.
|
|
23
|
+
/([?&](?:access_token|token|key|api[_-]?key|secret|signature|sig)=)[^&\s]+/gi,
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
const REDACTED = '[redacted]'
|
|
27
|
+
|
|
28
|
+
export function redactSecrets(text: string): string {
|
|
29
|
+
return SECRET_PATTERNS.reduce(
|
|
30
|
+
// Only the query-string pattern captures a group. `String.replace` passes the
|
|
31
|
+
// match offset in that slot for the patterns that do not, so the type check is
|
|
32
|
+
// load-bearing — without it a redacted value comes back stamped with a number.
|
|
33
|
+
(current, pattern) => current.replace(pattern, (_match, group: unknown) => (
|
|
34
|
+
typeof group === 'string' ? `${group}${REDACTED}` : REDACTED
|
|
35
|
+
)),
|
|
36
|
+
text,
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A one-line, operator-readable reason naming the agent that failed — which is
|
|
42
|
+
* the first thing someone staring at a silent channel needs to know — with the
|
|
43
|
+
* upstream message redacted and its stack left behind.
|
|
44
|
+
*/
|
|
45
|
+
export function describeAgentFailure(agentId: string, err: unknown): string {
|
|
46
|
+
const raw = err instanceof Error ? err.message : String(err)
|
|
47
|
+
const firstLine = raw.split('\n', 1)[0]?.trim() || 'unknown error'
|
|
48
|
+
return `agent ${agentId}: ${redactSecrets(firstLine)}`
|
|
49
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { InboundMessage } from '@open-mercato/core/modules/communication_channels/lib/adapter'
|
|
2
|
+
import type { InboundProcessorPayload } from '@open-mercato/core/modules/communication_channels/workers/inbound-processor'
|
|
3
|
+
import type { ReactionInboundJob } from '@open-mercato/core/modules/communication_channels/lib/reaction-processor-types'
|
|
4
|
+
import type { DiscordMessageObject } from './discord-rest'
|
|
5
|
+
import { getDiscordChannelAdapter } from './adapter'
|
|
6
|
+
import { isAuthoredByBot } from './normalize-inbound'
|
|
7
|
+
import { DISCORD_CONVERSATION_PREFIX } from './normalize-inbound'
|
|
8
|
+
|
|
9
|
+
export interface GatewayChannelScope {
|
|
10
|
+
channelId: string
|
|
11
|
+
channelType: string
|
|
12
|
+
tenantId: string
|
|
13
|
+
organizationId: string | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Build the hub inbound-processor job for a `MESSAGE_CREATE` gateway event.
|
|
18
|
+
*
|
|
19
|
+
* Returns `null` when the message was authored by the bot itself — this is the
|
|
20
|
+
* feedback-loop guard the spec requires (the bot must never ingest + answer its
|
|
21
|
+
* own outbound messages). The hub then dedups by `(channel_id, external_message_id)`.
|
|
22
|
+
*
|
|
23
|
+
* The raw Discord object is wrapped as an `InboundMessage` so the existing
|
|
24
|
+
* `inbound-processor` worker calls `adapter.normalizeInbound` exactly as it does
|
|
25
|
+
* for the webhook path — no hub change.
|
|
26
|
+
*/
|
|
27
|
+
export function buildInboundMessageJob(input: {
|
|
28
|
+
message: DiscordMessageObject
|
|
29
|
+
channel: GatewayChannelScope
|
|
30
|
+
botUserId: string | undefined
|
|
31
|
+
}): InboundProcessorPayload | null {
|
|
32
|
+
const { message, channel, botUserId } = input
|
|
33
|
+
if (isAuthoredByBot(message, botUserId)) return null
|
|
34
|
+
|
|
35
|
+
const raw: InboundMessage = {
|
|
36
|
+
raw: { discordMessage: message as unknown as Record<string, unknown> },
|
|
37
|
+
eventType: 'message',
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
providerKey: 'discord',
|
|
41
|
+
channelId: channel.channelId,
|
|
42
|
+
channelType: channel.channelType,
|
|
43
|
+
raw,
|
|
44
|
+
scope: { tenantId: channel.tenantId, organizationId: channel.organizationId },
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the hub reaction-processor job for a `MESSAGE_REACTION_ADD/REMOVE`
|
|
50
|
+
* gateway event. Returns `null` for reactions the bot itself added. The reaction
|
|
51
|
+
* is normalized through the adapter so it matches the webhook reaction path.
|
|
52
|
+
*/
|
|
53
|
+
export async function buildReactionJob(input: {
|
|
54
|
+
reaction: Record<string, unknown>
|
|
55
|
+
action: 'added' | 'removed'
|
|
56
|
+
channel: GatewayChannelScope
|
|
57
|
+
botUserId: string | undefined
|
|
58
|
+
}): Promise<ReactionInboundJob | null> {
|
|
59
|
+
const { reaction, action, channel, botUserId } = input
|
|
60
|
+
const reactionUserId = typeof reaction.user_id === 'string' ? reaction.user_id : undefined
|
|
61
|
+
if (botUserId && reactionUserId === botUserId) return null
|
|
62
|
+
|
|
63
|
+
const event = await getDiscordChannelAdapter().normalizeInboundReaction({
|
|
64
|
+
raw: { discordReaction: { ...reaction, action } },
|
|
65
|
+
eventType: 'reaction',
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
kind: 'inbound',
|
|
70
|
+
providerKey: 'discord',
|
|
71
|
+
channelId: channel.channelId,
|
|
72
|
+
channelType: channel.channelType,
|
|
73
|
+
event,
|
|
74
|
+
scope: { tenantId: channel.tenantId, organizationId: channel.organizationId },
|
|
75
|
+
attempt: 1,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export { DISCORD_CONVERSATION_PREFIX }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { IntegrationScope } from '@open-mercato/shared/modules/integrations/types'
|
|
2
|
+
import { discordCredentialsSchema } from './credentials'
|
|
3
|
+
import { DiscordApiError, getDiscordRestClient } from './discord-rest'
|
|
4
|
+
|
|
5
|
+
export type HealthCheckStatus = 'healthy' | 'degraded' | 'unhealthy'
|
|
6
|
+
|
|
7
|
+
export interface HealthCheckResult {
|
|
8
|
+
status: HealthCheckStatus
|
|
9
|
+
message?: string
|
|
10
|
+
details?: Record<string, unknown>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Liveness probe for the Discord integration. The hub resolves it by the service
|
|
15
|
+
* name declared in `integration.ts` (`channelDiscordHealthCheck`) and passes the
|
|
16
|
+
* tenant-scoped `IntegrationCredentials` blob (the bot token + ids).
|
|
17
|
+
*
|
|
18
|
+
* The bot token is enough for a real probe: we call `GET /users/@me`. A clean
|
|
19
|
+
* response is `healthy`; a `401` (revoked/invalid token) is `unhealthy` with an
|
|
20
|
+
* actionable reason; any other failure is `unhealthy`.
|
|
21
|
+
*/
|
|
22
|
+
export const channelDiscordHealthCheck = {
|
|
23
|
+
async check(
|
|
24
|
+
credentials: Record<string, unknown> | null,
|
|
25
|
+
_scope: IntegrationScope,
|
|
26
|
+
): Promise<HealthCheckResult> {
|
|
27
|
+
const parsed = discordCredentialsSchema.safeParse(credentials ?? {})
|
|
28
|
+
if (!parsed.success) {
|
|
29
|
+
const first = parsed.error.issues[0]
|
|
30
|
+
return {
|
|
31
|
+
status: 'unhealthy',
|
|
32
|
+
message: `Discord credentials invalid: ${first?.message ?? 'unknown validation error'}`,
|
|
33
|
+
details: { reason: 'invalid_credentials' },
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const user = await getDiscordRestClient().getCurrentUser({ botToken: parsed.data.botToken })
|
|
38
|
+
return {
|
|
39
|
+
status: 'healthy',
|
|
40
|
+
message: 'Discord bot token valid',
|
|
41
|
+
details: { botUserId: user.id, botUsername: user.username },
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const status = error instanceof DiscordApiError ? error.status : 0
|
|
45
|
+
const reason = status === 401 ? 'invalid_token' : 'discord_probe_failed'
|
|
46
|
+
const raw = error instanceof Error ? error.message : 'Discord probe failed'
|
|
47
|
+
const message = raw.replace(/^\[internal\]\s*/, '')
|
|
48
|
+
return {
|
|
49
|
+
status: 'unhealthy',
|
|
50
|
+
message: status === 401 ? 'Discord bot token rejected (401)' : `Discord probe failed: ${message}`,
|
|
51
|
+
details: { reason, status },
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
}
|