feat(poll): decrypt poll votes and resolve selected options to plaintext#293
feat(poll): decrypt poll votes and resolve selected options to plaintext#293dgattupalli696 wants to merge 2 commits intoasternic:mainfrom
Conversation
Poll vote webhooks currently only expose the encrypted PollUpdateMessage, which is not useful to downstream consumers. This change: 1. Stores plaintext poll options in-memory keyed by (userID, pollMsgID) when SendPoll is called. 2. On incoming PollUpdateMessage events, calls whatsmeow's DecryptPollVote to obtain the SHA-256 hashes of the selected options, then matches each hash back to the remembered plaintext option. 3. Emits both the decoded plaintext (`selectedOptions`) and the raw base64 hashes (`selectedHashesB64`) on the webhook payload under a `pollVote` key, alongside the existing fields. Falls back gracefully when options were not remembered (e.g. wuzapi restarted between send and vote) — hashes are still emitted so the consumer can perform matching itself if it has retained the options.
There was a problem hiding this comment.
Code Review
This pull request implements plaintext resolution for WhatsApp poll votes by caching sent poll options in memory. It adds storage methods to the ClientManager, captures options in the SendPoll handler, and resolves hashes to strings during event handling. Reviewers suggested removing the redundant DeletePollOptionsForUser method and optimizing the vote hash matching logic using a lookup map to improve performance.
| // DeletePollOptionsForUser drops all cached poll options for a user when | ||
| // their session disconnects or is removed. | ||
| func (cm *ClientManager) DeletePollOptionsForUser(userID string) { | ||
| cm.Lock() | ||
| defer cm.Unlock() | ||
| delete(cm.pollOptions, userID) | ||
| } |
There was a problem hiding this comment.
This function DeletePollOptionsForUser appears to be redundant. Its functionality to clear a user's poll options is already handled within DeleteMyClient, which is called in all relevant cleanup scenarios (like disconnect or logout). To improve code maintainability and remove dead code, I recommend deleting this function.
There was a problem hiding this comment.
Removed the redundant DeletePollOptionsForUser as per suggestion, please recheck
| selected := []string{} | ||
| if stored := clientManager.GetPollOptions(mycli.userID, pollMsgID); len(stored) > 0 { | ||
| for _, h := range hashes { | ||
| for _, opt := range stored { | ||
| sum := sha256.Sum256([]byte(opt)) | ||
| if bytes.Equal(sum[:], h) { | ||
| selected = append(selected, opt) | ||
| break | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation for matching vote hashes to plaintext options uses a nested loop, which has a time complexity of O(number of selected options * total number of options). While this is acceptable for polls with few options, it can be optimized by using a map for faster lookups. This would improve both performance and readability.
You can create a map from the hash of each option to the option's plaintext string, and then iterate over the selected hashes to find the corresponding plaintext options in the map.
selected := make([]string, 0, len(hashes))
if stored := clientManager.GetPollOptions(mycli.userID, pollMsgID); len(stored) > 0 {
// Create a lookup map from hash to plaintext option for efficient matching.
optionsByHash := make(map[string]string, len(stored))
for _, opt := range stored {
sum := sha256.Sum256([]byte(opt))
optionsByHash[string(sum[:])] = opt
}
for _, h := range hashes {
if opt, found := optionsByHash[string(h)]; found {
selected = append(selected, opt)
}
}
}There was a problem hiding this comment.
Changed it hash map as suggested
Addresses gemini-code-assist review comments on asternic#293: - DeleteMyClient already clears pollOptions for the user, so the standalone DeletePollOptionsForUser was dead code. - Replace nested loop in poll vote matching with a precomputed hash->option lookup map for O(1) resolution per selected hash.
Summary
Poll vote webhooks currently expose only the encrypted
PollUpdateMessage, which is not directly useful to downstream consumers: the "selected options" inside are SHA-256 hashes of the original option text, so matching them requires the consumer to independently retain the options it originally sent.This PR teaches wuzapi to do that resolution itself:
When
/chat/send/pollis called, the plaintextOptionslist is cached in-memory keyed by(userID, pollMsgID).When a
PollUpdateMessageevent arrives in the event handler, whatsmeow'sDecryptPollVoteis invoked to recover the selected option hashes.Each hash is SHA-256-matched back against the remembered plaintext options.
The webhook payload gains a new top-level
pollVoteobject:Behaviour
selectedOptionsis empty butselectedHashesB64is still emitted so the consumer can match on its own.DeleteMyClient).Why this is useful
Without plaintext resolution, every downstream system has to:
That's a lot of boilerplate for something wuzapi is already in the best position to do (it sent the poll and therefore already has the options in-process).
Testing
Manually verified end-to-end with a reminder-style poll (
Done/Snooze 30m/Skip today): webhook correctly emittedselectedOptions: ["Done"]when the user tapped Done on the phone.