Skip to content

feat(poll): decrypt poll votes and resolve selected options to plaintext#293

Open
dgattupalli696 wants to merge 2 commits intoasternic:mainfrom
dgattupalli696:feat/poll-vote-plaintext-options
Open

feat(poll): decrypt poll votes and resolve selected options to plaintext#293
dgattupalli696 wants to merge 2 commits intoasternic:mainfrom
dgattupalli696:feat/poll-vote-plaintext-options

Conversation

@dgattupalli696
Copy link
Copy Markdown

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:

  1. When /chat/send/poll is called, the plaintext Options list is cached in-memory keyed by (userID, pollMsgID).

  2. When a PollUpdateMessage event arrives in the event handler, whatsmeow's DecryptPollVote is invoked to recover the selected option hashes.

  3. Each hash is SHA-256-matched back against the remembered plaintext options.

  4. The webhook payload gains a new top-level pollVote object:

    "pollVote": {
      "pollCreationMsgID": "3EB0...",
      "selectedOptions": ["Done"],
      "selectedHashesB64": ["OS4m...=="]
    }

Behaviour

  • Cache is in-memory only and best-effort — if wuzapi restarts between send and vote, selectedOptions is empty but selectedHashesB64 is still emitted so the consumer can match on its own.
  • Cache is cleared when a client is deleted (DeleteMyClient).
  • Existing webhook fields are untouched; this is purely additive.

Why this is useful

Without plaintext resolution, every downstream system has to:

  • store every outgoing poll's options on its side,
  • re-hash them on vote arrival,
  • match by bytes.

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 emitted selectedOptions: ["Done"] when the user tapped Done on the phone.

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.
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread clients.go Outdated
Comment on lines +127 to +133
// 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)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the redundant DeletePollOptionsForUser as per suggestion, please recheck

Comment thread wmiau.go Outdated
Comment on lines +890 to +901
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
}
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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)
						}
					}
				}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant