From 149e9bc8af27cc089ebe4ef1a64ba1fdcc458b67 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 28 Oct 2021 12:59:22 +0300 Subject: [PATCH] Implement WhatsApp->Matrix group info updates --- config/bridge.go | 28 ++-- example-config.yaml | 44 ++----- go.mod | 2 +- go.sum | 4 +- portal.go | 313 +++++++++++++++++++++++--------------------- user.go | 213 +++++++++++++----------------- 6 files changed, 280 insertions(+), 324 deletions(-) diff --git a/config/bridge.go b/config/bridge.go index d597833..7000d92 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -42,15 +42,13 @@ type BridgeConfig struct { End bool `yaml:"end"` } `yaml:"call_notices"` - InitialChatSync int `yaml:"initial_chat_sync_count"` - InitialHistoryFill int `yaml:"initial_history_fill_count"` - HistoryDisableNotifs bool `yaml:"initial_history_disable_notifications"` - RecoverChatSync int `yaml:"recovery_chat_sync_count"` - RecoverHistory bool `yaml:"recovery_history_backfill"` - ChatMetaSync bool `yaml:"chat_meta_sync"` - UserAvatarSync bool `yaml:"user_avatar_sync"` - BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"` - SyncChatMaxAge int64 `yaml:"sync_max_chat_age"` + HistorySync struct { + CreatePortals bool `yaml:"create_portals"` + Backfill bool `yaml:"backfill"` + DoublePuppetBackfill bool `yaml:"double_puppet_backfill"` + } + UserAvatarSync bool `yaml:"user_avatar_sync"` + BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"` SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` SyncDirectChatList bool `yaml:"sync_direct_chat_list"` @@ -58,7 +56,6 @@ type BridgeConfig struct { DefaultBridgePresence bool `yaml:"default_bridge_presence"` LoginSharedSecret string `yaml:"login_shared_secret"` - DoublePuppetBackfill bool `yaml:"double_puppet_backfill"` PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` BridgeNotices bool `yaml:"bridge_notices"` ResendBridgeInfo bool `yaml:"resend_bridge_info"` @@ -95,7 +92,6 @@ type BridgeConfig struct { } func (bc *BridgeConfig) setDefaults() { - bc.DeliveryReceipts = false bc.MaxConnectionAttempts = 3 bc.ConnectionRetryDelay = -1 bc.ReportConnectionRetry = true @@ -104,22 +100,14 @@ func (bc *BridgeConfig) setDefaults() { bc.CallNotices.Start = true bc.CallNotices.End = true - bc.InitialChatSync = 10 - bc.InitialHistoryFill = 20 - bc.RecoverChatSync = -1 - bc.RecoverHistory = true - bc.ChatMetaSync = true + bc.HistorySync.CreatePortals = true bc.UserAvatarSync = true bc.BridgeMatrixLeave = true - bc.SyncChatMaxAge = 259200 bc.SyncWithCustomPuppets = true bc.DefaultBridgePresence = true bc.DefaultBridgeReceipts = true - bc.LoginSharedSecret = "" - bc.DoublePuppetBackfill = false - bc.PrivateChatPortalMeta = false bc.BridgeNotices = true bc.EnableStatusBroadcast = true } diff --git a/example-config.yaml b/example-config.yaml index 5177d54..dac7dd7 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -101,32 +101,20 @@ bridge: start: true end: true - # Number of chats to sync for new users. - initial_chat_sync_count: 10 - # Number of old messages to fill when creating new portal rooms. - initial_history_fill_count: 20 - # Whether or not notifications should be turned off while filling initial history. - # Only applicable when using double puppeting. - initial_history_disable_notifications: false - # Maximum number of chats to sync when recovering from downtime. - # Set to -1 to sync all new chats during downtime. - recovery_chat_sync_limit: -1 - # Whether or not to sync history when recovering from downtime. - recovery_history_backfill: true - # Whether or not portal info should be fetched from the server when syncing, - # instead of relying on finding any changes in the message history. - # If you get 599 errors often, you should try disabling this. - chat_meta_sync: true + history_sync: + # Whether to create portals from history sync payloads from WhatsApp. + create_portals: true + # Whether to enable backfilling history sync payloads from WhatsApp using batch sending + # This requires a server with MSC2716 support, which is currently an experimental feature in synapse. + # It can be enabled by setting experimental_features -> enable_msc2716 to true in homeserver.yaml. + backfill: false + # Whether to use custom puppet for backfilling. + # In order to use this, the custom puppets must be in the appservice's user ID namespace. + double_puppet_backfill: false # Whether or not puppet avatars should be fetched from the server even if an avatar is already set. - # If you get 599 errors often, you should try disabling this. user_avatar_sync: true # Whether or not Matrix users leaving groups should be bridged to WhatsApp bridge_matrix_leave: true - # Maximum number of seconds since last message in chat to skip - # syncing the chat in any case. This setting will take priority - # over both recovery_chat_sync_limit and initial_chat_sync_count. - # Default is 3 days = 259200 seconds - sync_max_chat_age: 259200 # Whether or not to sync with custom puppets to receive EDUs that # are not normally sent to appservices. @@ -147,18 +135,12 @@ bridge: # manually. login_shared_secret: null - # Whether to use custom puppet for backfilling. - # In order to use this, the custom puppets must be in the appservice's user ID namespace. - double_puppet_backfill: false - # Whether or not to explicitly set the avatar and room name for private - # chat portal rooms. This can be useful if the previous field works fine, - # but causes room avatar/name bugs. + # Whether to explicitly set the avatar and room name for private chat portal rooms. private_chat_portal_meta: false - # Whether or not Matrix m.notice-type messages should be bridged. + # Whether Matrix m.notice-type messages should be bridged. bridge_notices: true # Set this to true to tell the bridge to re-send m.bridge events to all rooms on the next run. - # This field will automatically be changed back to false after it, - # except if the config file is not writable. + # This field will automatically be changed back to false after it, except if the config file is not writable. resend_bridge_info: false # When using double puppeting, should muted chats be muted in Matrix? mute_bridging: false diff --git a/go.mod b/go.mod index 5ba5f40..286d7e9 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.9 github.com/prometheus/client_golang v1.11.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48 + go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600 golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d google.golang.org/protobuf v1.27.1 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index 3a35926..e26cce5 100644 --- a/go.sum +++ b/go.sum @@ -139,8 +139,8 @@ github.com/tidwall/sjson v1.2.3 h1:5+deguEhHSEjmuICXZ21uSSsXotWMA0orU783+Z7Cp8= github.com/tidwall/sjson v1.2.3/go.mod h1:5WdjKx3AQMvCJ4RG6/2UYT7dLrGvJUV1x4jdTAyGvZs= go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2 h1:xpQTMgJGGaF+c8jV/LA/FVXAPJxZbSAGeflOc+Ly6uQ= go.mau.fi/libsignal v0.0.0-20211024113310-f9fc6a1855f2/go.mod h1:3XlVlwOfp8f9Wri+C1D4ORqgUsN4ZvunJOoPjQMBhos= -go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48 h1:e4cAP66APziJd8YFAJbYtPtkMJLi4wullnqs87lWZWo= -go.mau.fi/whatsmeow v0.0.0-20211027183133-07bcb11ceb48/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI= +go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600 h1:3huw0OOUNmU1c9vJHifEdTJJnFn6UchoHFaazdHkd34= +go.mau.fi/whatsmeow v0.0.0-20211028095847-2a72655ef600/go.mod h1:ODEmmqeUn9eBDQHFc1S902YA3YFLtmaBujYRRFl53jI= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/portal.go b/portal.go index 714993b..f55603e 100644 --- a/portal.go +++ b/portal.go @@ -193,14 +193,6 @@ type Portal struct { hasRelaybot *bool } -func (portal *Portal) syncDoublePuppetDetailsAfterCreate(source *User) { - doublePuppet := portal.bridge.GetPuppetByCustomMXID(source.MXID) - if doublePuppet == nil { - return - } - source.syncChatDoublePuppetDetails(doublePuppet, portal, true) -} - func (portal *Portal) handleMessageLoop() { for msg := range portal.messages { if len(portal.MXID) == 0 { @@ -214,7 +206,6 @@ func (portal *Portal) handleMessageLoop() { portal.log.Errorln("Failed to create portal room:", err) continue } - portal.syncDoublePuppetDetailsAfterCreate(msg.source) } if msg.evt != nil { portal.handleMessage(msg.source, msg.evt) @@ -310,6 +301,7 @@ func (portal *Portal) convertMessage(intent *appservice.IntentAPI, source *User, const UndecryptableMessageNotice = "Decrypting message from WhatsApp failed, waiting for sender to re-send... " + "([learn more](https://faq.whatsapp.com/general/security-and-privacy/seeing-waiting-for-this-message-this-may-take-a-while))" + var undecryptableMessageContent event.MessageEventContent func init() { @@ -391,7 +383,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) { _, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{ Reason: "The undecryptable message was actually the deletion of another message", }) - existingMsg.UpdateMXID("net.maunium.whatsapp.fake::" + existingMsg.MXID, false) + existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, false) } } else { portal.log.Warnln("Unhandled message:", evt.Info, evt.Message) @@ -399,7 +391,7 @@ func (portal *Portal) handleMessage(source *User, evt *events.Message) { _, _ = portal.MainIntent().RedactEvent(portal.MXID, existingMsg.MXID, mautrix.ReqRedact{ Reason: "The undecryptable message contained an unsupported message type", }) - existingMsg.UpdateMXID("net.maunium.whatsapp.fake::" + existingMsg.MXID, false) + existingMsg.UpdateMXID("net.maunium.whatsapp.fake::"+existingMsg.MXID, false) } return } @@ -557,7 +549,7 @@ func (portal *Portal) SyncParticipants(source *User, metadata *types.GroupInfo) portal.kickExtraUsers(participantMap) } -func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool { +func (portal *Portal) UpdateAvatar(user *User, setBy types.JID, updateInfo bool) bool { avatar, err := user.Client.GetProfilePictureInfo(portal.Key.JID, false) if err != nil { if !errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) { @@ -585,7 +577,14 @@ func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool { } if len(portal.MXID) > 0 { - _, err := portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL) + intent := portal.MainIntent() + if !setBy.IsEmpty() { + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) + } + _, err = intent.SetRoomAvatar(portal.MXID, portal.AvatarURL) + if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() { + _, err = portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL) + } if err != nil { portal.log.Warnln("Failed to set room topic:", err) return false @@ -598,20 +597,22 @@ func (portal *Portal) UpdateAvatar(user *User, updateInfo bool) bool { return true } -func (portal *Portal) UpdateName(name string, setBy types.JID, intent *appservice.IntentAPI, updateInfo bool) bool { +func (portal *Portal) UpdateName(name string, setBy types.JID, updateInfo bool) bool { if name == "" && portal.IsBroadcastList() { name = UnnamedBroadcastName } if portal.Name != name { portal.log.Debugfln("Updating name %s -> %s", portal.Name, name) portal.Name = name - if intent == nil { - intent = portal.MainIntent() - if !setBy.IsEmpty() { - intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) - } + + intent := portal.MainIntent() + if !setBy.IsEmpty() { + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) } _, err := intent.SetRoomName(portal.MXID, name) + if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() { + _, err = portal.MainIntent().SetRoomName(portal.MXID, name) + } if err == nil { if updateInfo { portal.UpdateBridgeInfo() @@ -625,17 +626,19 @@ func (portal *Portal) UpdateName(name string, setBy types.JID, intent *appservic return false } -func (portal *Portal) UpdateTopic(topic string, setBy types.JID, intent *appservice.IntentAPI, updateInfo bool) bool { +func (portal *Portal) UpdateTopic(topic string, setBy types.JID, updateInfo bool) bool { if portal.Topic != topic { portal.log.Debugfln("Updating topic %s -> %s", portal.Topic, topic) portal.Topic = topic - if intent == nil { - intent = portal.MainIntent() - if !setBy.IsEmpty() { - intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) - } + + intent := portal.MainIntent() + if !setBy.IsEmpty() { + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) } _, err := intent.SetRoomTopic(portal.MXID, topic) + if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() { + _, err = portal.MainIntent().SetRoomTopic(portal.MXID, topic) + } if err == nil { if updateInfo { portal.UpdateBridgeInfo() @@ -654,8 +657,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool { return false } else if portal.IsStatusBroadcastList() { update := false - update = portal.UpdateName(StatusBroadcastName, types.EmptyJID, nil, false) || update - update = portal.UpdateTopic(StatusBroadcastTopic, types.EmptyJID, nil, false) || update + update = portal.UpdateName(StatusBroadcastName, types.EmptyJID, false) || update + update = portal.UpdateTopic(StatusBroadcastTopic, types.EmptyJID, false) || update return update } else if portal.IsBroadcastList() { update := false @@ -680,8 +683,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool { portal.SyncParticipants(user, metadata) update := false - update = portal.UpdateName(metadata.Name, metadata.NameSetBy, nil, false) || update - update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, nil, false) || update + update = portal.UpdateName(metadata.Name, metadata.NameSetBy, false) || update + update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, false) || update portal.RestrictMessageSending(metadata.IsAnnounce) portal.RestrictMetadataChanges(metadata.IsLocked) @@ -764,7 +767,7 @@ func (portal *Portal) Sync(user *User) bool { update := false update = portal.UpdateMetadata(user) || update if !portal.IsPrivateChat() && !portal.IsBroadcastList() && portal.Avatar == "" { - update = portal.UpdateAvatar(user, false) || update + update = portal.UpdateAvatar(user, types.EmptyJID, false) || update } if update { portal.Update() @@ -798,35 +801,35 @@ func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent { } } -//func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) id.EventID { -// levels, err := portal.MainIntent().PowerLevels(portal.MXID) -// if err != nil { -// levels = portal.GetBasePowerLevels() -// } -// newLevel := 0 -// if setAdmin { -// newLevel = 50 -// } -// changed := false -// for _, jid := range jids { -// puppet := portal.bridge.GetPuppetByJID(jid) -// changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed -// -// user := portal.bridge.GetUserByJID(jid) -// if user != nil { -// changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed -// } -// } -// if changed { -// resp, err := portal.MainIntent().SetPowerLevels(portal.MXID, levels) -// if err != nil { -// portal.log.Errorln("Failed to change power levels:", err) -// } else { -// return resp.EventID -// } -// } -// return "" -//} +func (portal *Portal) ChangeAdminStatus(jids []types.JID, setAdmin bool) id.EventID { + levels, err := portal.MainIntent().PowerLevels(portal.MXID) + if err != nil { + levels = portal.GetBasePowerLevels() + } + newLevel := 0 + if setAdmin { + newLevel = 50 + } + changed := false + for _, jid := range jids { + puppet := portal.bridge.GetPuppetByJID(jid) + changed = levels.EnsureUserLevel(puppet.MXID, newLevel) || changed + + user := portal.bridge.GetUserByJID(jid) + if user != nil { + changed = levels.EnsureUserLevel(user.MXID, newLevel) || changed + } + } + if changed { + resp, err := portal.MainIntent().SetPowerLevels(portal.MXID, levels) + if err != nil { + portal.log.Errorln("Failed to change power levels:", err) + } else { + return resp.EventID + } + } + return "" +} func (portal *Portal) RestrictMessageSending(restrict bool) id.EventID { levels, err := portal.MainIntent().PowerLevels(portal.MXID) @@ -1079,7 +1082,7 @@ func (portal *Portal) backfill(source *User, messages []*waProto.HistorySyncMsg) intent = puppet.DefaultIntent() } else { intent = puppet.IntentFor(portal) - if intent.IsCustomPuppet && !portal.bridge.Config.Bridge.DoublePuppetBackfill { + if intent.IsCustomPuppet && !portal.bridge.Config.Bridge.HistorySync.DoublePuppetBackfill { intent = puppet.DefaultIntent() addMember(puppet) } @@ -1266,7 +1269,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { portal.Name = metadata.Name portal.Topic = metadata.Topic } - portal.UpdateAvatar(user, false) + portal.UpdateAvatar(user, types.EmptyJID, false) } bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo() @@ -1337,6 +1340,7 @@ func (portal *Portal) CreateMatrixRoom(user *User) error { } portal.ensureUserInvited(user) + user.syncChatDoublePuppetDetails(portal, true) if metadata != nil { portal.SyncParticipants(user, metadata) @@ -1678,95 +1682,104 @@ func (portal *Portal) convertContactMessage(intent *appservice.IntentAPI, msg *w return &ConvertedMessage{Intent: intent, Type: event.EventMessage, Content: content} } -// FIXME -//func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI) error { -// _, err := intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID}) -// if err != nil { -// httpErr, ok := err.(mautrix.HTTPError) -// if ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_FORBIDDEN" { -// _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID}) -// } -// } -// return err -//} -// -//func (portal *Portal) removeUser(isSameUser bool, kicker *appservice.IntentAPI, target id.UserID, targetIntent *appservice.IntentAPI) { -// if !isSameUser || targetIntent == nil { -// err := portal.tryKickUser(target, kicker) -// if err != nil { -// portal.log.Warnfln("Failed to kick %s from %s: %v", target, portal.MXID, err) -// if targetIntent != nil { -// _, _ = targetIntent.LeaveRoom(portal.MXID) -// } -// } -// } else { -// _, err := targetIntent.LeaveRoom(portal.MXID) -// if err != nil { -// portal.log.Warnfln("Failed to leave portal as %s: %v", target, err) -// _, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: target}) -// } -// } -//} -// -//func (portal *Portal) HandleWhatsAppKick(source *User, senderJID string, jids []string) { -// sender := portal.bridge.GetPuppetByJID(senderJID) -// senderIntent := sender.IntentFor(portal) -// for _, jid := range jids { -// if source != nil && source.JID == jid { -// portal.log.Debugln("Ignoring self-kick by", source.MXID) -// continue -// } -// puppet := portal.bridge.GetPuppetByJID(jid) -// portal.removeUser(puppet.JID == sender.JID, senderIntent, puppet.MXID, puppet.DefaultIntent()) -// -// if !portal.IsBroadcastList() { -// user := portal.bridge.GetUserByJID(jid) -// if user != nil { -// var customIntent *appservice.IntentAPI -// if puppet.CustomMXID == user.MXID { -// customIntent = puppet.CustomIntent() -// } -// portal.removeUser(puppet.JID == sender.JID, senderIntent, user.MXID, customIntent) -// } -// } -// } -//} -// -//func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID string, intent *appservice.IntentAPI, jids []string) (evtID id.EventID) { -// if intent == nil { -// intent = portal.MainIntent() -// if senderJID != "unknown" { -// sender := portal.bridge.GetPuppetByJID(senderJID) -// intent = sender.IntentFor(portal) -// } -// } -// for _, jid := range jids { -// puppet := portal.bridge.GetPuppetByJID(jid) -// puppet.SyncContact(source, true) -// content := event.Content{ -// Parsed: event.MemberEventContent{ -// Membership: "invite", -// Displayname: puppet.Displayname, -// AvatarURL: puppet.AvatarURL.CUString(), -// }, -// Raw: map[string]interface{}{ -// "net.maunium.whatsapp.puppet": true, -// }, -// } -// resp, err := intent.SendStateEvent(portal.MXID, event.StateMember, puppet.MXID.String(), &content) -// if err != nil { -// portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, intent.UserID, err) -// _ = portal.MainIntent().EnsureInvited(portal.MXID, puppet.MXID) -// } else { -// evtID = resp.EventID -// } -// err = puppet.DefaultIntent().EnsureJoined(portal.MXID) -// if err != nil { -// portal.log.Errorfln("Failed to ensure %s is joined: %v", puppet.MXID, err) -// } -// } -// return -//} +func (portal *Portal) tryKickUser(userID id.UserID, intent *appservice.IntentAPI) error { + _, err := intent.KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID}) + if err != nil { + httpErr, ok := err.(mautrix.HTTPError) + if ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_FORBIDDEN" { + _, err = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: userID}) + } + } + return err +} + +func (portal *Portal) removeUser(isSameUser bool, kicker *appservice.IntentAPI, target id.UserID, targetIntent *appservice.IntentAPI) { + if !isSameUser || targetIntent == nil { + err := portal.tryKickUser(target, kicker) + if err != nil { + portal.log.Warnfln("Failed to kick %s from %s: %v", target, portal.MXID, err) + if targetIntent != nil { + _, _ = portal.leaveWithPuppetMeta(targetIntent) + } + } + } else { + _, err := portal.leaveWithPuppetMeta(targetIntent) + if err != nil { + portal.log.Warnfln("Failed to leave portal as %s: %v", target, err) + _, _ = portal.MainIntent().KickUser(portal.MXID, &mautrix.ReqKickUser{UserID: target}) + } + } +} + +func (portal *Portal) HandleWhatsAppKick(source *User, senderJID types.JID, jids []types.JID) { + sender := portal.bridge.GetPuppetByJID(senderJID) + senderIntent := sender.IntentFor(portal) + for _, jid := range jids { + if source != nil && source.JID.User == jid.User { + portal.log.Debugln("Ignoring self-kick by", source.MXID) + continue + } + puppet := portal.bridge.GetPuppetByJID(jid) + portal.removeUser(puppet.JID == sender.JID, senderIntent, puppet.MXID, puppet.DefaultIntent()) + + if !portal.IsBroadcastList() { + user := portal.bridge.GetUserByJID(jid) + if user != nil { + var customIntent *appservice.IntentAPI + if puppet.CustomMXID == user.MXID { + customIntent = puppet.CustomIntent() + } + portal.removeUser(puppet.JID == sender.JID, senderIntent, user.MXID, customIntent) + } + } + } +} + +func (portal *Portal) leaveWithPuppetMeta(intent *appservice.IntentAPI) (*mautrix.RespSendEvent, error) { + content := event.Content{ + Parsed: event.MemberEventContent{ + Membership: event.MembershipLeave, + }, + Raw: map[string]interface{}{ + "net.maunium.whatsapp.puppet": true, + }, + } + return intent.SendStateEvent(portal.MXID, event.StateMember, intent.UserID.String(), &content) +} + +func (portal *Portal) HandleWhatsAppInvite(source *User, senderJID *types.JID, jids []types.JID) (evtID id.EventID) { + intent := portal.MainIntent() + if senderJID != nil && !senderJID.IsEmpty() { + sender := portal.bridge.GetPuppetByJID(*senderJID) + intent = sender.IntentFor(portal) + } + for _, jid := range jids { + puppet := portal.bridge.GetPuppetByJID(jid) + puppet.SyncContact(source, true) + content := event.Content{ + Parsed: event.MemberEventContent{ + Membership: "invite", + Displayname: puppet.Displayname, + AvatarURL: puppet.AvatarURL.CUString(), + }, + Raw: map[string]interface{}{ + "net.maunium.whatsapp.puppet": true, + }, + } + resp, err := intent.SendStateEvent(portal.MXID, event.StateMember, puppet.MXID.String(), &content) + if err != nil { + portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, intent.UserID, err) + _ = portal.MainIntent().EnsureInvited(portal.MXID, puppet.MXID) + } else { + evtID = resp.EventID + } + err = puppet.DefaultIntent().EnsureJoined(portal.MXID) + if err != nil { + portal.log.Errorfln("Failed to ensure %s is joined: %v", puppet.MXID, err) + } + } + return +} func (portal *Portal) makeMediaBridgeFailureMessage(intent *appservice.IntentAPI, info *types.MessageInfo, bridgeErr error, captionContent *event.MessageEventContent) *ConvertedMessage { portal.log.Errorfln("Failed to bridge media for %s: %v", info.ID, bridgeErr) diff --git a/user.go b/user.go index 6d19b46..2ac6355 100644 --- a/user.go +++ b/user.go @@ -376,13 +376,31 @@ func (user *User) handleHistorySync(evt *waProto.HistorySync) { continue } - portal := user.GetPortalByJID(jid) - err = portal.CreateMatrixRoom(user) - if err != nil { - user.log.Warnfln("Failed to create room for %s during backfill: %v", portal.Key.JID, err) - continue + muteEnd := time.Unix(int64(conv.GetMuteEndTime()), 0) + if muteEnd.After(time.Now()) { + _ = user.Client.Store.ChatSettings.PutMutedUntil(jid, muteEnd) + } + if conv.GetArchived() { + _ = user.Client.Store.ChatSettings.PutArchived(jid, true) + } + if conv.GetPinned() > 0 { + _ = user.Client.Store.ChatSettings.PutPinned(jid, true) + } + + portal := user.GetPortalByJID(jid) + if user.bridge.Config.Bridge.HistorySync.CreatePortals { + err = portal.CreateMatrixRoom(user) + if err != nil { + user.log.Warnfln("Failed to create room for %s during backfill: %v", portal.Key.JID, err) + continue + } + } + if len(portal.MXID) > 0 && user.bridge.Config.Bridge.HistorySync.Backfill { + portal.backfill(user, conv.GetMessages()) + if !conv.GetMarkedAsUnread() && conv.GetUnreadCount() == 0 { + user.markSelfReadFull(portal) + } } - portal.backfill(user, conv.GetMessages()) } } @@ -432,6 +450,10 @@ func (user *User) HandleEvent(event interface{}) { go user.syncPuppet(v.JID) case *events.PushName: go user.syncPuppet(v.JID) + case *events.GroupInfo: + go user.handleGroupUpdate(v) + case *events.Picture: + go user.handlePictureUpdate(v) case *events.Receipt: go user.handleReceipt(v) case *events.ChatPresence: @@ -540,29 +562,21 @@ type CustomReadReceipt struct { DoublePuppet bool `json:"net.maunium.whatsapp.puppet,omitempty"` } -func (user *User) syncChatDoublePuppetDetails(doublePuppet *Puppet, portal *Portal, justCreated bool) { +func (user *User) syncChatDoublePuppetDetails(portal *Portal, justCreated bool) { + doublePuppet := portal.bridge.GetPuppetByCustomMXID(user.MXID) + if doublePuppet == nil { + return + } if doublePuppet == nil || doublePuppet.CustomIntent() == nil || len(portal.MXID) == 0 { return } - intent := doublePuppet.CustomIntent() - // FIXME this might not be possible to do anymore - //if chat.UnreadCount == 0 && (justCreated || !user.bridge.Config.Bridge.MarkReadOnlyOnCreate) { - // lastMessage := user.bridge.DB.Message.GetLastInChatBefore(chat.Portal.Key, chat.ReceivedAt.Unix()) - // if lastMessage != nil { - // err := intent.MarkReadWithContent(chat.Portal.MXID, lastMessage.MXID, &CustomReadReceipt{DoublePuppet: true}) - // if err != nil { - // user.log.Warnfln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, chat.Portal.MXID, err) - // } - // } - //} else if chat.UnreadCount == -1 { - // user.log.Debugfln("Invalid unread count (missing field?) in chat info %+v", chat.Source) - //} if justCreated || !user.bridge.Config.Bridge.TagOnlyOnCreate { chat, err := user.Client.Store.ChatSettings.GetChatSettings(portal.Key.JID) if err != nil { user.log.Warnfln("Failed to get settings of %s: %v", portal.Key.JID, err) return } + intent := doublePuppet.CustomIntent() user.updateChatMute(intent, portal, chat.MutedUntil) user.updateChatTag(intent, portal, user.bridge.Config.Bridge.ArchiveTag, chat.Archived) user.updateChatTag(intent, portal, user.bridge.Config.Bridge.PinnedTag, chat.Pinned) @@ -709,12 +723,8 @@ func (user *User) markOtherRead(portal *Portal, intent *appservice.IntentAPI, me } func (user *User) markSelfRead(portal *Portal, messageID types.MessageID) { - puppet := user.bridge.GetPuppetByJID(user.JID) - if puppet == nil { - return - } - intent := puppet.CustomIntent() - if intent == nil { + puppet := user.bridge.GetPuppetByCustomMXID(user.MXID) + if puppet == nil || puppet.CustomIntent() == nil { return } var message *database.Message @@ -731,106 +741,69 @@ func (user *User) markSelfRead(portal *Portal, messageID types.MessageID) { } user.log.Debugfln("User read message %s/%s in %s/%s in WhatsApp mobile", message.JID, message.MXID, portal.Key.JID, portal.MXID) } - err := intent.MarkReadWithContent(portal.MXID, message.MXID, &CustomReadReceipt{DoublePuppet: true}) + err := puppet.CustomIntent().MarkReadWithContent(portal.MXID, message.MXID, &CustomReadReceipt{DoublePuppet: true}) if err != nil { user.log.Warnfln("Failed to bridge own read receipt in %s: %v", portal.Key.JID, err) } } -//func (user *User) HandleCommand(cmd whatsapp.JSONCommand) { -// switch cmd.Type { -// case whatsapp.CommandPicture: -// if strings.HasSuffix(cmd.JID, whatsapp.NewUserSuffix) { -// puppet := user.bridge.GetPuppetByJID(cmd.JID) -// go puppet.UpdateAvatar(user, cmd.ProfilePicInfo) -// } else if user.bridge.Config.Bridge.ChatMetaSync { -// portal := user.GetPortalByJID(cmd.JID) -// go portal.UpdateAvatar(user, cmd.ProfilePicInfo, true) -// } -// case whatsapp.CommandDisconnect: -// if cmd.Kind == "replaced" { -// user.cleanDisconnection = true -// go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server because you opened another WhatsApp Web client.\n\n" + -// "Use the `reconnect` command to disconnect the other client and resume bridging.") -// } else { -// user.log.Warnln("Unknown kind of disconnect:", string(cmd.Raw)) -// go user.sendMarkdownBridgeAlert("\u26a0 Your WhatsApp connection was closed by the server (reason code: %s).\n\n"+ -// "Use the `reconnect` command to reconnect.", cmd.Kind) -// } -// } -//} +func (user *User) markSelfReadFull(portal *Portal) { + puppet := user.bridge.GetPuppetByCustomMXID(user.MXID) + if puppet == nil || puppet.CustomIntent() == nil { + return + } + lastMessage := user.bridge.DB.Message.GetLastInChat(portal.Key) + if lastMessage == nil { + return + } + err := puppet.CustomIntent().MarkReadWithContent(portal.MXID, lastMessage.MXID, &CustomReadReceipt{DoublePuppet: true}) + if err != nil { + user.log.Warnfln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, portal.MXID, err) + } +} -//func (user *User) HandleChatUpdate(cmd whatsapp.ChatUpdate) { -// if cmd.Command != whatsapp.ChatUpdateCommandAction { -// return -// } -// -// portal := user.GetPortalByJID(cmd.JID) -// if len(portal.MXID) == 0 { -// if cmd.Data.Action == whatsapp.ChatActionIntroduce || cmd.Data.Action == whatsapp.ChatActionCreate { -// go func() { -// err := portal.CreateMatrixRoom(user) -// if err != nil { -// user.log.Errorln("Failed to create portal room after receiving join event:", err) -// } -// }() -// } -// return -// } -// -// // These don't come down the message history :( -// switch cmd.Data.Action { -// case whatsapp.ChatActionAddTopic: -// go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, nil, true) -// case whatsapp.ChatActionRemoveTopic: -// go portal.UpdateTopic("", cmd.Data.SenderJID, nil, true) -// case whatsapp.ChatActionRemove: -// // We ignore leaving groups in the message history to avoid accidentally leaving rejoined groups, -// // but if we get a real-time command that says we left, it should be safe to bridge it. -// if !user.bridge.Config.Bridge.ChatMetaSync { -// for _, jid := range cmd.Data.UserChange.JIDs { -// if jid == user.JID { -// go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs) -// break -// } -// } -// } -// } -// -// if !user.bridge.Config.Bridge.ChatMetaSync { -// // Ignore chat update commands, we're relying on the message history. -// return -// } -// -// switch cmd.Data.Action { -// case whatsapp.ChatActionNameChange: -// go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, nil, true) -// case whatsapp.ChatActionPromote: -// go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, true) -// case whatsapp.ChatActionDemote: -// go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, false) -// case whatsapp.ChatActionAnnounce: -// go portal.RestrictMessageSending(cmd.Data.Announce) -// case whatsapp.ChatActionRestrict: -// go portal.RestrictMetadataChanges(cmd.Data.Restrict) -// case whatsapp.ChatActionRemove: -// go portal.HandleWhatsAppKick(nil, cmd.Data.SenderJID, cmd.Data.UserChange.JIDs) -// case whatsapp.ChatActionAdd: -// go portal.HandleWhatsAppInvite(user, cmd.Data.SenderJID, nil, cmd.Data.UserChange.JIDs) -// case whatsapp.ChatActionIntroduce: -// if cmd.Data.SenderJID != "unknown" { -// go portal.Sync(user, whatsapp.Contact{JID: portal.Key.JID}) -// } -// } -//} -// -//func (user *User) HandleJSONMessage(evt whatsapp.RawJSONMessage) { -// if !json.Valid(evt.RawMessage) { -// return -// } -// user.log.Debugfln("JSON message with tag %s: %s", evt.Tag, evt.RawMessage) -// user.updateLastConnectionIfNecessary() -//} +func (user *User) handleGroupUpdate(evt *events.GroupInfo) { + portal := user.GetPortalByJID(evt.JID) + if portal == nil || len(portal.MXID) == 0 { + // TODO create portal when added to group + user.log.Debugfln("Ignoring group info update in chat with no portal: %+v", evt) + return + } + switch { + case evt.Announce != nil: + portal.RestrictMessageSending(evt.Announce.IsAnnounce) + case evt.Locked != nil: + portal.RestrictMetadataChanges(evt.Locked.IsLocked) + case evt.Name != nil: + portal.UpdateName(evt.Name.Name, evt.Name.NameSetBy, true) + case evt.Topic != nil: + portal.UpdateTopic(evt.Topic.Topic, evt.Topic.TopicSetBy, true) + case evt.Leave != nil: + if evt.Sender != nil && !evt.Sender.IsEmpty() { + portal.HandleWhatsAppKick(user, *evt.Sender, evt.Leave) + } + case evt.Join != nil: + portal.HandleWhatsAppInvite(user, evt.Sender, evt.Join) + case evt.Promote != nil: + portal.ChangeAdminStatus(evt.Promote, true) + case evt.Demote != nil: + portal.ChangeAdminStatus(evt.Demote, false) + } +} + +func (user *User) handlePictureUpdate(evt *events.Picture) { + if evt.JID.Server == types.DefaultUserServer { + puppet := user.bridge.GetPuppetByJID(evt.JID) + if puppet.Avatar != evt.PictureID { + puppet.UpdateAvatar(user) + } + } else { + portal := user.GetPortalByJID(evt.JID) + if portal != nil && portal.Avatar != evt.PictureID { + portal.UpdateAvatar(user, evt.Author, true) + } + } +} func (user *User) NeedsRelaybot(portal *Portal) bool { return !user.HasSession() // || !user.IsInPortal(portal.Key)