diff --git a/config/bridge.go b/config/bridge.go index 6ad096d..e73c9c8 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -58,6 +58,8 @@ type BridgeConfig struct { 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"` SyncChatMaxAge uint64 `yaml:"sync_max_chat_age"` SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` @@ -116,6 +118,8 @@ func (bc *BridgeConfig) setDefaults() { bc.InitialHistoryFill = 20 bc.RecoverChatSync = -1 bc.RecoverHistory = true + bc.ChatMetaSync = true + bc.UserAvatarSync = true bc.SyncChatMaxAge = 259200 bc.SyncWithCustomPuppets = true diff --git a/database/message.go b/database/message.go index b6bd5a4..cc776f0 100644 --- a/database/message.go +++ b/database/message.go @@ -20,6 +20,7 @@ import ( "bytes" "database/sql" "encoding/json" + "strings" waProto "github.com/Rhymen/go-whatsapp/binary/proto" @@ -93,6 +94,10 @@ type Message struct { Content *waProto.Message } +func (msg *Message) IsFakeMXID() bool { + return strings.HasPrefix(msg.MXID.String(), "net.maunium.whatsapp.fake::") +} + func (msg *Message) Scan(row Scannable) *Message { var content []byte err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &msg.Timestamp, &content) diff --git a/example-config.yaml b/example-config.yaml index f83b2dc..0ad3cf5 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -134,6 +134,13 @@ bridge: 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 + # 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 # 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. diff --git a/go.mod b/go.mod index 19b9147..61c244d 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mauflag v1.0.0 maunium.net/go/maulogger/v2 v2.1.1 - maunium.net/go/mautrix v0.8.0 + maunium.net/go/mautrix v0.8.2 ) -replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.19 +replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.20 diff --git a/go.sum b/go.sum index 11d7332..dd93f1c 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/tulir/go-whatsapp v0.3.18 h1:45pkdjEnAp6yV4RTSWCZn2Eenep+MN7Kndf/rRXQ github.com/tulir/go-whatsapp v0.3.18/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o= github.com/tulir/go-whatsapp v0.3.19 h1:76VtmcjKGX8MbfJN9NNi1f0IVmigTLUcxqE1VRcovcQ= github.com/tulir/go-whatsapp v0.3.19/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o= +github.com/tulir/go-whatsapp v0.3.20 h1:nK92MgruqXwk+QlaAS39xhzHNbFvJIEgUIOUrN3i8Yc= +github.com/tulir/go-whatsapp v0.3.20/go.mod h1:U5+sm33vrv3wz62YyRM/VS7q2ObXkxU4Xqj/3KOmN9o= 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= @@ -234,3 +236,7 @@ maunium.net/go/mautrix v0.8.0-rc.4 h1:3JXoL2JJPE5nh/YSw9sv9dQA9ulma9yHTMOBMBY1xd maunium.net/go/mautrix v0.8.0-rc.4/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= maunium.net/go/mautrix v0.8.0 h1:G1jlVslNUTWEqaxuatHAMmzTWnGyoCIc4tAF5GpQJd8= maunium.net/go/mautrix v0.8.0/go.mod h1:TtVePxoEaw6+RZDKVajw66Yaj1lqLjH8l4FF3krsqWY= +maunium.net/go/mautrix v0.8.1 h1:YvJGy7euB+x6Mz74jJ+G4NiyrLiX9pzmXpnQB9vFONg= +maunium.net/go/mautrix v0.8.1/go.mod h1:KiViCshKBUZwrVRvTOXsJBFfstvR/btxckHUbOPdu54= +maunium.net/go/mautrix v0.8.2 h1:E3NudQ/QolmE/yhHau8iCkbmcq6gCLvoEvukdqPFJu4= +maunium.net/go/mautrix v0.8.2/go.mod h1:KiViCshKBUZwrVRvTOXsJBFfstvR/btxckHUbOPdu54= diff --git a/matrix.go b/matrix.go index 33b245b..d0ba4fb 100644 --- a/matrix.go +++ b/matrix.go @@ -289,6 +289,10 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) { defer mx.bridge.Metrics.TrackEvent(evt.Type)() + if mx.shouldIgnoreEvent(evt) { + return + } + user := mx.bridge.GetUserByMXID(evt.Sender) if user == nil || !user.Whitelisted || !user.IsConnected() { return @@ -299,22 +303,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) { return } - var resp <-chan string - var err error - switch content := evt.Content.Parsed.(type) { - case *event.RoomNameEventContent: - resp, err = user.Conn.UpdateGroupSubject(content.Name, portal.Key.JID) - case *event.TopicEventContent: - resp, err = user.Conn.UpdateGroupDescription(portal.Key.JID, content.Topic) - case *event.RoomAvatarEventContent: - return - } - if err != nil { - mx.log.Errorln(err) - } else { - out := <-resp - mx.log.Infoln(out) - } + portal.HandleMatrixMeta(user, evt) } func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool { diff --git a/portal.go b/portal.go index e0369e8..510e954 100644 --- a/portal.go +++ b/portal.go @@ -204,12 +204,12 @@ func (portal *Portal) handleMessageLoop() { } } portal.backfillLock.Lock() - portal.handleMessage(msg) + portal.handleMessage(msg, false) portal.backfillLock.Unlock() } } -func (portal *Portal) handleMessage(msg PortalMessage) { +func (portal *Portal) handleMessage(msg PortalMessage, isBackfill bool) { if len(portal.MXID) == 0 { portal.log.Warnln("handleMessage called even though portal.MXID is empty") return @@ -254,6 +254,8 @@ func (portal *Portal) handleMessage(msg PortalMessage) { portal.HandleContactMessage(msg.source, data) case whatsapp.LocationMessage: portal.HandleLocationMessage(msg.source, data) + case whatsapp.StubMessage: + portal.HandleStubMessage(msg.source, data, isBackfill) case whatsappExt.MessageRevocation: portal.HandleMessageRevoke(msg.source, data) case FakeMessage: @@ -421,30 +423,35 @@ func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInf } } - if avatar.Status != 0 { + if avatar.Status == 404 { + avatar.Tag = "remove" + avatar.Status = 0 + } + if avatar.Status != 0 || portal.Avatar == avatar.Tag { return false } - if portal.Avatar == avatar.Tag { - return false + if avatar.Tag == "remove" { + portal.AvatarURL = id.ContentURI{} + } else { + data, err := avatar.DownloadBytes() + if err != nil { + portal.log.Warnln("Failed to download avatar:", err) + return false + } + + mimeType := http.DetectContentType(data) + resp, err := portal.MainIntent().UploadBytes(data, mimeType) + if err != nil { + portal.log.Warnln("Failed to upload avatar:", err) + return false + } + + portal.AvatarURL = resp.ContentURI } - data, err := avatar.DownloadBytes() - if err != nil { - portal.log.Warnln("Failed to download avatar:", err) - return false - } - - mimeType := http.DetectContentType(data) - resp, err := portal.MainIntent().UploadBytes(data, mimeType) - if err != nil { - portal.log.Warnln("Failed to upload avatar:", err) - return false - } - - portal.AvatarURL = resp.ContentURI if len(portal.MXID) > 0 { - _, err = portal.MainIntent().SetRoomAvatar(portal.MXID, resp.ContentURI) + _, err := portal.MainIntent().SetRoomAvatar(portal.MXID, portal.AvatarURL) if err != nil { portal.log.Warnln("Failed to set room topic:", err) return false @@ -457,40 +464,50 @@ func (portal *Portal) UpdateAvatar(user *User, avatar *whatsappExt.ProfilePicInf return true } -func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID, updateInfo bool) bool { +func (portal *Portal) UpdateName(name string, setBy types.WhatsAppID, intent *appservice.IntentAPI, updateInfo bool) bool { if portal.Name != name { - intent := portal.MainIntent() - if len(setBy) > 0 { - intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) + portal.log.Debugfln("Updating name %s -> %s", portal.Name, name) + portal.Name = name + if intent == nil { + intent = portal.MainIntent() + if len(setBy) > 0 { + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) + } } _, err := intent.SetRoomName(portal.MXID, name) if err == nil { - portal.Name = name if updateInfo { portal.UpdateBridgeInfo() } return true + } else { + portal.Name = "" + portal.log.Warnln("Failed to set room name:", err) } - portal.log.Warnln("Failed to set room name:", err) } return false } -func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID, updateInfo bool) bool { +func (portal *Portal) UpdateTopic(topic string, setBy types.WhatsAppID, intent *appservice.IntentAPI, updateInfo bool) bool { if portal.Topic != topic { - intent := portal.MainIntent() - if len(setBy) > 0 { - intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) + portal.log.Debugfln("Updating topic %s -> %s", portal.Topic, topic) + portal.Topic = topic + if intent == nil { + intent = portal.MainIntent() + if len(setBy) > 0 { + intent = portal.bridge.GetPuppetByJID(setBy).IntentFor(portal) + } } _, err := intent.SetRoomTopic(portal.MXID, topic) if err == nil { - portal.Topic = topic if updateInfo { portal.UpdateBridgeInfo() } return true + } else { + portal.Topic = "" + portal.log.Warnln("Failed to set room topic:", err) } - portal.log.Warnln("Failed to set room topic:", err) } return false } @@ -500,8 +517,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool { return false } else if portal.IsStatusBroadcastRoom() { update := false - update = portal.UpdateName("WhatsApp Status Broadcast", "", false) || update - update = portal.UpdateTopic("WhatsApp status updates from your contacts", "", false) || update + update = portal.UpdateName("WhatsApp Status Broadcast", "", nil, false) || update + update = portal.UpdateTopic("WhatsApp status updates from your contacts", "", nil, false) || update return update } metadata, err := user.Conn.GetGroupMetaData(portal.Key.JID) @@ -521,8 +538,8 @@ func (portal *Portal) UpdateMetadata(user *User) bool { portal.SyncParticipants(metadata) update := false - update = portal.UpdateName(metadata.Name, metadata.NameSetBy, false) || update - update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, false) || update + update = portal.UpdateName(metadata.Name, metadata.NameSetBy, nil, false) || update + update = portal.UpdateTopic(metadata.Topic, metadata.TopicSetBy, nil, false) || update portal.RestrictMessageSending(metadata.Announce) @@ -586,7 +603,7 @@ func (portal *Portal) Sync(user *User, contact whatsapp.Contact) { update := false update = portal.UpdateMetadata(user) || update - if !portal.IsStatusBroadcastRoom() { + if !portal.IsStatusBroadcastRoom() && portal.Avatar == "" { update = portal.UpdateAvatar(user, nil, false) || update } if update { @@ -620,7 +637,7 @@ func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent { } } -func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) { +func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) id.EventID { levels, err := portal.MainIntent().PowerLevels(portal.MXID) if err != nil { levels = portal.GetBasePowerLevels() @@ -640,14 +657,17 @@ func (portal *Portal) ChangeAdminStatus(jids []string, setAdmin bool) { } } if changed { - _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + 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) { +func (portal *Portal) RestrictMessageSending(restrict bool) id.EventID { levels, err := portal.MainIntent().PowerLevels(portal.MXID) if err != nil { levels = portal.GetBasePowerLevels() @@ -659,17 +679,20 @@ func (portal *Portal) RestrictMessageSending(restrict bool) { } if levels.EventsDefault == newLevel { - return + return "" } levels.EventsDefault = newLevel - _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + resp, err := portal.MainIntent().SetPowerLevels(portal.MXID, levels) if err != nil { portal.log.Errorln("Failed to change power levels:", err) + return "" + } else { + return resp.EventID } } -func (portal *Portal) RestrictMetadataChanges(restrict bool) { +func (portal *Portal) RestrictMetadataChanges(restrict bool) id.EventID { levels, err := portal.MainIntent().PowerLevels(portal.MXID) if err != nil { levels = portal.GetBasePowerLevels() @@ -683,11 +706,14 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) { changed = levels.EnsureEventLevel(event.StateRoomAvatar, newLevel) || changed changed = levels.EnsureEventLevel(event.StateTopic, newLevel) || changed if changed { - _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) + 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) BackfillHistory(user *User, lastMessageTime uint64) error { @@ -873,7 +899,7 @@ func (portal *Portal) handleHistory(user *User, messages []interface{}) { if portal.privateChatBackfillInvitePuppet != nil && message.GetKey().GetFromMe() && portal.IsPrivateChat() { portal.privateChatBackfillInvitePuppet() } - portal.handleMessage(PortalMessage{portal.Key.JID, user, data, message.GetMessageTimestamp()}) + portal.handleMessage(PortalMessage{portal.Key.JID, user, data, message.GetMessageTimestamp()}, true) } } @@ -1105,7 +1131,7 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp return } message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID) - if message != nil { + if message != nil && !message.IsFakeMXID() { evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID) if err != nil { portal.log.Warnln("Failed to get reply target:", err) @@ -1128,7 +1154,7 @@ func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp func (portal *Portal) HandleMessageRevoke(user *User, message whatsappExt.MessageRevocation) { msg := portal.bridge.DB.Message.GetByJID(portal.Key, message.Id) - if msg == nil { + if msg == nil || msg.IsFakeMXID() { return } var intent *appservice.IntentAPI @@ -1198,7 +1224,7 @@ func isGatewayError(err error) bool { } func (portal *Portal) sendMessageWithRetry(intent *appservice.IntentAPI, eventType event.Type, content interface{}, timestamp int64, retries int) (*mautrix.RespSendEvent, error) { - for ;;retries-- { + for ; ; retries-- { resp, err := portal.sendMessageDirect(intent, eventType, content, timestamp) if retries > 0 && isGatewayError(err) { portal.log.Warnfln("Got gateway error trying to send message, retrying in %d seconds", int(BadGatewaySleep.Seconds())) @@ -1254,6 +1280,55 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa portal.finishHandling(source, message.Info.Source, resp.EventID) } +func (portal *Portal) HandleStubMessage(source *User, message whatsapp.StubMessage, isBackfill bool) { + if portal.bridge.Config.Bridge.ChatMetaSync { + // Chat meta sync is enabled, so we use chat update commands and full-syncs instead of message history + return + } + intent := portal.startHandling(source, message.Info) + if intent == nil { + return + } + var senderJID string + if message.Info.FromMe { + senderJID = source.JID + } else { + senderJID = message.Info.SenderJid + } + var eventID id.EventID + // TODO find more real event IDs + // TODO timestamp massaging + switch message.Type { + case waProto.WebMessageInfo_GROUP_CHANGE_SUBJECT: + portal.UpdateName(message.FirstParam, "", intent, true) + case waProto.WebMessageInfo_GROUP_CHANGE_ICON: + portal.UpdateAvatar(source, nil, true) + case waProto.WebMessageInfo_GROUP_CHANGE_DESCRIPTION: + if isBackfill { + // TODO fetch topic from server + } + //portal.UpdateTopic(message.FirstParam, "", intent, true) + case waProto.WebMessageInfo_GROUP_CHANGE_ANNOUNCE: + eventID = portal.RestrictMessageSending(message.FirstParam == "on") + case waProto.WebMessageInfo_GROUP_CHANGE_RESTRICT: + eventID = portal.RestrictMetadataChanges(message.FirstParam == "on") + case waProto.WebMessageInfo_GROUP_PARTICIPANT_ADD, waProto.WebMessageInfo_GROUP_PARTICIPANT_INVITE: + portal.HandleWhatsAppInvite(senderJID, intent, message.Params) + case waProto.WebMessageInfo_GROUP_PARTICIPANT_REMOVE, waProto.WebMessageInfo_GROUP_PARTICIPANT_LEAVE: + portal.HandleWhatsAppKick(senderJID, message.Params) + case waProto.WebMessageInfo_GROUP_PARTICIPANT_PROMOTE: + eventID = portal.ChangeAdminStatus(message.Params, true) + case waProto.WebMessageInfo_GROUP_PARTICIPANT_DEMOTE: + eventID = portal.ChangeAdminStatus(message.Params, false) + default: + return + } + if len(eventID) == 0 { + eventID = id.EventID(fmt.Sprintf("net.maunium.whatsapp.fake::%s", message.Info.Id)) + } + portal.markHandled(source, message.Info.Source, eventID) +} + func (portal *Portal) HandleLocationMessage(source *User, message whatsapp.LocationMessage) { intent := portal.startHandling(source, message.Info) if intent == nil { @@ -1428,17 +1503,19 @@ func (portal *Portal) HandleWhatsAppKick(senderJID string, jids []string) { } } -func (portal *Portal) HandleWhatsAppInvite(senderJID string, jids []string) { - senderIntent := portal.MainIntent() - if senderJID != "unknown" { - sender := portal.bridge.GetPuppetByJID(senderJID) - senderIntent = sender.IntentFor(portal) +func (portal *Portal) HandleWhatsAppInvite(senderJID string, intent *appservice.IntentAPI, jids []string) { + 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) - _, err := senderIntent.InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: puppet.MXID}) + _, err := intent.InviteUser(portal.MXID, &mautrix.ReqInviteUser{UserID: puppet.MXID}) if err != nil { - portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, senderIntent.UserID, err) + portal.log.Warnfln("Failed to invite %s as %s: %v", puppet.MXID, intent.UserID, err) } err = puppet.DefaultIntent().EnsureJoined(portal.MXID) if err != nil { @@ -1465,7 +1542,7 @@ type mediaMessage struct { } func (portal *Portal) uploadWithRetry(intent *appservice.IntentAPI, data []byte, mimeType string, retries int) (*mautrix.RespMediaUpload, error) { - for ;;retries-- { + for ; ; retries-- { uploaded, err := intent.UploadBytes(data, mimeType) if isGatewayError(err) { portal.log.Warnfln("Got gateway error trying to upload media, retrying in %d seconds", int(BadGatewaySleep.Seconds())) @@ -1998,8 +2075,6 @@ func (portal *Portal) sendDeliveryReceipt(eventID id.EventID) { } } -var timeout = errors.New("message sending timed out") - func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { if !portal.HasRelaybot() && ( (portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) || @@ -2013,10 +2088,10 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { } portal.markHandled(sender, info, evt.ID) portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.Key.GetId()) - portal.sendRaw(sender, evt, info, false) + portal.sendRaw(sender, evt, info) } -func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebMessageInfo, isRetry bool) { +func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebMessageInfo) { errChan := make(chan error, 1) go sender.Conn.SendRaw(info, errChan) @@ -2024,20 +2099,13 @@ func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebM var errorEventID id.EventID select { case err = <-errChan: - var statusResp whatsapp.StatusResponse - if !isRetry && errors.As(err, &statusResp) && statusResp.Status == 599 { - portal.log.Debugfln("599 status response sending %s to WhatsApp (%+v), retrying...", evt.ID, statusResp) - errorEventID = portal.sendErrorMessage(fmt.Sprintf("%v. The bridge will retry in 5 seconds.", err)) - time.Sleep(5 * time.Second) - portal.sendRaw(sender, evt, info, true) - } case <-time.After(time.Duration(portal.bridge.Config.Bridge.ConnectionTimeout) * time.Second): if portal.bridge.Config.Bridge.FetchMessageOnTimeout && portal.wasMessageSent(sender, info.Key.GetId()) { portal.log.Debugln("Matrix event %s was bridged, but response didn't arrive within timeout") portal.sendDeliveryReceipt(evt.ID) } else { portal.log.Warnfln("Response when bridging Matrix event %s is taking long to arrive", evt.ID) - errorEventID = portal.sendErrorMessage(timeout.Error()) + errorEventID = portal.sendErrorMessage("message sending timed out") } err = <-errChan } @@ -2045,9 +2113,11 @@ func (portal *Portal) sendRaw(sender *User, evt *event.Event, info *waProto.WebM portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) var statusResp whatsapp.StatusResponse if errors.As(err, &statusResp) && statusResp.Status == 599 { - portal.log.Debugfln("599 status response data: %+v", statusResp) + portal.log.Debugfln("599 status response extra data: %+v", statusResp.Extra) + portal.sendErrorMessage(fmt.Sprintf("%v. Please try again after a few minutes", err)) + } else { + portal.sendErrorMessage(err.Error()) } - portal.sendErrorMessage(err.Error()) } else { portal.log.Debugfln("Handled Matrix event %s", evt.ID) portal.sendDeliveryReceipt(evt.ID) @@ -2231,3 +2301,30 @@ func (portal *Portal) HandleMatrixInvite(sender *User, evt *event.Event) { portal.log.Infoln("Add %s response: %s", puppet.JID, <-resp) } } + +func (portal *Portal) HandleMatrixMeta(sender *User, evt *event.Event) { + var resp <-chan string + var err error + switch content := evt.Content.Parsed.(type) { + case *event.RoomNameEventContent: + if content.Name == portal.Name { + return + } + portal.Name = content.Name + resp, err = sender.Conn.UpdateGroupSubject(content.Name, portal.Key.JID) + case *event.TopicEventContent: + if content.Topic == portal.Topic { + return + } + portal.Topic = content.Topic + resp, err = sender.Conn.UpdateGroupDescription(portal.Key.JID, content.Topic) + case *event.RoomAvatarEventContent: + return + } + if err != nil { + portal.log.Errorln("Failed to update metadata:", err) + } else { + out := <-resp + portal.log.Debugln("Successfully updated metadata:", out) + } +} diff --git a/puppet.go b/puppet.go index 0c3d82c..03f3ac8 100644 --- a/puppet.go +++ b/puppet.go @@ -22,9 +22,10 @@ import ( "regexp" "strings" - "github.com/Rhymen/go-whatsapp" log "maunium.net/go/maulogger/v2" + "github.com/Rhymen/go-whatsapp" + "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/id" @@ -190,15 +191,18 @@ func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicI } } - if avatar.Status != 0 { + if avatar.Status == 404 { + avatar.Tag = "remove" + avatar.Status = 0 + } else if avatar.Status == 401 && puppet.Avatar != "unauthorized" { + puppet.Avatar = "unauthorized" + return true + } + if avatar.Status != 0 || avatar.Tag == puppet.Avatar { return false } - if avatar.Tag == puppet.Avatar { - return false - } - - if len(avatar.URL) == 0 { + if avatar.Tag == "remove" || len(avatar.URL) == 0 { err := puppet.DefaultIntent().SetAvatarURL(id.ContentURI{}) if err != nil { puppet.log.Warnln("Failed to remove avatar:", err) @@ -296,7 +300,10 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) { update := false update = puppet.UpdateName(source, contact) || update - update = puppet.UpdateAvatar(source, nil) || update + // TODO figure out how to update avatars after being offline + if len(puppet.Avatar) == 0 || puppet.bridge.Config.Bridge.UserAvatarSync { + update = puppet.UpdateAvatar(source, nil) || update + } if update { puppet.Update() } diff --git a/user.go b/user.go index 3c27dfa..ec9b016 100644 --- a/user.go +++ b/user.go @@ -641,8 +641,11 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) } create := (chat.LastMessageTime >= user.LastConnection && user.LastConnection > 0) || i < limit if len(chat.Portal.MXID) > 0 || create || createAll { - chat.Portal.Sync(user, chat.Contact) - err := chat.Portal.BackfillHistory(user, chat.LastMessageTime) + // Don't sync unless chat meta sync is enabled or portal doesn't exist + if user.bridge.Config.Bridge.ChatMetaSync || len(chat.Portal.MXID) == 0 { + chat.Portal.Sync(user, chat.Contact) + } + err = chat.Portal.BackfillHistory(user, chat.LastMessageTime) if err != nil { chat.Portal.log.Errorln("Error backfilling history:", err) } @@ -927,6 +930,10 @@ func (user *User) HandleContactMessage(message whatsapp.ContactMessage) { user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} } +func (user *User) HandleStubMessage(message whatsapp.StubMessage) { + user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} +} + func (user *User) HandleLocationMessage(message whatsapp.LocationMessage) { user.messageInput <- PortalMessage{message.Info.RemoteJid, user, message, message.Info.Timestamp} } @@ -1015,7 +1022,7 @@ func (user *User) HandleMsgInfo(info whatsappExt.MsgInfo) { intent := user.bridge.GetPuppetByJID(info.SenderJID).IntentFor(portal) for _, msgID := range info.IDs { msg := user.bridge.DB.Message.GetByJID(portal.Key, msgID) - if msg == nil { + if msg == nil || msg.IsFakeMXID() { continue } @@ -1066,7 +1073,7 @@ func (user *User) markSelfRead(jid, messageID string) { user.log.Debugfln("User read chat %s/%s in WhatsApp mobile (last known event: %s/%s)", portal.Key.JID, portal.MXID, message.JID, message.MXID) } else { message = user.bridge.DB.Message.GetByJID(portal.Key, messageID) - if message == nil { + if message == nil || message.IsFakeMXID() { return } user.log.Debugfln("User read message %s/%s in %s/%s in WhatsApp mobile", message.JID, message.MXID, portal.Key.JID, portal.MXID) @@ -1118,13 +1125,22 @@ func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) { return } + // These don't come down the message history :( + switch cmd.Data.Action { + case whatsappExt.ChatActionAddTopic: + go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, nil,true) + case whatsappExt.ChatActionRemoveTopic: + go portal.UpdateTopic("", cmd.Data.SenderJID, nil,true) + } + + if !user.bridge.Config.Bridge.ChatMetaSync { + // Ignore chat update commands, we're relying on the message history. + return + } + switch cmd.Data.Action { case whatsappExt.ChatActionNameChange: - go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, true) - case whatsappExt.ChatActionAddTopic: - go portal.UpdateTopic(cmd.Data.AddTopic.Topic, cmd.Data.SenderJID, true) - case whatsappExt.ChatActionRemoveTopic: - go portal.UpdateTopic("", cmd.Data.SenderJID, true) + go portal.UpdateName(cmd.Data.NameChange.Name, cmd.Data.SenderJID, nil, true) case whatsappExt.ChatActionPromote: go portal.ChangeAdminStatus(cmd.Data.UserChange.JIDs, true) case whatsappExt.ChatActionDemote: @@ -1136,7 +1152,7 @@ func (user *User) HandleChatUpdate(cmd whatsappExt.ChatUpdate) { case whatsappExt.ChatActionRemove: go portal.HandleWhatsAppKick(cmd.Data.SenderJID, cmd.Data.UserChange.JIDs) case whatsappExt.ChatActionAdd: - go portal.HandleWhatsAppInvite(cmd.Data.SenderJID, cmd.Data.UserChange.JIDs) + go portal.HandleWhatsAppInvite(cmd.Data.SenderJID, nil, cmd.Data.UserChange.JIDs) case whatsappExt.ChatActionIntroduce: if cmd.Data.SenderJID != "unknown" { go portal.Sync(user, whatsapp.Contact{Jid: portal.Key.JID})