From badea9c547ebeb9106e2203499cdda60aec18ed3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 19 Apr 2021 22:14:32 +0300 Subject: [PATCH] Add option to bridge archive and mute status from WhatsApp --- config/bridge.go | 28 +++++---- database/message.go | 2 +- database/user.go | 4 +- example-config.yaml | 6 ++ go.mod | 2 +- go.sum | 2 + portal.go | 4 +- user.go | 146 +++++++++++++++++++++++++++++++++----------- 8 files changed, 139 insertions(+), 55 deletions(-) diff --git a/config/bridge.go b/config/bridge.go index e30c62e..1278c43 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -51,15 +51,15 @@ 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 uint64 `yaml:"sync_max_chat_age"` + 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"` SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` SyncDirectChatList bool `yaml:"sync_direct_chat_list"` @@ -67,10 +67,12 @@ type BridgeConfig struct { DefaultBridgePresence bool `yaml:"default_bridge_presence"` LoginSharedSecret string `yaml:"login_shared_secret"` - InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"` - PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` - BridgeNotices bool `yaml:"bridge_notices"` - ResendBridgeInfo bool `yaml:"resend_bridge_info"` + InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"` + PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` + BridgeNotices bool `yaml:"bridge_notices"` + ResendBridgeInfo bool `yaml:"resend_bridge_info"` + MuteBridging bool `yaml:"mute_bridging"` + ArchiveTag string `yaml:"archive_tag"` WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"` diff --git a/database/message.go b/database/message.go index 1b6bbbf..847f946 100644 --- a/database/message.go +++ b/database/message.go @@ -91,7 +91,7 @@ type Message struct { JID whatsapp.MessageID MXID id.EventID Sender whatsapp.JID - Timestamp uint64 + Timestamp int64 Sent bool Content *waProto.Message } diff --git a/database/user.go b/database/user.go index a92f5eb..1b0d9ce 100644 --- a/database/user.go +++ b/database/user.go @@ -77,7 +77,7 @@ type User struct { JID whatsapp.JID ManagementRoom id.RoomID Session *whatsapp.Session - LastConnection uint64 + LastConnection int64 } func (user *User) Scan(row Scannable) *User { @@ -146,7 +146,7 @@ func (user *User) Insert() { } func (user *User) UpdateLastConnection() { - user.LastConnection = uint64(time.Now().Unix()) + user.LastConnection = time.Now().Unix() _, err := user.db.Exec(`UPDATE "user" SET last_connection=$1 WHERE mxid=$2`, user.LastConnection, user.MXID) if err != nil { diff --git a/example-config.yaml b/example-config.yaml index 6787e09..71a7564 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -183,6 +183,12 @@ bridge: # 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 + # When using double puppeting, should archived chats be moved to a specific tag in Matrix? + # Note that WhatsApp unarchives chats when a message is received, which will also be mirrored to Matrix. + # This can be set to a tag (e.g. m.lowpriority), or null to disable. + archive_tag: null # Whether or not thumbnails from WhatsApp should be sent. # They're disabled by default due to very low resolution. diff --git a/go.mod b/go.mod index 11a9c37..9996bf6 100644 --- a/go.mod +++ b/go.mod @@ -16,4 +16,4 @@ require ( maunium.net/go/mautrix v0.9.7 ) -replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.4.3 +replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.4.4 diff --git a/go.sum b/go.sum index ce33918..c4ebb5a 100644 --- a/go.sum +++ b/go.sum @@ -323,6 +323,8 @@ github.com/tulir/go-whatsapp v0.4.2 h1:UzBidzRazkbFhM7xyDBLvv4eD37zEtOsVLWK0m2CI github.com/tulir/go-whatsapp v0.4.2/go.mod h1:rwwuTh1bKqhgrRvOBAr8hDqtuz8Cc1Quqw/0BeXb+/E= github.com/tulir/go-whatsapp v0.4.3 h1:rQBBT40JHE4eLk5idQ3r/6jNj46nqjLyMnlJTKwyHl0= github.com/tulir/go-whatsapp v0.4.3/go.mod h1:rwwuTh1bKqhgrRvOBAr8hDqtuz8Cc1Quqw/0BeXb+/E= +github.com/tulir/go-whatsapp v0.4.4 h1:69AIE/CbmVYpBbug75meWFOS8lilzoafZFctt2JzRek= +github.com/tulir/go-whatsapp v0.4.4/go.mod h1:rwwuTh1bKqhgrRvOBAr8hDqtuz8Cc1Quqw/0BeXb+/E= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= diff --git a/portal.go b/portal.go index 8855dad..838676a 100644 --- a/portal.go +++ b/portal.go @@ -308,7 +308,7 @@ func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, msg.Chat = portal.Key msg.JID = message.GetKey().GetId() msg.MXID = mxid - msg.Timestamp = message.GetMessageTimestamp() + msg.Timestamp = int64(message.GetMessageTimestamp()) if message.GetKey().GetFromMe() { msg.Sender = source.JID } else if portal.IsPrivateChat() { @@ -765,7 +765,7 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) id.EventID { return "" } -func (portal *Portal) BackfillHistory(user *User, lastMessageTime uint64) error { +func (portal *Portal) BackfillHistory(user *User, lastMessageTime int64) error { if !portal.bridge.Config.Bridge.RecoverHistory { return nil } diff --git a/user.go b/user.go index f724d48..f05a19c 100644 --- a/user.go +++ b/user.go @@ -23,7 +23,6 @@ import ( "fmt" "net/http" "sort" - "strconv" "strings" "sync" "sync/atomic" @@ -31,6 +30,8 @@ import ( "github.com/skip2/go-qrcode" log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/appservice" + "maunium.net/go/mautrix/pushrules" "github.com/Rhymen/go-whatsapp" waBinary "github.com/Rhymen/go-whatsapp/binary" @@ -440,10 +441,9 @@ func (user *User) Login(ce *CommandEvent) { } type Chat struct { - Portal *Portal - LastMessageTime uint64 - MarkAsRead bool - Contact whatsapp.Contact + whatsapp.Chat + Portal *Portal + Contact whatsapp.Contact } type ChatList []Chat @@ -618,6 +618,16 @@ func (user *User) HandleEvent(event interface{}) { user.HandleChatUpdate(v) case whatsapp.ConnInfo: user.HandleConnInfo(v) + case whatsapp.MuteMessage: + portal := user.bridge.GetPortalByJID(user.PortalKey(v.JID)) + if portal != nil { + go user.updateChatMute(nil, portal, v.MutedUntil) + } + case whatsapp.ArchiveMessage: + portal := user.bridge.GetPortalByJID(user.PortalKey(v.JID)) + if portal != nil { + go user.updateChatArchive(nil, portal, v.IsArchived) + } case json.RawMessage: user.HandleJSONMessage(v) case *waProto.WebMessageInfo: @@ -664,9 +674,84 @@ func (user *User) HandleChatList(chats []whatsapp.Chat) { go user.syncPortals(chatMap, false) } -func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) { - // TODO use contexts instead of checking if user.Conn is the same? - connAtStart := user.Conn +func (user *User) updateChatMute(intent *appservice.IntentAPI, portal *Portal, mutedUntil int64) { + if len(portal.MXID) == 0 || !user.bridge.Config.Bridge.MuteBridging { + return + } else if intent == nil { + doublePuppet := user.bridge.GetPuppetByCustomMXID(user.MXID) + if doublePuppet == nil || doublePuppet.CustomIntent() == nil { + return + } + intent = doublePuppet.CustomIntent() + } + var err error + if mutedUntil < time.Now().Unix() { + user.log.Debugln("Unmuting", portal.MXID) + err = intent.DeletePushRule("global", pushrules.RoomRule, string(portal.MXID)) + } else { + user.log.Debugln("Muting", portal.MXID) + err = intent.PutPushRule("global", pushrules.RoomRule, string(portal.MXID), &mautrix.ReqPutPushRule{ + Actions: []pushrules.PushActionType{pushrules.ActionDontNotify}, + }) + } + if err != nil && !errors.Is(err, mautrix.MNotFound) { + user.log.Warnfln("Failed to update push rule for %s through double puppet: %v", portal.MXID, err) + } +} + +func (user *User) updateChatArchive(intent *appservice.IntentAPI, portal *Portal, archived bool) { + if len(portal.MXID) == 0 || len(user.bridge.Config.Bridge.ArchiveTag) == 0 { + return + } else if intent == nil { + doublePuppet := user.bridge.GetPuppetByCustomMXID(user.MXID) + if doublePuppet == nil || doublePuppet.CustomIntent() == nil { + return + } + intent = doublePuppet.CustomIntent() + } + var err error + if archived { + user.log.Debugln("Adding tag", user.bridge.Config.Bridge.ArchiveTag, "to", portal.MXID) + err = intent.AddTag(portal.MXID, user.bridge.Config.Bridge.ArchiveTag, 0.5) + } else { + user.log.Debugln("Removing tag", user.bridge.Config.Bridge.ArchiveTag, "from", portal.MXID) + err = intent.RemoveTag(portal.MXID, user.bridge.Config.Bridge.ArchiveTag) + } + if err != nil { + user.log.Warnfln("Failed to update tag for %s through double puppet: %v", portal.MXID, err) + } +} + +func (user *User) syncChatDoublePuppetDetails(doublePuppet *Puppet, chat Chat) { + if doublePuppet == nil || doublePuppet.CustomIntent() == nil { + return + } + intent := doublePuppet.CustomIntent() + if chat.UnreadCount == 0 { + lastMessage := user.bridge.DB.Message.GetLastInChat(chat.Portal.Key) + if lastMessage != nil { + err := intent.MarkRead(chat.Portal.MXID, lastMessage.MXID) + if err != nil { + user.log.Warnln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, chat.Portal.MXID, err) + } + } + } + user.updateChatMute(intent, chat.Portal, chat.MutedUntil) + user.updateChatArchive(intent, chat.Portal, chat.IsArchived) +} + +func (user *User) syncPortal(chat Chat) { + // 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) + } +} + +func (user *User) collectChatList(chatMap map[string]whatsapp.Chat) ChatList { if chatMap == nil { chatMap = user.Conn.Store.Chats } @@ -675,18 +760,12 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) existingKeys := user.GetInCommunityMap() portalKeys := make([]database.PortalKeyWithMeta, 0, len(chatMap)) for _, chat := range chatMap { - ts, err := strconv.ParseUint(chat.LastMessageTime, 10, 64) - if err != nil { - user.log.Warnfln("Non-integer last message time in %s: %s", chat.JID, chat.LastMessageTime) - continue - } portal := user.GetPortalByJID(chat.JID) chats = append(chats, Chat{ - Portal: portal, - Contact: user.Conn.Store.Contacts[chat.JID], - LastMessageTime: ts, - MarkAsRead: chat.Unread == "0", + Chat: chat, + Portal: portal, + Contact: user.Conn.Store.Contacts[chat.JID], }) var inCommunity, ok bool if inCommunity, ok = existingKeys[portal.Key]; !ok || !inCommunity { @@ -704,6 +783,15 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) user.log.Warnln("Failed to update user-portal mapping:", err) } sort.Sort(chats) + return chats +} + +func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) { + // TODO use contexts instead of checking if user.Conn is the same? + connAtStart := user.Conn + + chats := user.collectChatList(chatMap) + limit := user.bridge.Config.Bridge.InitialChatSync if limit < 0 { limit = len(chats) @@ -712,7 +800,7 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) user.log.Debugln("Connection seems to have changed before sync, cancelling") return } - now := uint64(time.Now().Unix()) + now := time.Now().Unix() user.log.Infoln("Syncing portals") doublePuppet := user.bridge.GetPuppetByCustomMXID(user.MXID) for i, chat := range chats { @@ -721,23 +809,8 @@ 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 { - // 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) - } - if chat.MarkAsRead && doublePuppet != nil && doublePuppet.CustomIntent() != nil { - lastMessage := user.bridge.DB.Message.GetLastInChat(chat.Portal.Key) - if lastMessage != nil { - err = doublePuppet.CustomIntent().MarkRead(chat.Portal.MXID, lastMessage.MXID) - if err != nil { - user.log.Warnln("Failed to mark %s in %s as read after backfill: %v", lastMessage.MXID, chat.Portal.MXID, err) - } - } - } + user.syncPortal(chat) + user.syncChatDoublePuppetDetails(doublePuppet, chat) } } if user.Conn != connAtStart { @@ -745,6 +818,7 @@ func (user *User) syncPortals(chatMap map[string]whatsapp.Chat, createAll bool) return } user.UpdateDirectChats(nil) + user.log.Infoln("Finished syncing portals") select { case user.syncPortalsDone <- struct{}{}: @@ -846,7 +920,7 @@ func (user *User) syncPuppets(contacts map[whatsapp.JID]whatsapp.Contact) { } func (user *User) updateLastConnectionIfNecessary() { - if user.LastConnection+60 < uint64(time.Now().Unix()) { + if user.LastConnection+60 < time.Now().Unix() { user.UpdateLastConnection() } }