134 lines
5.3 KiB
JavaScript
134 lines
5.3 KiB
JavaScript
/**
|
|
* Remote Questions — Discord adapter
|
|
*/
|
|
import { DISCORD_NUMBER_EMOJIS, formatForDiscord, parseDiscordResponse, } from "./format.js";
|
|
import { apiRequest } from "./http-client.js";
|
|
const DISCORD_API = "https://discord.com/api/v10";
|
|
export class DiscordAdapter {
|
|
name = "discord";
|
|
botUserId = null;
|
|
guildId = null;
|
|
token;
|
|
channelId;
|
|
constructor(token, channelId) {
|
|
this.token = token;
|
|
this.channelId = channelId;
|
|
}
|
|
async validate() {
|
|
const res = await this.discordApi("GET", "/users/@me");
|
|
if (!res.id)
|
|
throw new Error("Discord auth failed: invalid token");
|
|
this.botUserId = String(res.id);
|
|
// Resolve guild ID for message URL generation.
|
|
// The channel belongs to a guild — fetch channel info to discover it.
|
|
try {
|
|
const channelInfo = await this.discordApi("GET", `/channels/${this.channelId}`);
|
|
if (channelInfo.guild_id) {
|
|
this.guildId = String(channelInfo.guild_id);
|
|
}
|
|
}
|
|
catch {
|
|
// Non-fatal — message URLs will be omitted if guild ID can't be resolved
|
|
}
|
|
}
|
|
async sendPrompt(prompt) {
|
|
const { embeds, reactionEmojis } = formatForDiscord(prompt);
|
|
const res = await this.discordApi("POST", `/channels/${this.channelId}/messages`, {
|
|
content: "**SF needs your input** — reply to this message with your answer",
|
|
embeds,
|
|
});
|
|
if (!res.id)
|
|
throw new Error(`Discord send failed: ${JSON.stringify(res)}`);
|
|
const messageId = String(res.id);
|
|
if (prompt.questions.length === 1) {
|
|
for (const emoji of reactionEmojis) {
|
|
try {
|
|
await this.discordApi("PUT", `/channels/${this.channelId}/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/@me`);
|
|
}
|
|
catch {
|
|
// Best-effort only
|
|
}
|
|
}
|
|
}
|
|
// Build message URL if guild ID is available
|
|
const messageUrl = this.guildId
|
|
? `https://discord.com/channels/${this.guildId}/${this.channelId}/${messageId}`
|
|
: undefined;
|
|
return {
|
|
ref: {
|
|
id: prompt.id,
|
|
channel: "discord",
|
|
messageId,
|
|
channelId: this.channelId,
|
|
threadUrl: messageUrl,
|
|
},
|
|
};
|
|
}
|
|
async pollAnswer(prompt, ref) {
|
|
if (!this.botUserId)
|
|
await this.validate();
|
|
if (prompt.questions.length === 1) {
|
|
const reactionAnswer = await this.checkReactions(prompt, ref);
|
|
if (reactionAnswer)
|
|
return reactionAnswer;
|
|
}
|
|
return this.checkReplies(prompt, ref);
|
|
}
|
|
/**
|
|
* Acknowledge that an answer was received by adding a ✅ reaction to the
|
|
* original prompt message. Best-effort — failures are silently ignored.
|
|
*/
|
|
async acknowledgeAnswer(ref) {
|
|
try {
|
|
await this.discordApi("PUT", `/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent("✅")}/@me`);
|
|
}
|
|
catch {
|
|
// Best-effort — don't let acknowledgement failures affect the flow
|
|
}
|
|
}
|
|
async checkReactions(prompt, ref) {
|
|
const reactions = [];
|
|
for (const emoji of DISCORD_NUMBER_EMOJIS) {
|
|
try {
|
|
const users = await this.discordApi("GET", `/channels/${ref.channelId}/messages/${ref.messageId}/reactions/${encodeURIComponent(emoji)}`);
|
|
if (Array.isArray(users)) {
|
|
const humanUsers = users.filter((u) => u.id !== this.botUserId);
|
|
if (humanUsers.length > 0)
|
|
reactions.push({ emoji, count: humanUsers.length });
|
|
}
|
|
}
|
|
catch (err) {
|
|
const msg = String(err.message ?? "");
|
|
// 404 = no reactions for this emoji — expected, continue
|
|
if (msg.includes("HTTP 404"))
|
|
continue;
|
|
// 401/403 = auth failure — surface to caller so it can fail the poll
|
|
if (msg.includes("HTTP 401") || msg.includes("HTTP 403"))
|
|
throw err;
|
|
// Other errors (rate limit, network) — skip this emoji, best-effort
|
|
}
|
|
}
|
|
if (reactions.length === 0)
|
|
return null;
|
|
return parseDiscordResponse(reactions, null, prompt.questions);
|
|
}
|
|
async checkReplies(prompt, ref) {
|
|
const messages = await this.discordApi("GET", `/channels/${ref.channelId}/messages?after=${ref.messageId}&limit=10`);
|
|
if (!Array.isArray(messages))
|
|
return null;
|
|
const replies = messages.filter((m) => m.author?.id &&
|
|
m.author.id !== this.botUserId &&
|
|
m.message_reference?.message_id === ref.messageId &&
|
|
m.content);
|
|
if (replies.length === 0)
|
|
return null;
|
|
return parseDiscordResponse([], String(replies[0].content), prompt.questions);
|
|
}
|
|
async discordApi(method, path, body) {
|
|
return apiRequest(`${DISCORD_API}${path}`, method, body, {
|
|
authScheme: "Bot",
|
|
authToken: this.token,
|
|
errorLabel: "Discord API",
|
|
});
|
|
}
|
|
}
|