mirror of
https://github.com/tulir/mautrix-whatsapp
synced 2024-12-04 20:52:54 +01:00
msgconv/from-whatsapp: cache converted media messages to bridge caption edits
This commit is contained in:
parent
c40fa063b4
commit
d961ed1de7
12 changed files with 171 additions and 21 deletions
2
go.mod
2
go.mod
|
@ -18,7 +18,7 @@ require (
|
|||
golang.org/x/sync v0.8.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.21.1-0.20241010140510-38610d681dcd
|
||||
maunium.net/go/mautrix v0.21.1-0.20241014164722-965008e8462e
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
4
go.sum
4
go.sum
|
@ -101,5 +101,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||
maunium.net/go/mautrix v0.21.1-0.20241010140510-38610d681dcd h1:otYRelHwBCFpyqc0zIk4RjUx0tVbUuKstsfE0mLWcfU=
|
||||
maunium.net/go/mautrix v0.21.1-0.20241010140510-38610d681dcd/go.mod h1:+fF5qsmXRCEXQZgW5ececC0PI3c7gISHTLcyftP4Bh0=
|
||||
maunium.net/go/mautrix v0.21.1-0.20241014164722-965008e8462e h1:iGplBWWCj/QJ9yceX8jg6LS6tZZKKmm5uX3MUQxR4Rg=
|
||||
maunium.net/go/mautrix v0.21.1-0.20241014164722-965008e8462e/go.mod h1:yIs8uVcl3ZiTuDzAYmk/B4/z9dQqegF0rcOWV4ncgko=
|
||||
|
|
|
@ -330,7 +330,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
|
|||
// TODO use proper intent
|
||||
intent := wa.Main.Bridge.Bot
|
||||
wrapped := &bridgev2.BackfillMessage{
|
||||
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, info, isViewOnce),
|
||||
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, info, isViewOnce, nil),
|
||||
Sender: wa.makeEventSender(info.Sender),
|
||||
ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
|
||||
TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)),
|
||||
|
|
|
@ -18,6 +18,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
|
|||
}
|
||||
|
||||
const WAMaxFileSize = 2000 * 1024 * 1024
|
||||
const EditMaxAge = 15 * time.Minute
|
||||
|
||||
var whatsappCaps = &bridgev2.NetworkRoomCapabilities{
|
||||
FormattedText: true,
|
||||
|
@ -28,7 +29,7 @@ var whatsappCaps = &bridgev2.NetworkRoomCapabilities{
|
|||
Polls: true,
|
||||
Edits: true,
|
||||
EditMaxCount: 10,
|
||||
EditMaxAge: 15 * time.Minute,
|
||||
EditMaxAge: EditMaxAge,
|
||||
Deletes: true,
|
||||
DeleteMaxAge: 48 * time.Hour,
|
||||
DefaultFileRestriction: &bridgev2.FileRestriction{
|
||||
|
|
|
@ -153,6 +153,7 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) error {
|
|||
wa.UserLogin.BridgeState.Send(state)
|
||||
return nil
|
||||
}
|
||||
wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect)
|
||||
if err := wa.Main.updateProxy(ctx, wa.Client, false); err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy")
|
||||
}
|
||||
|
|
|
@ -1,8 +1,26 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2024 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||
|
@ -23,10 +41,19 @@ type WhatsAppConnector struct {
|
|||
DeviceStore *sqlstore.Container
|
||||
MsgConv *msgconv.MessageConverter
|
||||
DB *wadb.Database
|
||||
|
||||
firstClientConnectOnce sync.Once
|
||||
|
||||
mediaEditCache MediaEditCache
|
||||
mediaEditCacheLock sync.RWMutex
|
||||
stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc]
|
||||
}
|
||||
|
||||
var _ bridgev2.NetworkConnector = (*WhatsAppConnector)(nil)
|
||||
var _ bridgev2.MaxFileSizeingNetwork = (*WhatsAppConnector)(nil)
|
||||
var (
|
||||
_ bridgev2.NetworkConnector = (*WhatsAppConnector)(nil)
|
||||
_ bridgev2.MaxFileSizeingNetwork = (*WhatsAppConnector)(nil)
|
||||
_ bridgev2.StoppableNetwork = (*WhatsAppConnector)(nil)
|
||||
)
|
||||
|
||||
func (wa *WhatsAppConnector) SetMaxFileSize(maxSize int64) {
|
||||
wa.MsgConv.MaxFileSize = maxSize
|
||||
|
@ -63,6 +90,7 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
|
|||
wa.Bridge.Commands.(*commands.Processor).AddHandlers(
|
||||
cmdAccept,
|
||||
)
|
||||
wa.mediaEditCache = make(MediaEditCache)
|
||||
|
||||
wa.DeviceStore = sqlstore.NewWithDB(
|
||||
bridge.DB.RawDB,
|
||||
|
@ -95,6 +123,16 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
|
|||
return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) Stop() {
|
||||
if stop := wa.stopMediaEditCacheLoop.Load(); stop != nil {
|
||||
(*stop)()
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) onFirstClientConnect() {
|
||||
ver, err := whatsmeow.GetLatestVersion(nil)
|
||||
if err != nil {
|
||||
wa.Bridge.Log.Err(err).Msg("Failed to get latest WhatsApp web version number")
|
||||
|
@ -105,5 +143,7 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
|
|||
Msg("Got latest WhatsApp web version number")
|
||||
store.SetWAVersion(*ver)
|
||||
}
|
||||
return nil
|
||||
meclCtx, cancel := context.WithCancel(context.Background())
|
||||
wa.stopMediaEditCacheLoop.Store(&cancel)
|
||||
go wa.mediaEditCacheExpireLoop(meclCtx)
|
||||
}
|
||||
|
|
|
@ -132,15 +132,16 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
|
|||
zerolog.Ctx(ctx).Warn().Msg("Got edit to message with multiple parts")
|
||||
}
|
||||
var editedMsg *waE2E.Message
|
||||
var previouslyConvertedPart *bridgev2.ConvertedMessagePart
|
||||
if evt.isUndecryptableUpsertSubEvent {
|
||||
// TODO db metadata needs to be updated in this case to remove the error
|
||||
editedMsg = evt.Message
|
||||
} else {
|
||||
editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage()
|
||||
previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, evt.GetTargetMessage())
|
||||
}
|
||||
|
||||
// TODO edits to media captions may not contain the media
|
||||
cm := evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, editedMsg, &evt.Info, evt.isViewOnce())
|
||||
cm := evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, editedMsg, &evt.Info, evt.isViewOnce(), previouslyConvertedPart)
|
||||
if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) {
|
||||
evt.postHandle = func() {
|
||||
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false)
|
||||
|
@ -228,11 +229,13 @@ func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2.
|
|||
|
||||
func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
|
||||
evt.wa.EnqueuePortalResync(portal)
|
||||
converted := evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, evt.Message, &evt.Info, evt.isViewOnce())
|
||||
converted := evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, evt.Message, &evt.Info, evt.isViewOnce(), nil)
|
||||
if isFailedMedia(converted) {
|
||||
evt.postHandle = func() {
|
||||
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), converted, false)
|
||||
}
|
||||
} else if len(converted.Parts) > 0 {
|
||||
evt.wa.Main.AddMediaEditCache(portal, evt.GetID(), converted.Parts[0])
|
||||
}
|
||||
return converted, nil
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ var (
|
|||
const LoginConnectWait = 15 * time.Second
|
||||
|
||||
func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect)
|
||||
device := wl.Main.DeviceStore.NewDevice()
|
||||
wl.Client = whatsmeow.NewClient(device, waLog.Zerolog(wl.Log))
|
||||
wl.Client.EnableAutoReconnect = false
|
||||
|
|
91
pkg/connector/mediaeditcache.go
Normal file
91
pkg/connector/mediaeditcache.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2024 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type MediaEditCacheKey struct {
|
||||
MessageID networkid.MessageID
|
||||
PortalMXID id.RoomID
|
||||
}
|
||||
|
||||
type MediaEditCacheValue struct {
|
||||
Part *bridgev2.ConvertedMessagePart
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
type MediaEditCache map[MediaEditCacheKey]MediaEditCacheValue
|
||||
|
||||
func (wa *WhatsAppConnector) mediaEditCacheExpireLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
ctxDone := ctx.Done()
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-ctxDone:
|
||||
return
|
||||
}
|
||||
wa.expireMediaEditCache()
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) AddMediaEditCache(portal *bridgev2.Portal, messageID networkid.MessageID, converted *bridgev2.ConvertedMessagePart) {
|
||||
if converted.Type != event.EventSticker && !converted.Content.MsgType.IsMedia() {
|
||||
return
|
||||
}
|
||||
wa.mediaEditCacheLock.Lock()
|
||||
defer wa.mediaEditCacheLock.Unlock()
|
||||
wa.mediaEditCache[MediaEditCacheKey{
|
||||
MessageID: messageID,
|
||||
PortalMXID: portal.MXID,
|
||||
}] = MediaEditCacheValue{
|
||||
Part: converted,
|
||||
Expiry: time.Now().Add(EditMaxAge + 5*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetMediaEditCache(portal *bridgev2.Portal, messageID networkid.MessageID) *bridgev2.ConvertedMessagePart {
|
||||
wa.mediaEditCacheLock.RLock()
|
||||
defer wa.mediaEditCacheLock.RUnlock()
|
||||
value, ok := wa.mediaEditCache[MediaEditCacheKey{
|
||||
MessageID: messageID,
|
||||
PortalMXID: portal.MXID,
|
||||
}]
|
||||
if !ok || time.Until(value.Expiry) < 0 {
|
||||
return nil
|
||||
}
|
||||
return value.Part
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) expireMediaEditCache() {
|
||||
wa.mediaEditCacheLock.Lock()
|
||||
defer wa.mediaEditCacheLock.Unlock()
|
||||
for key, value := range wa.mediaEditCache {
|
||||
if time.Until(value.Expiry) < 0 {
|
||||
delete(wa.mediaEditCache, key)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -103,6 +103,7 @@ func (mc *MessageConverter) ToMatrix(
|
|||
waMsg *waE2E.Message,
|
||||
info *types.MessageInfo,
|
||||
isViewOnce bool,
|
||||
previouslyConvertedPart *bridgev2.ConvertedMessagePart,
|
||||
) *bridgev2.ConvertedMessage {
|
||||
ctx = context.WithValue(ctx, contextKeyClient, client)
|
||||
ctx = context.WithValue(ctx, contextKeyIntent, intent)
|
||||
|
@ -133,21 +134,21 @@ func (mc *MessageConverter) ToMatrix(
|
|||
case waMsg.EventMessage != nil:
|
||||
part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage)
|
||||
case waMsg.ImageMessage != nil:
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.ImageMessage, "photo", isViewOnce)
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.ImageMessage, "photo", isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.StickerMessage != nil:
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.StickerMessage, "sticker", isViewOnce)
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.StickerMessage, "sticker", isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.VideoMessage != nil:
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.VideoMessage, "video attachment", isViewOnce)
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.VideoMessage, "video attachment", isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.PtvMessage != nil:
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.PtvMessage, "video message", isViewOnce)
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.PtvMessage, "video message", isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.AudioMessage != nil:
|
||||
typeName := "audio attachment"
|
||||
if waMsg.AudioMessage.GetPTT() {
|
||||
typeName = "voice message"
|
||||
}
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.AudioMessage, typeName, isViewOnce)
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.AudioMessage, typeName, isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.DocumentMessage != nil:
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.DocumentMessage, "file attachment", isViewOnce)
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.DocumentMessage, "file attachment", isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.LocationMessage != nil:
|
||||
part, contextInfo = mc.convertLocationMessage(ctx, waMsg.LocationMessage)
|
||||
case waMsg.LiveLocationMessage != nil:
|
||||
|
|
|
@ -70,11 +70,11 @@ func (mc *MessageConverter) convertTemplateMessage(ctx context.Context, info *ty
|
|||
var convertedTitle *bridgev2.ConvertedMessagePart
|
||||
switch title := tpl.GetTitle().(type) {
|
||||
case *waE2E.TemplateMessage_HydratedFourRowTemplate_DocumentMessage:
|
||||
convertedTitle, _ = mc.convertMediaMessage(ctx, title.DocumentMessage, "file attachment", false)
|
||||
convertedTitle, _ = mc.convertMediaMessage(ctx, title.DocumentMessage, "file attachment", false, nil)
|
||||
case *waE2E.TemplateMessage_HydratedFourRowTemplate_ImageMessage:
|
||||
convertedTitle, _ = mc.convertMediaMessage(ctx, title.ImageMessage, "photo", false)
|
||||
convertedTitle, _ = mc.convertMediaMessage(ctx, title.ImageMessage, "photo", false, nil)
|
||||
case *waE2E.TemplateMessage_HydratedFourRowTemplate_VideoMessage:
|
||||
convertedTitle, _ = mc.convertMediaMessage(ctx, title.VideoMessage, "video attachment", false)
|
||||
convertedTitle, _ = mc.convertMediaMessage(ctx, title.VideoMessage, "video attachment", false, nil)
|
||||
case *waE2E.TemplateMessage_HydratedFourRowTemplate_LocationMessage:
|
||||
content = fmt.Sprintf("Unsupported location message\n\n%s", content)
|
||||
case *waE2E.TemplateMessage_HydratedFourRowTemplate_HydratedTitleText:
|
||||
|
|
|
@ -44,7 +44,13 @@ import (
|
|||
"maunium.net/go/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (mc *MessageConverter) convertMediaMessage(ctx context.Context, msg MediaMessage, typeName string, isViewOnce bool) (part *bridgev2.ConvertedMessagePart, contextInfo *waE2E.ContextInfo) {
|
||||
func (mc *MessageConverter) convertMediaMessage(
|
||||
ctx context.Context,
|
||||
msg MediaMessage,
|
||||
typeName string,
|
||||
isViewOnce bool,
|
||||
cachedPart *bridgev2.ConvertedMessagePart,
|
||||
) (part *bridgev2.ConvertedMessagePart, contextInfo *waE2E.ContextInfo) {
|
||||
if mc.DisableViewOnce && isViewOnce {
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
|
@ -60,6 +66,12 @@ func (mc *MessageConverter) convertMediaMessage(ctx context.Context, msg MediaMe
|
|||
mc.parseFormatting(preparedMedia.MessageEventContent, false, false)
|
||||
}
|
||||
contextInfo = preparedMedia.ContextInfo
|
||||
if cachedPart != nil && msg.GetDirectPath() == "" {
|
||||
cachedPart.Content.Body = preparedMedia.Body
|
||||
cachedPart.Content.Format = preparedMedia.Format
|
||||
cachedPart.Content.FormattedBody = preparedMedia.FormattedBody
|
||||
return cachedPart, contextInfo
|
||||
}
|
||||
err := mc.reuploadWhatsAppAttachment(ctx, msg, preparedMedia)
|
||||
if err != nil {
|
||||
part = mc.makeMediaFailure(ctx, preparedMedia, &FailedMediaKeys{
|
||||
|
|
Loading…
Reference in a new issue