Add poll chat command with reaction voting

This commit is contained in:
Fisto
2026-09-11 15:59:08 +02:00
parent 8e8c15e8bc
commit ace0bb18ff
16 changed files with 291 additions and 9 deletions
+53 -4
View File
@@ -24,6 +24,9 @@ const SENDABLE_CHANNEL_TYPES = new Set([0, 1, 3, 5, 10, 11, 12]);
/** Discord's message fetch action, located lazily at runtime. */
const MessageActions = findByPropsLazy("fetchMessages");
/** Discord's reaction actions, located lazily at runtime. */
const ReactionActions = findByPropsLazy("addReaction", "removeReaction");
function compareIds(a: string, b: string): number {
try {
const left = BigInt(a);
@@ -122,14 +125,30 @@ function buildState() {
};
}
async function send(channelId: string, content: string) {
async function send(channelId: string, content: string): Promise<unknown> {
try {
await sendMessage(channelId, { content }, false);
return await sendMessage(channelId, { content }, false);
} catch (error) {
throw new ActionError(PuppetErrorCode.SendFailed, `Discord rejected the message: ${String(error)}`);
}
}
/** Find the ID of a just-sent message. Use the send result first, then the store. */
async function resolveMessageId(raw: unknown, channelId: string, content: string): Promise<string | undefined> {
if (raw && typeof raw === "object" && typeof (raw as any).id === "string") return (raw as any).id;
const selfId = UserStore.getCurrentUser()?.id;
for (let attempt = 0; attempt < 10; attempt++) {
const messages: any[] = MessageStore.getMessages(channelId)?._array ?? [];
const match = [...messages].reverse().find(message => message.author?.id === selfId && message.content === content);
if (match) return match.id;
await new Promise(resolve => setTimeout(resolve, 100));
}
return undefined;
}
async function resolveDmChannel(userId: string): Promise<string> {
let channelId = ChannelStore.getDMFromUserId(userId);
if (!channelId) {
@@ -204,8 +223,38 @@ export async function handlePuppetAction(
getSendableChannel(channelId);
enforceOutboundRateLimit();
await send(channelId, content);
return ok({ channelId, sent: true });
const messageId = await resolveMessageId(await send(channelId, content), channelId, content);
return ok({ channelId, messageId, sent: true });
}
case PuppetAction.ReactionAdd: {
const { channelId, messageId, emoji } = params;
assert(isSnowflake(channelId), PuppetErrorCode.InvalidSnowflake, "channelId must be a snowflake");
assert(isSnowflake(messageId), PuppetErrorCode.InvalidSnowflake, "messageId must be a snowflake");
assert(
typeof emoji === "string" && emoji.length > 0 && emoji.length <= 32 && !/[\r\n]/.test(emoji),
PuppetErrorCode.InvalidParam,
"emoji must be a short string"
);
enforceOutboundRateLimit();
const addReaction = (ReactionActions as any)?.addReaction;
assert(
typeof addReaction === "function",
PuppetErrorCode.NotSupported,
"reaction action is not available in this client"
);
try {
await addReaction(channelId, messageId, emoji);
} catch (error) {
throw new ActionError(
PuppetErrorCode.ReactionFailed,
`Discord rejected the reaction: ${String(error)}`
);
}
return ok({ channelId, messageId, emoji, added: true });
}
case PuppetAction.MessageRead: {