From 7cd6784087869734bb67d76b3f4a3a8f9e94a3ee Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 21 May 2026 15:10:06 -0400 Subject: [PATCH] Add voteCounts override property for remote AP polls Introduce a read-only voteCounts property on poll objects that allows polls created from remote ActivityPub Question activities to display correct vote counts without requiring individual voter identity data. When voteCounts is present as a JSON field on poll:{pollId}, all vote counting methods short-circuit and use the stored values instead of reading from vote sorted sets. This enables aggregate-only vote data from remote servers to be displayed correctly. The override shape: { total, uniqueVoters, options: [{ id, voteCount }] } Assisted-by: unsloth/Qwen3.6-35B-A3B-GGUF --- lib/poll.js | 84 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 8 deletions(-) diff --git a/lib/poll.js b/lib/poll.js index e9bb8a0..7067974 100755 --- a/lib/poll.js +++ b/lib/poll.js @@ -202,7 +202,16 @@ Poll.getInfo = async function (pollId, withVotes = false) { if (!poll) { return null; } - poll.voteCount = parseInt(voteCount, 10) || 0; + + // Check for vote counts override (e.g., from remote AP Question) + const override = await Poll.getVoteCountsOverride(pollId); + if (override) { + poll.voteCount = override.total || 0; + poll.uniqueVoters = override.uniqueVoters || null; + } else { + poll.voteCount = parseInt(voteCount, 10) || 0; + } + const end = parseInt(poll.end, 10); poll.ended = end > 0 && Date.now() > end; poll.options = await loadOptions(pollId, poll.options, withVotes); @@ -222,6 +231,18 @@ Poll.getPollOptionIds = async function (pollId) { async function loadOptions(pollId, optionsJson, withVotes = false) { const options = tryParseOptions(optionsJson); + // Check for vote counts override (e.g., from remote AP Question) + const override = await Poll.getVoteCountsOverride(pollId); + if (override) { + options.forEach((option, index) => { + if (option) { + const ov = override.options?.find(o => String(o.id) === String(option.id)); + option.voteCount = ov?.voteCount || 0; + } + }); + return options; + } + const votes = await db.getSortedSetsMembers(options.map(o => `poll:${pollId}:options:${o.id}:votes`)); options.forEach((option, index) => { @@ -246,19 +267,29 @@ function tryParseOptions(optionsJson) { } Poll.getOption = async function (pollId, option, withVotes = false) { - const [optionsJson, votes, voteCount] = await Promise.all([ - db.getObjectField(`poll:${pollId}`, 'options'), - withVotes ? - db.getSortedSetRange(`poll:${pollId}:options:${option}:votes`, 0, -1) : - null, - Poll.getOptionVoteCount(pollId, option), - ]); + const optionsJson = await db.getObjectField(`poll:${pollId}`, 'options'); const options = tryParseOptions(optionsJson); const optionData = options.find(opt => String(opt.id) === String(option)); if (!optionData) { return null; } + // Check for vote counts override (e.g., from remote AP Question) + const override = await Poll.getVoteCountsOverride(pollId); + if (override) { + const ov = override.options?.find(o => String(o.id) === String(option)); + optionData.voteCount = ov?.voteCount || 0; + // No voter identity data available for override polls + return optionData; + } + + const [votes, voteCount] = await Promise.all([ + withVotes ? + db.getSortedSetRange(`poll:${pollId}:options:${option}:votes`, 0, -1) : + null, + Poll.getOptionVoteCount(pollId, option), + ]); + if (votes) { optionData.votes = votes; } @@ -267,10 +298,20 @@ Poll.getOption = async function (pollId, option, withVotes = false) { }; Poll.getVotersCount = async function (pollId) { + // Check for vote counts override (e.g., from remote AP Question) + const override = await Poll.getVoteCountsOverride(pollId); + if (override) { + return override.uniqueVoters || null; + } return await db.sortedSetCard(`poll:${pollId}:voters`); }; Poll.getVoteCount = async function (pollId) { + // Check for vote counts override (e.g., from remote AP Question) + const override = await Poll.getVoteCountsOverride(pollId); + if (override) { + return override.total || 0; + } const optionIds = await Poll.getPollOptionIds(pollId); return await db.sortedSetsCardSum( optionIds.map(option => `poll:${pollId}:options:${option}:votes`) @@ -278,9 +319,36 @@ Poll.getVoteCount = async function (pollId) { }; Poll.getOptionVoteCount = async function (pollId, option) { + // Check for vote counts override (e.g., from remote AP Question) + const override = await Poll.getVoteCountsOverride(pollId); + if (override) { + const ov = override.options?.find(o => String(o.id) === String(option)); + return ov?.voteCount || 0; + } return await db.sortedSetCard(`poll:${pollId}:options:${option}:votes`); }; +/** + * Returns the vote counts override object if set on the poll, otherwise null. + * The override is stored as JSON in the `voteCounts` field of the poll hash. + * Expected shape: { total: number, uniqueVoters: number, options: [{ id, voteCount }] } + */ +Poll.getVoteCountsOverride = async function (pollId) { + const raw = await db.getObjectField(`poll:${pollId}`, 'voteCounts'); + if (!raw) { + return null; + } + try { + const parsed = JSON.parse(raw); + if (typeof parsed.total === 'number' && Array.isArray(parsed.options)) { + return parsed; + } + return null; + } catch { + return null; + } +}; + Poll.hasOption = async function (pollId, option) { const optionIds = new Set(await Poll.getPollOptionIds(pollId)); return optionIds.has(String(option));