Skip to content

handle*: add option to bridge polls with a provisioning API for fetching results - #1135

Merged
iFixRobots merged 11 commits into
mainfrom
iFixRobots/polls
Oct 30, 2026
Merged

iFixRobots merged 11 commits into
mainfrom
iFixRobots/polls

Conversation

@iFixRobots

@iFixRobots iFixRobots commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

With extev_polls enabled, Telegram polls are sent as MSC3381 polls and clients fetch the current results through the poll results provisioning API from mautrix/go#658, since votes aren't tied to a message and anonymous votes aren't visible at all. Polls and votes from Matrix are sent with sendMedia/sendVote.

View in Indent
Tag @indent to continue the conversation here.

@indent

indent Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Issues

All clear! No issues remaining. 🎉

4 issues already resolved
  • Bot logins advertise Poll and PollResultsAPI even though Telegram bots can't call messages.sendVote or messages.getPollResults, so voting and GET /v3/poll_results always fail for them. Capabilities are also advertised when extev_polls is off, while Telegram-created polls arrive as text. (fixed by commit e143fc3)
    Found by Indent Review Agent
  • The partial multi-choice vote overwrite is fixed, but getPollVotes is still called synchronously on every min updateMessagePoll (where mine is nil) with no throttle. It fails with POLL_VOTE_REQUIRED until the user votes and can FLOOD_WAIT, adding an RPC and a warning log per vote. (fixed by commit 70a4015)
    Found by Indent Review Agent
  • One failing getPollResults (deleted poll, flood wait) aborts refresh of the remaining polls, and in HandleMatrixViewingChat the returned error also skips sponsored-message polling, repeating on every view. (fixed by commit 5280ce2)
    Found by Indent Review Agent
  • go.mod pins maunium.net/go/mautrix to the head of the still-open bridgev2: add provisioning API for fetching poll results go#658, a commit that exists only on that feature branch. The poll APIs this PR uses aren't on mautrix/go main, so re-pin to the merge commit or a release after Leave a Portal Room without leaving the matrix channel #658 merges and before merging this PR. go.sum is now tidy. (fixed by commit 4d08e8a)
    Found by Indent Review Agent

CI Checks

All required CI checks passed on e2bbb04.

Review agents

Select any unchecked box below to run or rerun that agent.

Passed (1)
  • Indent Review Agent · Only nits left: max_selections, vote receipts
Full results

Indent Review Agent

  • Summary: Only nits left: max_selections, vote receipts
  • Last ran on commit: e2bbb04d
  • Latest result
    {
      "summary": "Only nits left: max_selections, vote receipts",
      "findings": [
        {
          "fix": "Use `MaxSelections > 1`.",
          "line": 164,
          "path": "pkg/connector/polls.go",
          "issue": "Still present. `MultipleChoice: MaxSelections != 1` treats a missing `max_selections` (parsed as 0) as multiple choice. MSC3381 says the default is 1.",
          "title": "Matrix polls without max_selections become multiple-choice",
          "severity": "nit"
        },
        {
          "fix": "Don't save vote rows under the poll's message ID (use a separate synthetic ID), or don't save a DB row for votes.",
          "line": 231,
          "path": "pkg/connector/polls.go",
          "issue": "Still present. Matrix votes are saved as extra parts with the poll's message ID and PartID `vote:$event`. bridgev2's `handleRemoteReadReceipt` resolves targets with `GetLastPartByID` (ORDER BY part_id DESC), so a Telegram read receipt pointing at the poll lands on the user's latest vote event. That event can be much later in the Matrix timeline, which marks newer messages as read.",
          "title": "Remote read receipts on a voted poll land on the vote event",
          "severity": "nit"
        }
      ]
    }

@iFixRobots
iFixRobots requested a review from tulir October 29, 2026 21:51
Comment thread pkg/connector/capabilities.go Resolved
Comment thread pkg/connector/polls.go Resolved
Comment thread pkg/connector/handlematrix.go Resolved
@indent indent Bot changed the title polls: bridge polls with public voters in both directions polls: bridge polls and fetch results through the provisioning API Oct 29, 2026
Comment thread pkg/connector/capabilities.go Outdated
feat.DeleteChat = topicID == 0
case ids.PeerTypeUser:
baseID += "+dm"
feat.Poll = event.CapLevelRejected

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm, telegram allows forwarding polls into DMs, I wonder if sending new ones is actually blocked or just hidden in the UI

Comment thread pkg/connector/example-config.yaml Outdated
Comment on lines +122 to +123
# Should polls be sent using unstable MSC3381 event types? Votes aren't bridged as events,
# clients fetch the current results with the poll results provisioning API instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The provisioning API detail is fairly irrelevant for people reading this

Suggested change
# Should polls be sent using unstable MSC3381 event types? Votes aren't bridged as events,
# clients fetch the current results with the poll results provisioning API instead.
# Should polls be sent using unstable MSC3381 event types?
# Bridging votes from Telegram is not supported.

Comment thread pkg/connector/tomatrix.go Outdated
Comment on lines 1012 to 1019
"org.matrix.msc3381.poll.start": map[string]any{
"kind": kind,
"max_selections": maxSelections,
"question": map[string]any{
"org.matrix.msc1767.text": poll.Poll.Question.Text,
},
"answers": msc3381Answers,
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There's a struct for this so it's nicer to use that

Suggested change
"org.matrix.msc3381.poll.start": map[string]any{
"kind": kind,
"max_selections": maxSelections,
"question": map[string]any{
"org.matrix.msc1767.text": poll.Poll.Question.Text,
},
"answers": msc3381Answers,
},
"org.matrix.msc3381.poll.start": &event.PollStart{
Kind: kind,
MaxSelections: maxSelections,
Question: event.MSC1767Message{
Text: poll.Poll.Question.Text,
},
Answers msc3381Answers,
},

Comment thread pkg/connector/tomatrix.go Outdated
poll := media.(*tg.MessageMediaPoll)
var textAnswers []string
var htmlAnswers strings.Builder
msc3381Answers := make([]map[string]any, 0, len(poll.Poll.Answers))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
msc3381Answers := make([]map[string]any, 0, len(poll.Poll.Answers))
msc3381Answers := make([]event.PollOption, 0, len(poll.Poll.Answers))

Comment thread pkg/connector/tomatrix.go Outdated
Comment on lines +979 to +982
msc3381Answers = append(msc3381Answers, map[string]any{
"id": pollAnswerID(nil, answer.Option),
"org.matrix.msc1767.text": text,
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
msc3381Answers = append(msc3381Answers, map[string]any{
"id": pollAnswerID(nil, answer.Option),
"org.matrix.msc1767.text": text,
})
msc3381Answers = append(msc3381Answers, event.PollOption{
ID: pollAnswerID(nil, answer.Option),
Text: text,
})

Comment thread pkg/connector/tomatrix.go Outdated
Comment on lines +1008 to +1011
"org.matrix.msc1767.message": []map[string]any{
{"mimetype": "text/html", "body": part.Content.FormattedBody},
{"mimetype": "text/plain", "body": part.Content.Body},
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think nothing actually reads this so let's leave it out

Suggested change
"org.matrix.msc1767.message": []map[string]any{
{"mimetype": "text/html", "body": part.Content.FormattedBody},
{"mimetype": "text/plain", "body": part.Content.Body},
},

Comment thread pkg/connector/tomatrix.go Outdated
"kind": kind,
"max_selections": maxSelections,
"question": map[string]any{
"org.matrix.msc1767.text": poll.Poll.Question.Text,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think both questions and answers on telegram polls can have formatting, which is probably worth bridging if it doesn't complicate this too much

(they can also have media, but that part is definitely complicated, so let's leave it out for now)

Comment thread pkg/connector/polls.go Outdated
Comment on lines +42 to +59
func pollAnswerID(meta *PollMetadata, option []byte) string {
answerID := base64.RawURLEncoding.EncodeToString(option)
if meta != nil {
if mxID, ok := meta.MatrixAnswerIDs[answerID]; ok {
return mxID
}
}
return answerID
}

func pollOption(meta *PollMetadata, answerID string) ([]byte, error) {
for option, mxID := range meta.MatrixAnswerIDs {
if mxID == answerID {
return base64.RawURLEncoding.DecodeString(option)
}
}
return base64.RawURLEncoding.DecodeString(answerID)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

should call these telegramToMatrixAnswerID and matrixToTelegramPollOption for a bit more clarity

@iFixRobots

Copy link
Copy Markdown
Contributor Author

@tulir - It's blocked server side, sending one to a user DM gave us MEDIA_INVALID. tdlib has the same check, forwards are allowed and so are bots and saved messages, so I opened up the capability for those two.

@iFixRobots
iFixRobots requested a review from tulir October 30, 2026 16:48
@tulir tulir changed the title polls: bridge polls and fetch results through the provisioning API handle*: add option to bridge polls with a provisioning API for fetching results Oct 30, 2026
@iFixRobots
iFixRobots merged commit 83d7e6b into main Oct 30, 2026
12 checks passed
@iFixRobots
iFixRobots deleted the iFixRobots/polls branch October 30, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants