From a8d1900203cfe3d9b7158edfdddc81f2dc7ce4e0 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 19 Nov 2023 14:51:04 +0200 Subject: [PATCH 01/12] Add support for sending media to newsletters --- CHANGELOG.md | 4 ++++ go.mod | 2 +- go.sum | 4 ++-- portal.go | 27 ++++++++++++++++++++++++--- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ab52bc..289838b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# unreleaased + +* Added support for sending media to channels. + # v0.10.4 (2023-11-16) * Added support for channels in `join` and `open` commands. diff --git a/go.mod b/go.mod index 40eebf7..37d4152 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/tidwall/gjson v1.17.0 go.mau.fi/util v0.2.1 go.mau.fi/webp v0.1.0 - go.mau.fi/whatsmeow v0.0.0-20231116213319-217e0c985fd6 + go.mau.fi/whatsmeow v0.0.0-20231119124248-cbe5f2ddc37a golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/image v0.14.0 golang.org/x/net v0.18.0 diff --git a/go.sum b/go.sum index 0852109..4624c07 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw= go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c= go.mau.fi/webp v0.1.0 h1:BHObH/DcFntT9KYun5pDr0Ot4eUZO8k2C7eP7vF4ueA= go.mau.fi/webp v0.1.0/go.mod h1:e42Z+VMFrUMS9cpEwGRIor+lQWO8oUAyPyMtcL+NMt8= -go.mau.fi/whatsmeow v0.0.0-20231116213319-217e0c985fd6 h1:GLGU8Q7zoW0NjqZGXdPAjAwmJTSI09HIdhjL0DY21oc= -go.mau.fi/whatsmeow v0.0.0-20231116213319-217e0c985fd6/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= +go.mau.fi/whatsmeow v0.0.0-20231119124248-cbe5f2ddc37a h1:u7W/BzMZla2yD5xf0txDRx6ZI7rJNZ1HjLQQKgOgdTw= +go.mau.fi/whatsmeow v0.0.0-20231119124248-cbe5f2ddc37a/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= diff --git a/portal.go b/portal.go index 778f4ce..be07262 100644 --- a/portal.go +++ b/portal.go @@ -3894,7 +3894,12 @@ func (portal *Portal) preprocessMatrixMedia(ctx context.Context, sender *User, r portal.log.Warnfln("Failed to re-encode %s media: %v, continuing with original file", mimeType, convertErr) } } - uploadResp, err := sender.Client.Upload(ctx, data, mediaType) + var uploadResp whatsmeow.UploadResponse + if portal.Key.JID.Server == types.NewsletterServer { + uploadResp, err = sender.Client.UploadNewsletter(ctx, data, mediaType) + } else { + uploadResp, err = sender.Client.Upload(ctx, data, mediaType) + } if err != nil { return nil, exerrors.NewDualError(errMediaWhatsAppUploadFailed, err) } @@ -4202,6 +4207,8 @@ type extraConvertMeta struct { EditRootMsg *database.Message GalleryExtraParts []*waProto.Message + + MediaHandle string } func getEditError(rootMsg *database.Message, editer *User) error { @@ -4303,6 +4310,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if media == nil { return nil, sender, extraMeta, err } + extraMeta.MediaHandle = media.Handle ctxInfo.MentionedJid = media.MentionedJIDs msg.ImageMessage = &waProto.ImageMessage{ ContextInfo: ctxInfo, @@ -4321,6 +4329,9 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev return nil, sender, extraMeta, errGalleryRelay } else if content.BeeperGalleryCaption != "" { return nil, sender, extraMeta, errGalleryCaption + } else if portal.Key.JID.Server == types.NewsletterServer { + // We don't handle the media handles properly for multiple messages + return nil, sender, extraMeta, fmt.Errorf("can't send gallery to newsletter") } for i, part := range content.BeeperGalleryImages { // TODO support videos @@ -4352,6 +4363,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if media == nil { return nil, sender, extraMeta, err } + extraMeta.MediaHandle = media.Handle ctxInfo.MentionedJid = media.MentionedJIDs msg.StickerMessage = &waProto.StickerMessage{ ContextInfo: ctxInfo, @@ -4371,6 +4383,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev return nil, sender, extraMeta, err } duration := uint32(content.GetInfo().Duration / 1000) + extraMeta.MediaHandle = media.Handle ctxInfo.MentionedJid = media.MentionedJIDs msg.VideoMessage = &waProto.VideoMessage{ ContextInfo: ctxInfo, @@ -4391,6 +4404,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if media == nil { return nil, sender, extraMeta, err } + extraMeta.MediaHandle = media.Handle duration := uint32(content.GetInfo().Duration / 1000) msg.AudioMessage = &waProto.AudioMessage{ ContextInfo: ctxInfo, @@ -4415,6 +4429,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if media == nil { return nil, sender, extraMeta, err } + extraMeta.MediaHandle = media.Handle msg.DocumentMessage = &waProto.DocumentMessage{ ContextInfo: ctxInfo, Caption: &media.Caption, @@ -4562,6 +4577,9 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event, timing go ms.sendMessageMetrics(evt, err, "Error converting", true) return } + if extraMeta == nil { + extraMeta = &extraConvertMeta{} + } dbMsgType := database.MsgNormal if msg.PollCreationMessage != nil || msg.PollCreationMessageV2 != nil || msg.PollCreationMessageV3 != nil { dbMsgType = database.MsgMatrixPoll @@ -4576,12 +4594,15 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event, timing } else { info.ID = dbMsg.JID } - if dbMsgType == database.MsgMatrixPoll && extraMeta != nil && extraMeta.PollOptions != nil { + if dbMsgType == database.MsgMatrixPoll && extraMeta.PollOptions != nil { dbMsg.PutPollOptions(extraMeta.PollOptions) } portal.log.Debugln("Sending event", evt.ID, "to WhatsApp", info.ID) start = time.Now() - resp, err := sender.Client.SendMessage(ctx, portal.Key.JID, msg, whatsmeow.SendRequestExtra{ID: info.ID}) + resp, err := sender.Client.SendMessage(ctx, portal.Key.JID, msg, whatsmeow.SendRequestExtra{ + ID: info.ID, + MediaHandle: extraMeta.MediaHandle, + }) timings.totalSend = time.Since(start) timings.whatsmeow = resp.DebugTimings if err != nil { From 6c983176bd4aff5956e2a1b9375257b80c12dd69 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 27 Nov 2023 20:00:01 +0200 Subject: [PATCH 02/12] Update dependencies --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 37d4152..241a6e7 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,10 @@ require ( github.com/tidwall/gjson v1.17.0 go.mau.fi/util v0.2.1 go.mau.fi/webp v0.1.0 - go.mau.fi/whatsmeow v0.0.0-20231119124248-cbe5f2ddc37a + go.mau.fi/whatsmeow v0.0.0-20231127175850-3c97433b4676 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/image v0.14.0 - golang.org/x/net v0.18.0 + golang.org/x/net v0.19.0 google.golang.org/protobuf v1.31.0 maunium.net/go/maulogger/v2 v2.4.1 maunium.net/go/mautrix v0.16.2 @@ -41,8 +41,8 @@ require ( github.com/yuin/goldmark v1.6.0 // indirect go.mau.fi/libsignal v0.1.0 // indirect go.mau.fi/zeroconfig v0.1.2 // indirect - golang.org/x/crypto v0.15.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 4624c07..7b20e9e 100644 --- a/go.sum +++ b/go.sum @@ -68,24 +68,24 @@ go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw= go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c= go.mau.fi/webp v0.1.0 h1:BHObH/DcFntT9KYun5pDr0Ot4eUZO8k2C7eP7vF4ueA= go.mau.fi/webp v0.1.0/go.mod h1:e42Z+VMFrUMS9cpEwGRIor+lQWO8oUAyPyMtcL+NMt8= -go.mau.fi/whatsmeow v0.0.0-20231119124248-cbe5f2ddc37a h1:u7W/BzMZla2yD5xf0txDRx6ZI7rJNZ1HjLQQKgOgdTw= -go.mau.fi/whatsmeow v0.0.0-20231119124248-cbe5f2ddc37a/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= +go.mau.fi/whatsmeow v0.0.0-20231127175850-3c97433b4676 h1:YaFOVOdFJCSSByUT6WSgh65ChcVEDZN4QD7OTwaMw+k= +go.mau.fi/whatsmeow v0.0.0-20231127175850-3c97433b4676/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 4e6e486e19772aaf8f76c2261b58c0c7611641f6 Mon Sep 17 00:00:00 2001 From: Toni Spets Date: Tue, 5 Dec 2023 11:08:42 +0200 Subject: [PATCH 03/12] Expose debug API with pprof Runs along the provisioning API with same authentication. --- config/bridge.go | 5 +++-- config/upgrade.go | 1 + example-config.yaml | 2 ++ provisioning.go | 8 ++++++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/config/bridge.go b/config/bridge.go index 689c263..538908f 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -136,8 +136,9 @@ type BridgeConfig struct { Encryption bridgeconfig.EncryptionConfig `yaml:"encryption"` Provisioning struct { - Prefix string `yaml:"prefix"` - SharedSecret string `yaml:"shared_secret"` + Prefix string `yaml:"prefix"` + SharedSecret string `yaml:"shared_secret"` + DebugEndpoints bool `yaml:"debug_endpoints"` } `yaml:"provisioning"` Permissions bridgeconfig.PermissionConfig `yaml:"permissions"` diff --git a/config/upgrade.go b/config/upgrade.go index 570e201..b1999c8 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -160,6 +160,7 @@ func DoUpgrade(helper *up.Helper) { } else { helper.Copy(up.Str, "bridge", "provisioning", "prefix") } + helper.Copy(up.Bool, "bridge", "provisioning", "debug_endpoints") if secret, ok := helper.Get(up.Str, "appservice", "provisioning", "shared_secret"); ok && secret != "generate" { helper.Set(up.Str, secret, "bridge", "provisioning", "shared_secret") } else if secret, ok = helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" { diff --git a/example-config.yaml b/example-config.yaml index 95ed83d..058f3e1 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -424,6 +424,8 @@ bridge: # Shared secret for authentication. If set to "generate", a random secret will be generated, # or if set to "disable", the provisioning API will be disabled. shared_secret: generate + # Enable debug API at /debug with provisioning authentication. + debug_endpoints: false # Permissions for using the bridge. # Permitted values: diff --git a/provisioning.go b/provisioning.go index 65c8a2b..911338d 100644 --- a/provisioning.go +++ b/provisioning.go @@ -24,6 +24,7 @@ import ( "fmt" "net" "net/http" + _ "net/http/pprof" "strings" "time" @@ -71,6 +72,13 @@ func (prov *ProvisioningAPI) Init() { prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.asmux/ping", prov.BridgeStatePing).Methods(http.MethodPost) prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.bridge_state", prov.BridgeStatePing).Methods(http.MethodPost) + if prov.bridge.Config.Bridge.Provisioning.DebugEndpoints { + prov.log.Debugln("Enabling debug API at /debug") + r := prov.bridge.AS.Router.PathPrefix("/debug").Subrouter() + r.Use(prov.AuthMiddleware) + r.PathPrefix("/pprof").Handler(http.DefaultServeMux) + } + // Deprecated, just use /disconnect r.HandleFunc("/v1/delete_connection", prov.Disconnect).Methods(http.MethodPost) } From 2589049f141cf33190afc6a6f7ee8a293e8b34de Mon Sep 17 00:00:00 2001 From: Toni Spets Date: Wed, 6 Dec 2023 08:58:46 +0200 Subject: [PATCH 04/12] Compress portal event channels into one Use a single channel of pointers to pre-allocate less memory per portal. --- portal.go | 42 +++++++++++++++++++++++++++--------------- user.go | 52 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/portal.go b/portal.go index be07262..ddf13a7 100644 --- a/portal.go +++ b/portal.go @@ -110,7 +110,13 @@ func (portal *Portal) MarkEncrypted() { func (portal *Portal) ReceiveMatrixEvent(user bridge.User, evt *event.Event) { if user.GetPermissionLevel() >= bridgeconfig.PermissionLevelUser || portal.HasRelaybot() { - portal.matrixMessages <- PortalMatrixMessage{user: user.(*User), evt: evt, receivedAt: time.Now()} + portal.events <- &PortalEvent{ + MatrixMessage: &PortalMatrixMessage{ + user: user.(*User), + evt: evt, + receivedAt: time.Now(), + }, + } } } @@ -199,9 +205,7 @@ func (br *WABridge) newBlankPortal(key database.PortalKey) *Portal { log: br.Log.Sub(fmt.Sprintf("Portal/%s", key)), zlog: br.ZLog.With().Str("portal_key", key.String()).Logger(), - messages: make(chan PortalMessage, br.Config.Bridge.PortalMessageBuffer), - matrixMessages: make(chan PortalMatrixMessage, br.Config.Bridge.PortalMessageBuffer), - mediaRetries: make(chan PortalMediaRetry, br.Config.Bridge.PortalMessageBuffer), + events: make(chan *PortalEvent, br.Config.Bridge.PortalMessageBuffer), mediaErrorCache: make(map[types.MessageID]*FailedMediaMeta), } @@ -232,6 +236,12 @@ type fakeMessage struct { Important bool } +type PortalEvent struct { + Message *PortalMessage + MatrixMessage *PortalMatrixMessage + MediaRetry *PortalMediaRetry +} + type PortalMessage struct { evt *events.Message undecryptable *events.UndecryptableMessage @@ -279,9 +289,7 @@ type Portal struct { currentlyTyping []id.UserID currentlyTypingLock sync.Mutex - messages chan PortalMessage - matrixMessages chan PortalMatrixMessage - mediaRetries chan PortalMediaRetry + events chan *PortalEvent mediaErrorCache map[types.MessageID]*FailedMediaMeta @@ -337,7 +345,7 @@ var ( _ bridge.TypingPortal = (*Portal)(nil) ) -func (portal *Portal) handleWhatsAppMessageLoopItem(msg PortalMessage) { +func (portal *Portal) handleWhatsAppMessageLoopItem(msg *PortalMessage) { if len(portal.MXID) == 0 { if msg.fake == nil && msg.undecryptable == nil && (msg.evt == nil || !containsSupportedMessage(msg.evt.Message)) { portal.log.Debugln("Not creating portal room for incoming message: message is not a chat message") @@ -369,7 +377,7 @@ func (portal *Portal) handleWhatsAppMessageLoopItem(msg PortalMessage) { } } -func (portal *Portal) handleMatrixMessageLoopItem(msg PortalMatrixMessage) { +func (portal *Portal) handleMatrixMessageLoopItem(msg *PortalMatrixMessage) { portal.latestEventBackfillLock.Lock() defer portal.latestEventBackfillLock.Unlock() evtTS := time.UnixMilli(msg.evt.Timestamp) @@ -483,12 +491,16 @@ func (portal *Portal) handleOneMessageLoopItem() { } }() select { - case msg := <-portal.messages: - portal.handleWhatsAppMessageLoopItem(msg) - case msg := <-portal.matrixMessages: - portal.handleMatrixMessageLoopItem(msg) - case retry := <-portal.mediaRetries: - portal.handleMediaRetry(retry.evt, retry.source) + case msg := <-portal.events: + if msg.Message != nil { + portal.handleWhatsAppMessageLoopItem(msg.Message) + } else if msg.MatrixMessage != nil { + portal.handleMatrixMessageLoopItem(msg.MatrixMessage) + } else if msg.MediaRetry != nil { + portal.handleMediaRetry(msg.MediaRetry.evt, msg.MediaRetry.source) + } else { + portal.log.Warn("Portal event loop returned an event without any data") + } } } diff --git a/user.go b/user.go index 5926fe7..00e276c 100644 --- a/user.go +++ b/user.go @@ -635,15 +635,17 @@ func (user *User) handleCallStart(sender types.JID, id, callType string, ts time if callType != "" { text = fmt.Sprintf("Incoming %s call. Use the WhatsApp app to answer.", callType) } - portal.messages <- PortalMessage{ - fake: &fakeMessage{ - Sender: sender, - Text: text, - ID: id, - Time: ts, - Important: true, + portal.events <- &PortalEvent{ + Message: &PortalMessage{ + fake: &fakeMessage{ + Sender: sender, + Text: text, + ID: id, + Time: ts, + Important: true, + }, + source: user, }, - source: user, } } @@ -865,11 +867,15 @@ func (user *User) HandleEvent(event interface{}) { go user.handleChatPresence(v) case *events.Message: portal := user.GetPortalByMessageSource(v.Info.MessageSource) - portal.messages <- PortalMessage{evt: v, source: user} + portal.events <- &PortalEvent{ + Message: &PortalMessage{evt: v, source: user}, + } case *events.MediaRetry: user.phoneSeen(v.Timestamp) portal := user.GetPortalByJID(v.ChatID) - portal.mediaRetries <- PortalMediaRetry{evt: v, source: user} + portal.events <- &PortalEvent{ + MediaRetry: &PortalMediaRetry{evt: v, source: user}, + } case *events.CallOffer: user.handleCallStart(v.CallCreator, v.CallID, "", v.Timestamp) case *events.CallOfferNotice: @@ -885,22 +891,26 @@ func (user *User) HandleEvent(event interface{}) { if v.Implicit { text = fmt.Sprintf("Your security code with %s (device #%d) changed.", puppet.Displayname, v.JID.Device) } - portal.messages <- PortalMessage{ - fake: &fakeMessage{ - Sender: v.JID, - Text: text, - ID: strconv.FormatInt(v.Timestamp.Unix(), 10), - Time: v.Timestamp, - Important: false, + portal.events <- &PortalEvent{ + Message: &PortalMessage{ + fake: &fakeMessage{ + Sender: v.JID, + Text: text, + ID: strconv.FormatInt(v.Timestamp.Unix(), 10), + Time: v.Timestamp, + Important: false, + }, + source: user, }, - source: user, } } case *events.CallTerminate, *events.CallRelayLatency, *events.CallAccept, *events.UnknownCallEvent: // ignore case *events.UndecryptableMessage: portal := user.GetPortalByMessageSource(v.Info.MessageSource) - portal.messages <- PortalMessage{undecryptable: v, source: user} + portal.events <- &PortalEvent{ + Message: &PortalMessage{undecryptable: v, source: user}, + } case *events.HistorySync: if user.bridge.Config.Bridge.HistorySync.Backfill { user.historySyncs <- v @@ -1235,7 +1245,9 @@ func (user *User) handleReceipt(receipt *events.Receipt) { if portal == nil || len(portal.MXID) == 0 { return } - portal.messages <- PortalMessage{receipt: receipt, source: user} + portal.events <- &PortalEvent{ + Message: &PortalMessage{receipt: receipt, source: user}, + } } func (user *User) makeReadMarkerContent(eventID id.EventID, doublePuppet bool) CustomReadMarkers { From 345288bf0a37cf36e0d3ce097926a0fc7896ada5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Thu, 7 Dec 2023 13:48:19 +0200 Subject: [PATCH 05/12] Add icon for IDEA --- .idea/icon.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 .idea/icon.svg diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 0000000..17c2d59 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1 @@ + From e780eb2eb6869ca19387bfcf75e86b9b6b3a137d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 8 Dec 2023 15:02:40 +0200 Subject: [PATCH 06/12] Update Docker image to Alpine 3.19 --- Dockerfile | 4 ++-- Dockerfile.ci | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index b2943ea..5931d5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1-alpine3.18 AS builder +FROM golang:1-alpine3.19 AS builder RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev @@ -6,7 +6,7 @@ COPY . /build WORKDIR /build RUN go build -o /usr/bin/mautrix-whatsapp -FROM alpine:3.18 +FROM alpine:3.19 ENV UID=1337 \ GID=1337 diff --git a/Dockerfile.ci b/Dockerfile.ci index 2860a1d..e08e6ca 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1,4 +1,4 @@ -FROM alpine:3.18 +FROM alpine:3.19 ENV UID=1337 \ GID=1337 From 3e29c51c06561189636bb721490a812c58bb8ef6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 13 Dec 2023 19:57:23 +0200 Subject: [PATCH 07/12] Add missing nil check --- portal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal.go b/portal.go index ddf13a7..85c61e3 100644 --- a/portal.go +++ b/portal.go @@ -4622,7 +4622,7 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event, timing return } dbMsg.MarkSent(resp.Timestamp) - if len(extraMeta.GalleryExtraParts) > 0 { + if extraMeta != nil && len(extraMeta.GalleryExtraParts) > 0 { for i, part := range extraMeta.GalleryExtraParts { partInfo := portal.generateMessageInfo(sender) partDBMsg := portal.markHandled(nil, nil, partInfo, evt.ID, evt.Sender, false, true, database.MsgBeeperGallery, i+1, database.MsgNoError) From b752bb9c15a2b83a512eab996ff6eca30226b384 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 6 Dec 2023 14:47:41 +0200 Subject: [PATCH 08/12] Update whatsmeow --- go.mod | 2 +- go.sum | 4 ++-- portal.go | 10 +++------- user.go | 2 +- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 241a6e7..012b2b8 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/tidwall/gjson v1.17.0 go.mau.fi/util v0.2.1 go.mau.fi/webp v0.1.0 - go.mau.fi/whatsmeow v0.0.0-20231127175850-3c97433b4676 + go.mau.fi/whatsmeow v0.0.0-20231213182926-f17761e0fe8d golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/image v0.14.0 golang.org/x/net v0.19.0 diff --git a/go.sum b/go.sum index 7b20e9e..fb22b62 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw= go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c= go.mau.fi/webp v0.1.0 h1:BHObH/DcFntT9KYun5pDr0Ot4eUZO8k2C7eP7vF4ueA= go.mau.fi/webp v0.1.0/go.mod h1:e42Z+VMFrUMS9cpEwGRIor+lQWO8oUAyPyMtcL+NMt8= -go.mau.fi/whatsmeow v0.0.0-20231127175850-3c97433b4676 h1:YaFOVOdFJCSSByUT6WSgh65ChcVEDZN4QD7OTwaMw+k= -go.mau.fi/whatsmeow v0.0.0-20231127175850-3c97433b4676/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= +go.mau.fi/whatsmeow v0.0.0-20231213182926-f17761e0fe8d h1:ERk1aBRvcEC1fLb2HXDCb6UFvFHtuOSyRzFee9BxII4= +go.mau.fi/whatsmeow v0.0.0-20231213182926-f17761e0fe8d/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= diff --git a/portal.go b/portal.go index 85c61e3..f330bb4 100644 --- a/portal.go +++ b/portal.go @@ -430,7 +430,7 @@ func (portal *Portal) handleReceipt(receipt *events.Receipt, source *User) { // TODO handle lids return } - if receipt.Type == events.ReceiptTypeDelivered { + if receipt.Type == types.ReceiptTypeDelivered { portal.handleDeliveryReceipt(receipt, source) return } @@ -5039,9 +5039,7 @@ func (portal *Portal) HandleMatrixLeave(brSender bridge.User) { func (portal *Portal) HandleMatrixKick(brSender bridge.User, brTarget bridge.Ghost) { sender := brSender.(*User) target := brTarget.(*Puppet) - _, err := sender.Client.UpdateGroupParticipants(portal.Key.JID, map[types.JID]whatsmeow.ParticipantChange{ - target.JID: whatsmeow.ParticipantChangeRemove, - }) + _, err := sender.Client.UpdateGroupParticipants(portal.Key.JID, []types.JID{target.JID}, whatsmeow.ParticipantChangeRemove) if err != nil { portal.log.Errorfln("Failed to kick %s from group as %s: %v", target.JID, sender.MXID, err) return @@ -5052,9 +5050,7 @@ func (portal *Portal) HandleMatrixKick(brSender bridge.User, brTarget bridge.Gho func (portal *Portal) HandleMatrixInvite(brSender bridge.User, brTarget bridge.Ghost) { sender := brSender.(*User) target := brTarget.(*Puppet) - _, err := sender.Client.UpdateGroupParticipants(portal.Key.JID, map[types.JID]whatsmeow.ParticipantChange{ - target.JID: whatsmeow.ParticipantChangeAdd, - }) + _, err := sender.Client.UpdateGroupParticipants(portal.Key.JID, []types.JID{target.JID}, whatsmeow.ParticipantChangeAdd) if err != nil { portal.log.Errorfln("Failed to add %s to group as %s: %v", target.JID, sender.MXID, err) return diff --git a/user.go b/user.go index 00e276c..a6f9f68 100644 --- a/user.go +++ b/user.go @@ -1238,7 +1238,7 @@ func (user *User) handleChatPresence(presence *events.ChatPresence) { } func (user *User) handleReceipt(receipt *events.Receipt) { - if receipt.Type != events.ReceiptTypeRead && receipt.Type != events.ReceiptTypeReadSelf && receipt.Type != events.ReceiptTypeDelivered { + if receipt.Type != types.ReceiptTypeRead && receipt.Type != types.ReceiptTypeReadSelf && receipt.Type != types.ReceiptTypeDelivered { return } portal := user.GetPortalByMessageSource(receipt.MessageSource) From 19f6f8c896e19ad2e36ed1cc585d6b5facbc1a22 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 13 Dec 2023 20:41:16 +0200 Subject: [PATCH 09/12] Update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 289838b..5ed46d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ -# unreleaased +# v0.10.5 (unreleaased) * Added support for sending media to channels. +* Fixed voting in polls (seems to have broken due to a server-side change). +* Improved memory usage for bridges with lots of portals. # v0.10.4 (2023-11-16) From 6097735b3c4355a7007beecb23b183ab7eda0bb7 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 15 Dec 2023 14:36:18 +0200 Subject: [PATCH 10/12] Add word to error message --- commands.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands.go b/commands.go index ef2edae..339a195 100644 --- a/commands.go +++ b/commands.go @@ -114,7 +114,7 @@ func fnSetRelay(ce *WrappedCommandEvent) { if !ce.Bridge.Config.Bridge.Relay.Enabled { ce.Reply("Relay mode is not enabled on this instance of the bridge") } else if ce.Bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin { - ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge") + ce.Reply("Only bridge admins are allowed to enable relay mode on this instance of the bridge") } else { ce.Portal.RelayUserID = ce.User.MXID ce.Portal.Update(nil) @@ -136,7 +136,7 @@ func fnUnsetRelay(ce *WrappedCommandEvent) { if !ce.Bridge.Config.Bridge.Relay.Enabled { ce.Reply("Relay mode is not enabled on this instance of the bridge") } else if ce.Bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin { - ce.Reply("Only admins are allowed to enable relay mode on this instance of the bridge") + ce.Reply("Only bridge admins are allowed to enable relay mode on this instance of the bridge") } else { ce.Portal.RelayUserID = "" ce.Portal.Update(nil) From 687a90b7b2ec9ab0ef21a5b21e415920ee4d93f1 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 16 Dec 2023 23:37:12 +0200 Subject: [PATCH 11/12] Bump version to v0.10.5 --- CHANGELOG.md | 2 +- go.mod | 6 +++--- go.sum | 12 ++++++------ main.go | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed46d1..949d7df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# v0.10.5 (unreleaased) +# v0.10.5 (2023-12-16) * Added support for sending media to channels. * Fixed voting in polls (seems to have broken due to a server-side change). diff --git a/go.mod b/go.mod index 012b2b8..6135da5 100644 --- a/go.mod +++ b/go.mod @@ -6,15 +6,15 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/lib/pq v1.10.9 - github.com/mattn/go-sqlite3 v1.14.18 + github.com/mattn/go-sqlite3 v1.14.19 github.com/prometheus/client_golang v1.17.0 github.com/rs/zerolog v1.31.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tidwall/gjson v1.17.0 go.mau.fi/util v0.2.1 go.mau.fi/webp v0.1.0 - go.mau.fi/whatsmeow v0.0.0-20231213182926-f17761e0fe8d - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + go.mau.fi/whatsmeow v0.0.0-20231216213200-9d803dd92735 + golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 golang.org/x/image v0.14.0 golang.org/x/net v0.19.0 google.golang.org/protobuf v1.31.0 diff --git a/go.sum b/go.sum index fb22b62..f568af8 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= -github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= +github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -68,14 +68,14 @@ go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw= go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c= go.mau.fi/webp v0.1.0 h1:BHObH/DcFntT9KYun5pDr0Ot4eUZO8k2C7eP7vF4ueA= go.mau.fi/webp v0.1.0/go.mod h1:e42Z+VMFrUMS9cpEwGRIor+lQWO8oUAyPyMtcL+NMt8= -go.mau.fi/whatsmeow v0.0.0-20231213182926-f17761e0fe8d h1:ERk1aBRvcEC1fLb2HXDCb6UFvFHtuOSyRzFee9BxII4= -go.mau.fi/whatsmeow v0.0.0-20231213182926-f17761e0fe8d/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= +go.mau.fi/whatsmeow v0.0.0-20231216213200-9d803dd92735 h1:+teJYCOK6M4Kn2TYCj29levhHVwnJTmgCtEXLtgwQtM= +go.mau.fi/whatsmeow v0.0.0-20231216213200-9d803dd92735/go.mod h1:5xTtHNaZpGni6z6aE1iEopjW7wNgsKcolZxZrOujK9M= go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto= go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4= +golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= diff --git a/main.go b/main.go index 281c9d3..c8958af 100644 --- a/main.go +++ b/main.go @@ -268,7 +268,7 @@ func main() { Name: "mautrix-whatsapp", URL: "https://github.com/mautrix/whatsapp", Description: "A Matrix-WhatsApp puppeting bridge.", - Version: "0.10.4", + Version: "0.10.5", ProtocolName: "WhatsApp", BeeperServiceName: "whatsapp", BeeperNetworkName: "whatsapp", From a1f1c91be1d0192d306f31f48b8fe71203d9ccc7 Mon Sep 17 00:00:00 2001 From: Toni Spets Date: Tue, 19 Dec 2023 13:28:34 +0200 Subject: [PATCH 12/12] Prevent creating portals for non-backfillable conversations Defer initializing portal on backfill until there are messages. --- historysync.go | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/historysync.go b/historysync.go index bd16077..332f922 100644 --- a/historysync.go +++ b/historysync.go @@ -21,6 +21,7 @@ import ( "encoding/base64" "fmt" "strings" + "sync" "time" "github.com/rs/zerolog" @@ -452,26 +453,29 @@ func (user *User) storeHistorySync(evt *waProto.HistorySync) { continue } totalMessageCount += len(conv.GetMessages()) - portal := user.GetPortalByJID(jid) log := log.With(). - Str("chat_jid", portal.Key.JID.String()). + Str("chat_jid", jid.String()). Int("msg_count", len(conv.GetMessages())). Logger() - historySyncConversation := user.bridge.DB.HistorySync.NewConversationWithValues( - user.MXID, - conv.GetId(), - &portal.Key, - getConversationTimestamp(conv), - conv.GetMuteEndTime(), - conv.GetArchived(), - conv.GetPinned(), - conv.GetDisappearingMode().GetInitiator(), - conv.GetEndOfHistoryTransferType(), - conv.EphemeralExpiration, - conv.GetMarkedAsUnread(), - conv.GetUnreadCount()) - historySyncConversation.Upsert() + initPortal := sync.OnceFunc(func() { + portal := user.GetPortalByJID(jid) + historySyncConversation := user.bridge.DB.HistorySync.NewConversationWithValues( + user.MXID, + conv.GetId(), + &portal.Key, + getConversationTimestamp(conv), + conv.GetMuteEndTime(), + conv.GetArchived(), + conv.GetPinned(), + conv.GetDisappearingMode().GetInitiator(), + conv.GetEndOfHistoryTransferType(), + conv.EphemeralExpiration, + conv.GetMarkedAsUnread(), + conv.GetUnreadCount()) + historySyncConversation.Upsert() + }) + var minTime, maxTime time.Time var minTimeIndex, maxTimeIndex int @@ -479,7 +483,7 @@ func (user *User) storeHistorySync(evt *waProto.HistorySync) { unsupportedTypes := 0 for i, rawMsg := range conv.GetMessages() { // Don't store messages that will just be skipped. - msgEvt, err := user.Client.ParseWebMessage(portal.Key.JID, rawMsg.GetMessage()) + msgEvt, err := user.Client.ParseWebMessage(jid, rawMsg.GetMessage()) if err != nil { log.Warn().Err(err). Int("msg_index", i). @@ -503,6 +507,8 @@ func (user *User) storeHistorySync(evt *waProto.HistorySync) { continue } + initPortal() + message, err := user.bridge.DB.HistorySync.NewMessageWithValues(user.MXID, conv.GetId(), msgEvt.Info.ID, rawMsg) if err != nil { log.Error().Err(err).