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: {
+34
View File
@@ -34,6 +34,22 @@ function normalizeMessage(event: any): PuppetEvent | undefined {
};
}
function normalizeReaction(event: any, type: "MESSAGE_REACTION_ADD" | "MESSAGE_REACTION_REMOVE"): PuppetEvent | undefined {
if (!event?.messageId || !event?.userId) return undefined;
return {
type,
messageId: event.messageId,
channelId: event.channelId,
guildId: event.guildId ?? null,
userId: event.userId,
emoji: {
name: event.emoji?.name ?? null,
id: event.emoji?.id ?? null
}
};
}
let installed = false;
export function initPuppetEvents() {
@@ -50,6 +66,24 @@ export function initPuppetEvents() {
}
});
dispatcher.subscribe("MESSAGE_REACTION_ADD", (event: any) => {
try {
const normalized = normalizeReaction(event, "MESSAGE_REACTION_ADD");
if (normalized) VesktopNative.puppet.emit(normalized);
} catch (error) {
console.error("[puppet] reaction normalization failed:", error);
}
});
dispatcher.subscribe("MESSAGE_REACTION_REMOVE", (event: any) => {
try {
const normalized = normalizeReaction(event, "MESSAGE_REACTION_REMOVE");
if (normalized) VesktopNative.puppet.emit(normalized);
} catch (error) {
console.error("[puppet] reaction normalization failed:", error);
}
});
console.log("[veskslop] puppet events active");
});
}