Add poll chat command with reaction voting
This commit is contained in:
@@ -38,6 +38,7 @@ const KNOWN_ACTIONS = new Set<string>([
|
||||
PuppetAction.MessageSend,
|
||||
PuppetAction.MessageRead,
|
||||
PuppetAction.MessageFetch,
|
||||
PuppetAction.ReactionAdd,
|
||||
PuppetAction.DmSend
|
||||
]);
|
||||
|
||||
|
||||
@@ -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,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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export const enum PuppetAction {
|
||||
MessageSend = "message.send",
|
||||
MessageRead = "message.read",
|
||||
MessageFetch = "message.fetch",
|
||||
ReactionAdd = "reaction.add",
|
||||
DmSend = "dm.send"
|
||||
}
|
||||
|
||||
@@ -25,6 +26,7 @@ export const PuppetActions: readonly PuppetAction[] = [
|
||||
PuppetAction.MessageSend,
|
||||
PuppetAction.MessageRead,
|
||||
PuppetAction.MessageFetch,
|
||||
PuppetAction.ReactionAdd,
|
||||
PuppetAction.DmSend
|
||||
];
|
||||
|
||||
@@ -41,6 +43,7 @@ export const enum PuppetErrorCode {
|
||||
ChannelNotFound = "CHANNEL_NOT_FOUND",
|
||||
NotSendable = "NOT_SENDABLE",
|
||||
SendFailed = "SEND_FAILED",
|
||||
ReactionFailed = "REACTION_FAILED",
|
||||
DmUnavailable = "DM_UNAVAILABLE",
|
||||
NotSupported = "NOT_SUPPORTED",
|
||||
RateLimited = "RATE_LIMITED",
|
||||
|
||||
Reference in New Issue
Block a user