diff --git a/config/bridge.go b/config/bridge.go index a0c4c84..7e5a06f 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -111,6 +111,7 @@ type BridgeConfig struct { FederateRooms bool `yaml:"federate_rooms"` URLPreviews bool `yaml:"url_previews"` CaptionInMessage bool `yaml:"caption_in_message"` + ExtEvPolls int `yaml:"extev_polls"` SendWhatsAppEdits bool `yaml:"send_whatsapp_edits"` MessageHandlingTimeout struct { diff --git a/config/upgrade.go b/config/upgrade.go index 7dfe962..9d104fb 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -94,6 +94,7 @@ func DoUpgrade(helper *up.Helper) { helper.Copy(up.Bool, "bridge", "crash_on_stream_replaced") helper.Copy(up.Bool, "bridge", "url_previews") helper.Copy(up.Bool, "bridge", "caption_in_message") + helper.Copy(up.Int, "bridge", "extev_polls") helper.Copy(up.Bool, "bridge", "send_whatsapp_edits") helper.Copy(up.Str|up.Null, "bridge", "message_handling_timeout", "error_after") helper.Copy(up.Str|up.Null, "bridge", "message_handling_timeout", "deadline") diff --git a/example-config.yaml b/example-config.yaml index bc3e55e..8790f3f 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -298,6 +298,9 @@ bridge: # Send captions in the same message as images. This will send data compatible with both MSC2530 and MSC3552. # This is currently not supported in most clients. caption_in_message: false + # Should polls be sent using MSC3381 event types? This should either be 1 for original polls MSC, + # 2 for the updated MSC as of November 2022, or 0 to use legacy m.room.message (which doesn't support voting). + extev_polls: 0 # Should Matrix edits be bridged to WhatsApp edits? # Official WhatsApp clients don't render edits yet, but once they do, the bridge should work with them right away. send_whatsapp_edits: false diff --git a/portal.go b/portal.go index 020e64c..43b5fa8 100644 --- a/portal.go +++ b/portal.go @@ -19,6 +19,8 @@ package main import ( "bytes" "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -533,6 +535,8 @@ func (portal *Portal) convertMessage(intent *appservice.IntentAPI, source *User, return portal.convertListResponseMessage(intent, waMsg.GetListResponseMessage()) case waMsg.PollCreationMessage != nil: return portal.convertPollCreationMessage(intent, waMsg.GetPollCreationMessage()) + case waMsg.PollUpdateMessage != nil: + return portal.convertPollUpdateMessage(intent, source, info, waMsg.GetPollUpdateMessage()) case waMsg.ImageMessage != nil: return portal.convertMediaMessage(intent, source, info, waMsg.GetImageMessage(), "photo", isBackfill) case waMsg.StickerMessage != nil: @@ -2095,20 +2099,88 @@ func (portal *Portal) convertListResponseMessage(intent *appservice.IntentAPI, m } } +func (portal *Portal) convertPollUpdateMessage(intent *appservice.IntentAPI, source *User, info *types.MessageInfo, msg *waProto.PollUpdateMessage) *ConvertedMessage { + if portal.bridge.Config.Bridge.ExtEvPolls == 0 { + return nil + } + pollMessage := portal.bridge.DB.Message.GetByJID(portal.Key, msg.GetPollCreationMessageKey().GetId()) + if pollMessage == nil { + portal.log.Warnfln("Failed to convert vote message %s: poll message %s not found", info.ID, msg.GetPollCreationMessageKey().GetId()) + return nil + } + vote, err := source.Client.DecryptPollVote(&events.Message{ + Info: *info, + Message: &waProto.Message{PollUpdateMessage: msg}, + }) + if err != nil { + portal.log.Errorfln("Failed to decrypt vote message %s: %v", info.ID, err) + return nil + } + selectedHashes := make([]string, len(vote.GetSelectedOptions())) + for i, opt := range vote.GetSelectedOptions() { + selectedHashes[i] = hex.EncodeToString(opt) + } + + evtType := event.Type{Class: event.MessageEventType, Type: "org.matrix.msc3381.poll.response"} + if portal.bridge.Config.Bridge.ExtEvPolls == 2 { + evtType.Type = "org.matrix.msc3381.v2.poll.response" + } + return &ConvertedMessage{ + Intent: intent, + Type: evtType, + Content: &event.MessageEventContent{ + RelatesTo: &event.RelatesTo{ + Type: event.RelReference, + EventID: pollMessage.MXID, + }, + }, + Extra: map[string]any{ + "org.matrix.msc3381.poll.response": map[string]any{ + "answers": selectedHashes, + }, + "org.matrix.msc3381.v2.selections": selectedHashes, + }, + } +} + func (portal *Portal) convertPollCreationMessage(intent *appservice.IntentAPI, msg *waProto.PollCreationMessage) *ConvertedMessage { - optionsListText := make([]string, len(msg.GetOptions())) - optionsListHTML := make([]string, len(msg.GetOptions())) optionNames := make([]string, len(msg.GetOptions())) + optionsListText := make([]string, len(optionNames)) + optionsListHTML := make([]string, len(optionNames)) + msc3381Answers := make([]map[string]any, len(optionNames)) + msc3381V2Answers := make([]map[string]any, len(optionNames)) for i, opt := range msg.GetOptions() { optionNames[i] = opt.GetOptionName() optionsListText[i] = fmt.Sprintf("%d. %s\n", i+1, optionNames[i]) optionsListHTML[i] = fmt.Sprintf("
%s