Add option to bridge archive and mute status from WhatsApp

This commit is contained in:
Tulir Asokan 2021-04-19 22:14:32 +03:00
parent 06a041981d
commit badea9c547
8 changed files with 139 additions and 55 deletions

View file

@ -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"`

View file

@ -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
}

View file

@ -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 {

View file

@ -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.

2
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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
}

146
user.go
View file

@ -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()
}
}