2023-07-02 14:21:55 +00:00
|
|
|
// mautrix-gmessages - A Matrix-Google Messages puppeting bridge.
|
|
|
|
// Copyright (C) 2023 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 main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2023-08-24 11:48:03 +00:00
|
|
|
"encoding/base64"
|
2023-07-02 14:21:55 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"image"
|
|
|
|
_ "image/gif"
|
2023-09-04 08:51:52 +00:00
|
|
|
"runtime/debug"
|
2023-08-10 13:25:42 +00:00
|
|
|
"strconv"
|
2023-07-02 14:21:55 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
2023-08-08 14:05:56 +00:00
|
|
|
"sync/atomic"
|
2023-07-02 14:21:55 +00:00
|
|
|
"time"
|
|
|
|
|
2023-07-09 17:28:20 +00:00
|
|
|
"github.com/gabriel-vasile/mimetype"
|
2023-07-02 14:21:55 +00:00
|
|
|
"github.com/rs/zerolog"
|
2023-08-08 13:13:44 +00:00
|
|
|
"go.mau.fi/util/exerrors"
|
2024-03-11 14:55:11 +00:00
|
|
|
"go.mau.fi/util/ffmpeg"
|
2023-08-10 08:30:17 +00:00
|
|
|
"go.mau.fi/util/random"
|
2023-08-08 13:13:44 +00:00
|
|
|
"go.mau.fi/util/variationselector"
|
2023-07-02 14:21:55 +00:00
|
|
|
"maunium.net/go/mautrix"
|
|
|
|
"maunium.net/go/mautrix/appservice"
|
|
|
|
"maunium.net/go/mautrix/bridge"
|
2024-05-24 09:42:28 +00:00
|
|
|
"maunium.net/go/mautrix/bridge/status"
|
2023-07-02 14:21:55 +00:00
|
|
|
"maunium.net/go/mautrix/crypto/attachment"
|
|
|
|
"maunium.net/go/mautrix/event"
|
|
|
|
"maunium.net/go/mautrix/id"
|
|
|
|
|
|
|
|
"go.mau.fi/mautrix-gmessages/database"
|
2023-07-15 17:08:11 +00:00
|
|
|
"go.mau.fi/mautrix-gmessages/libgm"
|
2023-07-17 13:51:31 +00:00
|
|
|
"go.mau.fi/mautrix-gmessages/libgm/gmproto"
|
2023-07-02 14:21:55 +00:00
|
|
|
"go.mau.fi/mautrix-gmessages/libgm/util"
|
|
|
|
)
|
|
|
|
|
|
|
|
func (br *GMBridge) GetPortalByMXID(mxid id.RoomID) *Portal {
|
|
|
|
br.portalsLock.Lock()
|
|
|
|
defer br.portalsLock.Unlock()
|
|
|
|
portal, ok := br.portalsByMXID[mxid]
|
|
|
|
if !ok {
|
|
|
|
dbPortal, err := br.DB.Portal.GetByMXID(context.TODO(), mxid)
|
|
|
|
if err != nil {
|
|
|
|
br.ZLog.Err(err).Str("mxid", mxid.String()).Msg("Failed to get portal from database")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return br.loadDBPortal(dbPortal, nil)
|
|
|
|
}
|
|
|
|
return portal
|
|
|
|
}
|
|
|
|
|
|
|
|
func (br *GMBridge) GetIPortal(mxid id.RoomID) bridge.Portal {
|
|
|
|
p := br.GetPortalByMXID(mxid)
|
|
|
|
if p == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) IsEncrypted() bool {
|
|
|
|
return portal.Encrypted
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) MarkEncrypted() {
|
|
|
|
portal.Encrypted = true
|
|
|
|
err := portal.Update(context.TODO())
|
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to save portal to database after marking it as encrypted")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) ReceiveMatrixEvent(brUser bridge.User, evt *event.Event) {
|
|
|
|
user := brUser.(*User)
|
|
|
|
if user.RowID == portal.Receiver {
|
|
|
|
portal.matrixMessages <- PortalMatrixMessage{user: user, evt: evt, receivedAt: time.Now()}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (br *GMBridge) GetPortalByKey(key database.Key) *Portal {
|
|
|
|
br.portalsLock.Lock()
|
|
|
|
defer br.portalsLock.Unlock()
|
|
|
|
portal, ok := br.portalsByKey[key]
|
|
|
|
if !ok {
|
|
|
|
dbPortal, err := br.DB.Portal.GetByKey(context.TODO(), key)
|
|
|
|
if err != nil {
|
|
|
|
br.ZLog.Err(err).Object("portal_key", key).Msg("Failed to get portal from database")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return br.loadDBPortal(dbPortal, &key)
|
|
|
|
}
|
|
|
|
return portal
|
|
|
|
}
|
|
|
|
|
2023-09-04 22:34:47 +00:00
|
|
|
func (br *GMBridge) GetPortalByOtherUser(key database.Key) *Portal {
|
|
|
|
br.portalsLock.Lock()
|
|
|
|
defer br.portalsLock.Unlock()
|
|
|
|
portal, ok := br.portalsByOtherUser[key]
|
|
|
|
if !ok {
|
2023-09-04 23:05:01 +00:00
|
|
|
dbPortal, err := br.DB.Portal.GetByOtherUser(context.TODO(), key)
|
2023-09-04 22:34:47 +00:00
|
|
|
if err != nil {
|
|
|
|
br.ZLog.Err(err).Object("portal_key", key).Msg("Failed to get portal from database")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
if dbPortal != nil {
|
|
|
|
existingPortal, ok := br.portalsByKey[dbPortal.Key]
|
|
|
|
if ok {
|
|
|
|
return existingPortal
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return br.loadDBPortal(dbPortal, nil)
|
|
|
|
}
|
|
|
|
return portal
|
|
|
|
}
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
func (br *GMBridge) GetExistingPortalByKey(key database.Key) *Portal {
|
|
|
|
br.portalsLock.Lock()
|
|
|
|
defer br.portalsLock.Unlock()
|
|
|
|
portal, ok := br.portalsByKey[key]
|
|
|
|
if !ok {
|
|
|
|
dbPortal, err := br.DB.Portal.GetByKey(context.TODO(), key)
|
|
|
|
if err != nil {
|
|
|
|
br.ZLog.Err(err).Object("portal_key", key).Msg("Failed to get portal from database")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return br.loadDBPortal(dbPortal, nil)
|
|
|
|
}
|
|
|
|
return portal
|
|
|
|
}
|
|
|
|
|
|
|
|
func (br *GMBridge) GetAllPortals() []*Portal {
|
|
|
|
return br.loadManyPortals(br.DB.Portal.GetAll)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (br *GMBridge) GetAllPortalsForUser(userID int) []*Portal {
|
|
|
|
return br.loadManyPortals(func(ctx context.Context) ([]*database.Portal, error) {
|
|
|
|
return br.DB.Portal.GetAllForUser(ctx, userID)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (br *GMBridge) GetAllIPortals() (iportals []bridge.Portal) {
|
|
|
|
portals := br.GetAllPortals()
|
|
|
|
iportals = make([]bridge.Portal, len(portals))
|
|
|
|
for i, portal := range portals {
|
|
|
|
iportals[i] = portal
|
|
|
|
}
|
|
|
|
return iportals
|
|
|
|
}
|
|
|
|
|
|
|
|
func (br *GMBridge) loadManyPortals(query func(ctx context.Context) ([]*database.Portal, error)) []*Portal {
|
|
|
|
br.portalsLock.Lock()
|
|
|
|
defer br.portalsLock.Unlock()
|
|
|
|
dbPortals, err := query(context.TODO())
|
|
|
|
if err != nil {
|
|
|
|
br.ZLog.Err(err).Msg("Failed to load all portals from database")
|
|
|
|
return []*Portal{}
|
|
|
|
}
|
|
|
|
output := make([]*Portal, len(dbPortals))
|
|
|
|
for index, dbPortal := range dbPortals {
|
|
|
|
if dbPortal == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
portal, ok := br.portalsByKey[dbPortal.Key]
|
|
|
|
if !ok {
|
|
|
|
portal = br.loadDBPortal(dbPortal, nil)
|
|
|
|
}
|
|
|
|
output[index] = portal
|
|
|
|
}
|
|
|
|
return output
|
|
|
|
}
|
|
|
|
|
|
|
|
func (br *GMBridge) loadDBPortal(dbPortal *database.Portal, key *database.Key) *Portal {
|
|
|
|
if dbPortal == nil {
|
|
|
|
if key == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
dbPortal = br.DB.Portal.New()
|
|
|
|
dbPortal.Key = *key
|
|
|
|
err := dbPortal.Insert(context.TODO())
|
|
|
|
if err != nil {
|
|
|
|
br.ZLog.Err(err).Object("portal_key", key).Msg("Failed to insert portal")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
portal := br.NewPortal(dbPortal)
|
|
|
|
br.portalsByKey[portal.Key] = portal
|
|
|
|
if len(portal.MXID) > 0 {
|
|
|
|
br.portalsByMXID[portal.MXID] = portal
|
|
|
|
}
|
2023-09-04 22:34:47 +00:00
|
|
|
if len(portal.OtherUserID) > 0 {
|
|
|
|
br.portalsByOtherUser[database.Key{ID: portal.OtherUserID, Receiver: portal.Receiver}] = portal
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
return portal
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) GetUsers() []*User {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-08-02 12:01:50 +00:00
|
|
|
func (portal *Portal) updateLogger() {
|
|
|
|
portal.zlog = portal.bridge.ZLog.With().
|
|
|
|
Str("portal_id", portal.ID).
|
|
|
|
Int("portal_receiver", portal.Receiver).
|
|
|
|
Str("room_id", portal.MXID.String()).
|
|
|
|
Logger()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (br *GMBridge) NewPortal(dbPortal *database.Portal) *Portal {
|
2023-07-02 14:21:55 +00:00
|
|
|
portal := &Portal{
|
2023-08-02 12:01:50 +00:00
|
|
|
Portal: dbPortal,
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
bridge: br,
|
|
|
|
|
|
|
|
messages: make(chan PortalMessage, br.Config.Bridge.PortalMessageBuffer),
|
|
|
|
matrixMessages: make(chan PortalMatrixMessage, br.Config.Bridge.PortalMessageBuffer),
|
|
|
|
|
2023-07-09 21:34:44 +00:00
|
|
|
outgoingMessages: make(map[string]*outgoingMessage),
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-08-02 12:01:50 +00:00
|
|
|
portal.updateLogger()
|
2023-07-02 14:21:55 +00:00
|
|
|
go portal.handleMessageLoop()
|
2024-06-04 17:55:12 +00:00
|
|
|
go portal.outgoingMessageTimeoutLoop()
|
2023-07-02 14:21:55 +00:00
|
|
|
return portal
|
|
|
|
}
|
|
|
|
|
|
|
|
const recentlyHandledLength = 100
|
|
|
|
|
|
|
|
type PortalMessage struct {
|
2023-07-17 13:51:31 +00:00
|
|
|
evt *gmproto.Message
|
2023-07-02 14:21:55 +00:00
|
|
|
source *User
|
2023-08-24 11:48:03 +00:00
|
|
|
raw []byte
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type PortalMatrixMessage struct {
|
|
|
|
evt *event.Event
|
|
|
|
user *User
|
|
|
|
receivedAt time.Time
|
|
|
|
}
|
|
|
|
|
2023-07-09 21:34:44 +00:00
|
|
|
type outgoingMessage struct {
|
|
|
|
*event.Event
|
2024-06-08 09:47:41 +00:00
|
|
|
AckedAt time.Time
|
2024-05-24 09:42:28 +00:00
|
|
|
Errored bool
|
|
|
|
Timeouted bool
|
2023-07-09 21:34:44 +00:00
|
|
|
}
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
type Portal struct {
|
|
|
|
*database.Portal
|
|
|
|
|
|
|
|
bridge *GMBridge
|
2023-07-19 21:01:05 +00:00
|
|
|
zlog zerolog.Logger
|
2023-07-02 14:21:55 +00:00
|
|
|
|
|
|
|
roomCreateLock sync.Mutex
|
|
|
|
encryptLock sync.Mutex
|
|
|
|
backfillLock sync.Mutex
|
|
|
|
avatarLock sync.Mutex
|
|
|
|
|
2023-07-11 22:57:07 +00:00
|
|
|
forwardBackfillLock sync.Mutex
|
|
|
|
lastMessageTS time.Time
|
2023-08-09 14:53:11 +00:00
|
|
|
lastUserReadID string
|
|
|
|
hasSyncedThisRun bool
|
2023-07-02 14:21:55 +00:00
|
|
|
|
2023-08-08 14:05:56 +00:00
|
|
|
pendingRecentBackfill atomic.Pointer[pendingBackfill]
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
recentlyHandled [recentlyHandledLength]string
|
|
|
|
recentlyHandledLock sync.Mutex
|
|
|
|
recentlyHandledIndex uint8
|
|
|
|
|
2023-07-09 21:34:44 +00:00
|
|
|
outgoingMessages map[string]*outgoingMessage
|
2023-07-02 14:21:55 +00:00
|
|
|
outgoingMessagesLock sync.Mutex
|
|
|
|
|
|
|
|
currentlyTyping []id.UserID
|
|
|
|
currentlyTypingLock sync.Mutex
|
|
|
|
|
|
|
|
messages chan PortalMessage
|
|
|
|
matrixMessages chan PortalMatrixMessage
|
2023-08-21 16:42:57 +00:00
|
|
|
|
|
|
|
cancelCreation atomic.Pointer[context.CancelCauseFunc]
|
2024-04-16 12:48:00 +00:00
|
|
|
markedSpamAt time.Time
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
2023-07-15 11:38:24 +00:00
|
|
|
_ bridge.Portal = (*Portal)(nil)
|
|
|
|
_ bridge.ReadReceiptHandlingPortal = (*Portal)(nil)
|
2023-07-02 14:21:55 +00:00
|
|
|
//_ bridge.MembershipHandlingPortal = (*Portal)(nil)
|
|
|
|
//_ bridge.MetaHandlingPortal = (*Portal)(nil)
|
|
|
|
//_ bridge.TypingPortal = (*Portal)(nil)
|
|
|
|
)
|
|
|
|
|
|
|
|
func (portal *Portal) handleMessageLoopItem(msg PortalMessage) {
|
|
|
|
if len(portal.MXID) == 0 {
|
2024-02-29 11:38:50 +00:00
|
|
|
portal.zlog.Warn().Str("message_id", msg.evt.MessageID).Msg("Dropping message as portal is not yet created")
|
2023-07-02 14:21:55 +00:00
|
|
|
return
|
|
|
|
}
|
2023-07-11 22:57:07 +00:00
|
|
|
portal.forwardBackfillLock.Lock()
|
|
|
|
defer portal.forwardBackfillLock.Unlock()
|
2023-07-02 14:21:55 +00:00
|
|
|
switch {
|
|
|
|
case msg.evt != nil:
|
2024-03-20 11:05:25 +00:00
|
|
|
doneChan := make(chan struct{})
|
|
|
|
go func() {
|
|
|
|
defer close(doneChan)
|
|
|
|
portal.handleMessage(msg.source, msg.evt, msg.raw)
|
|
|
|
}()
|
|
|
|
timer := time.NewTimer(1 * time.Minute)
|
|
|
|
select {
|
|
|
|
case <-doneChan:
|
|
|
|
if !timer.Stop() {
|
|
|
|
<-timer.C
|
|
|
|
}
|
|
|
|
case <-timer.C:
|
|
|
|
portal.zlog.Error().
|
|
|
|
Str("message_id", msg.evt.MessageID).
|
|
|
|
Msg("Google Messages event handling is taking over a minute, unblocking loop")
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
default:
|
|
|
|
portal.zlog.Warn().Interface("portal_message", msg).Msg("Unexpected PortalMessage with no message")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) handleMatrixMessageLoopItem(msg PortalMatrixMessage) {
|
2023-09-07 14:21:15 +00:00
|
|
|
if msg.user.RowID != portal.Receiver {
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.sendMessageMetrics(context.TODO(), msg.user, msg.evt, errIncorrectUser, "Ignoring", nil)
|
2023-09-07 14:21:15 +00:00
|
|
|
return
|
|
|
|
} else if msg.user.Client == nil {
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.sendMessageMetrics(context.TODO(), msg.user, msg.evt, errNotLoggedIn, "Ignoring", nil)
|
2023-09-07 14:21:15 +00:00
|
|
|
return
|
|
|
|
}
|
2023-07-11 22:57:07 +00:00
|
|
|
portal.forwardBackfillLock.Lock()
|
|
|
|
defer portal.forwardBackfillLock.Unlock()
|
2023-07-02 14:21:55 +00:00
|
|
|
evtTS := time.UnixMilli(msg.evt.Timestamp)
|
|
|
|
timings := messageTimings{
|
|
|
|
initReceive: msg.evt.Mautrix.ReceivedAt.Sub(evtTS),
|
|
|
|
decrypt: msg.evt.Mautrix.DecryptionDuration,
|
|
|
|
portalQueue: time.Since(msg.receivedAt),
|
|
|
|
totalReceive: time.Since(evtTS),
|
|
|
|
}
|
2024-03-20 11:05:25 +00:00
|
|
|
doneChan := make(chan struct{})
|
|
|
|
go func() {
|
|
|
|
defer close(doneChan)
|
|
|
|
switch msg.evt.Type {
|
|
|
|
case event.EventMessage, event.EventSticker:
|
|
|
|
portal.HandleMatrixMessage(msg.user, msg.evt, timings)
|
|
|
|
case event.EventReaction:
|
|
|
|
portal.HandleMatrixReaction(msg.user, msg.evt)
|
|
|
|
case event.EventRedaction:
|
|
|
|
portal.HandleMatrixRedaction(msg.user, msg.evt)
|
|
|
|
default:
|
|
|
|
portal.zlog.Warn().
|
|
|
|
Str("event_type", msg.evt.Type.Type).
|
|
|
|
Msg("Unsupported event type in portal message channel")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
timer := time.NewTimer(1 * time.Minute)
|
|
|
|
select {
|
|
|
|
case <-doneChan:
|
|
|
|
if !timer.Stop() {
|
|
|
|
<-timer.C
|
|
|
|
}
|
|
|
|
case <-timer.C:
|
|
|
|
portal.zlog.Error().
|
|
|
|
Stringer("event_id", msg.evt.ID).
|
|
|
|
Msg("Matrix event handling is taking over a minute, unblocking loop")
|
2024-05-24 09:49:16 +00:00
|
|
|
go portal.bridge.SendMessageCheckpoint(msg.evt, status.MsgStepRemote, errHandlingTakingLong, status.MsgStatusTimeout, 0)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) handleMessageLoop() {
|
|
|
|
for {
|
2024-06-04 17:55:12 +00:00
|
|
|
portal.handleOneMessageLoopItem()
|
2023-09-04 08:51:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-04 17:55:12 +00:00
|
|
|
func (portal *Portal) outgoingMessageTimeoutLoop() {
|
|
|
|
ticker := time.NewTicker(1 * time.Minute)
|
|
|
|
for range ticker.C {
|
|
|
|
portal.outgoingMessagesLock.Lock()
|
|
|
|
for _, out := range portal.outgoingMessages {
|
2024-06-08 09:47:41 +00:00
|
|
|
if !out.Timeouted && !out.AckedAt.IsZero() && !out.Errored && time.Since(out.AckedAt) > 1*time.Minute {
|
2024-06-04 17:55:12 +00:00
|
|
|
go portal.sendStatusEvent(context.TODO(), out.ID, "", errEchoTimeout, nil)
|
|
|
|
go portal.sendErrorMessage(context.TODO(), out.Event, errEchoTimeout, "message", false, "")
|
|
|
|
go portal.bridge.SendMessageCheckpoint(out.Event, status.MsgStepRemote, errEchoTimeout, status.MsgStatusTimeout, 0)
|
|
|
|
out.Timeouted = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
portal.outgoingMessagesLock.Unlock()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) handleOneMessageLoopItem() {
|
2023-09-04 08:51:52 +00:00
|
|
|
defer func() {
|
|
|
|
if err := recover(); err != nil {
|
|
|
|
logEvt := portal.zlog.WithLevel(zerolog.FatalLevel).
|
|
|
|
Str(zerolog.ErrorStackFieldName, string(debug.Stack()))
|
|
|
|
actualErr, ok := err.(error)
|
|
|
|
if ok {
|
|
|
|
logEvt = logEvt.Err(actualErr)
|
|
|
|
} else {
|
|
|
|
logEvt = logEvt.Any(zerolog.ErrorFieldName, err)
|
|
|
|
}
|
|
|
|
logEvt.Msg("Portal message handler panicked")
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-09-04 08:51:52 +00:00
|
|
|
}()
|
|
|
|
select {
|
|
|
|
case msg := <-portal.messages:
|
|
|
|
portal.handleMessageLoopItem(msg)
|
|
|
|
case msg := <-portal.matrixMessages:
|
|
|
|
portal.handleMatrixMessageLoopItem(msg)
|
2024-05-24 09:42:28 +00:00
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
2023-08-08 15:45:48 +00:00
|
|
|
func (portal *Portal) isOutgoingMessage(msg *gmproto.Message) *database.Message {
|
2023-07-02 14:21:55 +00:00
|
|
|
portal.outgoingMessagesLock.Lock()
|
|
|
|
defer portal.outgoingMessagesLock.Unlock()
|
2023-07-09 21:34:44 +00:00
|
|
|
out, ok := portal.outgoingMessages[msg.TmpID]
|
2023-07-02 14:21:55 +00:00
|
|
|
if ok {
|
2023-08-08 15:45:48 +00:00
|
|
|
delete(portal.outgoingMessages, msg.TmpID)
|
|
|
|
return portal.markHandled(&ConvertedMessage{
|
|
|
|
ID: msg.MessageID,
|
|
|
|
Timestamp: time.UnixMicro(msg.GetTimestamp()),
|
|
|
|
SenderID: msg.ParticipantID,
|
2023-08-09 16:49:36 +00:00
|
|
|
PartCount: len(msg.GetMessageInfo()),
|
2023-08-09 14:53:11 +00:00
|
|
|
}, out.ID, nil, true)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-08-08 15:45:48 +00:00
|
|
|
return nil
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
|
2023-07-17 13:51:31 +00:00
|
|
|
func hasInProgressMedia(msg *gmproto.Message) bool {
|
2023-07-09 17:28:20 +00:00
|
|
|
for _, part := range msg.MessageInfo {
|
2023-07-17 13:51:31 +00:00
|
|
|
media, ok := part.GetData().(*gmproto.MessageInfo_MediaContent)
|
2023-09-02 09:09:59 +00:00
|
|
|
if ok && media.MediaContent.GetMediaID() == "" && media.MediaContent.GetMediaID2() == "" {
|
2023-07-09 17:28:20 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-08-08 15:45:48 +00:00
|
|
|
func isSuccessfullySentStatus(status gmproto.MessageStatusType) bool {
|
|
|
|
switch status {
|
|
|
|
case gmproto.MessageStatusType_OUTGOING_DELIVERED, gmproto.MessageStatusType_OUTGOING_COMPLETE, gmproto.MessageStatusType_OUTGOING_DISPLAYED:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-09 14:53:11 +00:00
|
|
|
func downloadPendingStatusMessage(status gmproto.MessageStatusType) string {
|
|
|
|
switch status {
|
|
|
|
case gmproto.MessageStatusType_INCOMING_YET_TO_MANUAL_DOWNLOAD:
|
2023-08-14 11:32:12 +00:00
|
|
|
return "Attachment message (auto-download is disabled, use Messages on Android to download)"
|
2023-08-09 14:53:11 +00:00
|
|
|
case gmproto.MessageStatusType_INCOMING_MANUAL_DOWNLOADING,
|
|
|
|
gmproto.MessageStatusType_INCOMING_AUTO_DOWNLOADING,
|
|
|
|
gmproto.MessageStatusType_INCOMING_RETRYING_MANUAL_DOWNLOAD,
|
|
|
|
gmproto.MessageStatusType_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
|
|
|
return "Downloading message..."
|
|
|
|
case gmproto.MessageStatusType_INCOMING_DOWNLOAD_FAILED:
|
|
|
|
return "Message download failed"
|
|
|
|
case gmproto.MessageStatusType_INCOMING_DOWNLOAD_FAILED_TOO_LARGE:
|
|
|
|
return "Message download failed (too large)"
|
|
|
|
case gmproto.MessageStatusType_INCOMING_DOWNLOAD_FAILED_SIM_HAS_NO_DATA:
|
|
|
|
return "Message download failed (no mobile data connection)"
|
|
|
|
case gmproto.MessageStatusType_INCOMING_DOWNLOAD_CANCELED:
|
|
|
|
return "Message download canceled"
|
|
|
|
default:
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-08 15:45:48 +00:00
|
|
|
func isFailSendStatus(status gmproto.MessageStatusType) bool {
|
|
|
|
switch status {
|
|
|
|
case gmproto.MessageStatusType_OUTGOING_FAILED_GENERIC,
|
|
|
|
gmproto.MessageStatusType_OUTGOING_FAILED_EMERGENCY_NUMBER,
|
|
|
|
gmproto.MessageStatusType_OUTGOING_CANCELED,
|
|
|
|
gmproto.MessageStatusType_OUTGOING_FAILED_TOO_LARGE,
|
|
|
|
gmproto.MessageStatusType_OUTGOING_FAILED_RECIPIENT_LOST_RCS,
|
|
|
|
gmproto.MessageStatusType_OUTGOING_FAILED_NO_RETRY_NO_FALLBACK,
|
|
|
|
gmproto.MessageStatusType_OUTGOING_FAILED_RECIPIENT_DID_NOT_DECRYPT,
|
|
|
|
gmproto.MessageStatusType_OUTGOING_FAILED_RECIPIENT_LOST_ENCRYPTION,
|
|
|
|
gmproto.MessageStatusType_OUTGOING_FAILED_RECIPIENT_DID_NOT_DECRYPT_NO_MORE_RETRY:
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 14:41:45 +00:00
|
|
|
func downloadStatusRank(status gmproto.MessageStatusType) int {
|
|
|
|
switch status {
|
|
|
|
case gmproto.MessageStatusType_INCOMING_AUTO_DOWNLOADING:
|
|
|
|
return 0
|
|
|
|
case gmproto.MessageStatusType_INCOMING_MANUAL_DOWNLOADING,
|
|
|
|
gmproto.MessageStatusType_INCOMING_RETRYING_AUTO_DOWNLOAD,
|
|
|
|
gmproto.MessageStatusType_INCOMING_DOWNLOAD_FAILED,
|
|
|
|
gmproto.MessageStatusType_INCOMING_YET_TO_MANUAL_DOWNLOAD,
|
|
|
|
gmproto.MessageStatusType_INCOMING_RETRYING_MANUAL_DOWNLOAD,
|
|
|
|
gmproto.MessageStatusType_INCOMING_DOWNLOAD_FAILED_SIM_HAS_NO_DATA,
|
|
|
|
gmproto.MessageStatusType_INCOMING_DOWNLOAD_FAILED_TOO_LARGE,
|
|
|
|
gmproto.MessageStatusType_INCOMING_DOWNLOAD_CANCELED:
|
|
|
|
return 1
|
|
|
|
default:
|
|
|
|
return 100
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-14 11:32:12 +00:00
|
|
|
func (portal *Portal) redactMessage(ctx context.Context, msg *database.Message) {
|
|
|
|
if msg.IsFakeMXID() {
|
|
|
|
return
|
|
|
|
}
|
2023-08-08 15:45:48 +00:00
|
|
|
log := zerolog.Ctx(ctx)
|
2023-08-14 11:32:12 +00:00
|
|
|
intent := portal.MainIntent()
|
|
|
|
if msg.Chat.ID != portal.ID {
|
|
|
|
otherPortal := portal.bridge.GetExistingPortalByKey(msg.Chat)
|
|
|
|
if otherPortal != nil {
|
|
|
|
intent = otherPortal.MainIntent()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for partID, part := range msg.Status.MediaParts {
|
|
|
|
if part.EventID != "" {
|
2024-02-23 19:10:31 +00:00
|
|
|
if _, err := intent.RedactEvent(ctx, msg.RoomID, part.EventID); err != nil {
|
2023-08-14 11:32:12 +00:00
|
|
|
log.Err(err).Str("part_id", partID).Msg("Failed to redact part of message")
|
|
|
|
}
|
|
|
|
part.EventID = ""
|
|
|
|
msg.Status.MediaParts[partID] = part
|
|
|
|
}
|
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
if _, err := intent.RedactEvent(ctx, msg.RoomID, msg.MXID); err != nil {
|
2023-08-14 11:32:12 +00:00
|
|
|
log.Err(err).Msg("Failed to redact message")
|
|
|
|
}
|
|
|
|
msg.MXID = ""
|
|
|
|
}
|
|
|
|
|
2023-08-24 11:48:03 +00:00
|
|
|
func (portal *Portal) handleExistingMessageUpdate(ctx context.Context, source *User, dbMsg *database.Message, evt *gmproto.Message, raw []byte) {
|
2023-08-14 11:32:12 +00:00
|
|
|
log := *zerolog.Ctx(ctx)
|
2023-08-08 15:45:48 +00:00
|
|
|
newStatus := evt.GetMessageStatus().GetStatus()
|
2023-09-07 14:41:45 +00:00
|
|
|
// Messages in different portals may have race conditions, ignore the most common case
|
|
|
|
// (group MMS event happens in DM after group).
|
|
|
|
if downloadStatusRank(newStatus) < downloadStatusRank(dbMsg.Status.Type) {
|
|
|
|
log.Debug().
|
|
|
|
Str("old_status", dbMsg.Status.Type.String()).
|
|
|
|
Str("new_status", newStatus.String()).
|
|
|
|
Msg("Ignoring message status change as it's a downgrade")
|
|
|
|
return
|
|
|
|
}
|
2023-08-14 11:32:12 +00:00
|
|
|
chatIDChanged := dbMsg.Chat.ID != portal.ID
|
2023-09-01 18:30:29 +00:00
|
|
|
hasPendingMedia := dbMsg.Status.HasPendingMediaParts()
|
|
|
|
updatedMediaIsComplete := !hasInProgressMedia(evt)
|
|
|
|
if dbMsg.Status.Type == newStatus && !chatIDChanged && !(hasPendingMedia && updatedMediaIsComplete) {
|
|
|
|
logEvt := log.Debug().
|
|
|
|
Str("old_status", dbMsg.Status.Type.String()).
|
|
|
|
Bool("has_pending_media", hasPendingMedia).
|
|
|
|
Bool("updated_media_is_complete", updatedMediaIsComplete)
|
|
|
|
if hasPendingMedia {
|
|
|
|
debugData := zerolog.Dict()
|
|
|
|
for _, part := range evt.MessageInfo {
|
|
|
|
media, ok := part.GetData().(*gmproto.MessageInfo_MediaContent)
|
|
|
|
if ok {
|
|
|
|
debugData.Dict(
|
|
|
|
part.GetActionMessageID(),
|
|
|
|
zerolog.Dict().
|
|
|
|
Str("media_id_1", media.MediaContent.GetMediaID()).
|
|
|
|
Str("media_id_2", media.MediaContent.GetMediaID2()).
|
|
|
|
Int64("size", media.MediaContent.GetSize()).
|
|
|
|
Int64("width", media.MediaContent.GetDimensions().GetWidth()).
|
|
|
|
Int64("height", media.MediaContent.GetDimensions().GetHeight()).
|
|
|
|
Bool("has_key_1", len(media.MediaContent.GetDecryptionKey()) > 0).
|
|
|
|
Bool("has_key_2", len(media.MediaContent.GetDecryptionKey2()) > 0).
|
|
|
|
Bool("has_unknown_fields", len(media.MediaContent.ProtoReflect().GetUnknown()) > 0),
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
debugData.Str(part.GetActionMessageID(), "not media")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
logEvt = logEvt.Dict("pending_media_debug_data", debugData)
|
|
|
|
}
|
|
|
|
logEvt.Msg("Nothing changed in message update, just syncing reactions")
|
2023-08-14 11:32:12 +00:00
|
|
|
portal.syncReactions(ctx, source, dbMsg, evt.Reactions)
|
2023-08-09 14:53:11 +00:00
|
|
|
return
|
|
|
|
}
|
2023-08-14 11:32:12 +00:00
|
|
|
if chatIDChanged {
|
|
|
|
log = log.With().Str("old_chat_id", dbMsg.Chat.ID).Logger()
|
2023-08-21 07:58:34 +00:00
|
|
|
if downloadPendingStatusMessage(newStatus) != "" && !portal.IsPrivateChat() {
|
|
|
|
log.Debug().Msg("Ignoring chat ID change from group chat as update is a pending download")
|
|
|
|
return
|
|
|
|
}
|
2023-08-14 11:32:12 +00:00
|
|
|
log.Debug().
|
|
|
|
Str("old_room_id", dbMsg.RoomID.String()).
|
|
|
|
Str("sender_id", dbMsg.Sender).
|
|
|
|
Msg("Redacting events from old room")
|
|
|
|
ctx = log.WithContext(ctx)
|
|
|
|
err := portal.bridge.DB.Reaction.DeleteAllByMessage(ctx, dbMsg.Chat, dbMsg.ID)
|
|
|
|
if err != nil {
|
|
|
|
log.Warn().Err(err).Msg("Failed to delete db reactions for message that moved to another room")
|
|
|
|
}
|
|
|
|
portal.redactMessage(ctx, dbMsg)
|
|
|
|
}
|
|
|
|
log.Debug().
|
|
|
|
Str("old_status", dbMsg.Status.Type.String()).
|
2023-09-02 10:29:48 +00:00
|
|
|
Bool("has_pending_media", hasPendingMedia).
|
|
|
|
Bool("updated_media_is_complete", updatedMediaIsComplete).
|
2023-08-14 11:32:12 +00:00
|
|
|
Msg("Message status changed")
|
2023-08-09 14:53:11 +00:00
|
|
|
switch {
|
|
|
|
case newStatus == gmproto.MessageStatusType_MESSAGE_DELETED:
|
2023-08-14 11:32:12 +00:00
|
|
|
portal.redactMessage(ctx, dbMsg)
|
|
|
|
if err := dbMsg.Delete(ctx); err != nil {
|
2023-08-09 14:53:11 +00:00
|
|
|
log.Err(err).Msg("Failed to delete message from database")
|
|
|
|
} else {
|
|
|
|
log.Debug().Msg("Handled message deletion")
|
|
|
|
}
|
|
|
|
return
|
2023-08-14 11:32:12 +00:00
|
|
|
case chatIDChanged,
|
|
|
|
dbMsg.Status.MediaStatus != downloadPendingStatusMessage(newStatus),
|
2023-09-01 18:30:29 +00:00
|
|
|
hasPendingMedia && updatedMediaIsComplete,
|
2023-08-09 16:49:36 +00:00
|
|
|
dbMsg.Status.PartCount != len(evt.MessageInfo):
|
2023-08-24 11:48:03 +00:00
|
|
|
converted := portal.convertGoogleMessage(ctx, source, evt, false, raw)
|
2023-09-11 17:09:08 +00:00
|
|
|
if converted == nil {
|
|
|
|
log.Warn().Msg("Didn't get converted parts for updated event")
|
|
|
|
return
|
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
dbMsg.Status.MediaStatus = converted.MediaStatus
|
|
|
|
if dbMsg.Status.MediaParts == nil {
|
|
|
|
dbMsg.Status.MediaParts = make(map[string]database.MediaPart)
|
|
|
|
}
|
|
|
|
eventIDs := make([]id.EventID, 0, len(converted.Parts))
|
|
|
|
for i, part := range converted.Parts {
|
|
|
|
isEdit := true
|
|
|
|
ts := time.Now().UnixMilli()
|
2023-08-14 11:32:12 +00:00
|
|
|
if chatIDChanged {
|
|
|
|
isEdit = false
|
|
|
|
} else if i == 0 {
|
2023-09-04 08:33:03 +00:00
|
|
|
part.SetEdit(dbMsg.MXID)
|
2023-08-09 14:53:11 +00:00
|
|
|
} else if existingPart, ok := dbMsg.Status.MediaParts[part.ID]; ok {
|
2023-09-04 08:33:03 +00:00
|
|
|
part.SetEdit(existingPart.EventID)
|
2023-08-09 14:53:11 +00:00
|
|
|
} else {
|
|
|
|
ts = converted.Timestamp.UnixMilli()
|
|
|
|
isEdit = false
|
2023-08-08 15:45:48 +00:00
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
resp, err := portal.sendMessage(ctx, converted.Intent, event.EventMessage, part.Content, part.Extra, ts)
|
2023-08-08 15:45:48 +00:00
|
|
|
if err != nil {
|
2023-08-09 14:53:11 +00:00
|
|
|
log.Err(err).Msg("Failed to send message")
|
2023-09-27 12:51:05 +00:00
|
|
|
continue
|
2023-08-09 14:53:11 +00:00
|
|
|
} else {
|
|
|
|
eventIDs = append(eventIDs, resp.EventID)
|
|
|
|
}
|
|
|
|
if i == 0 {
|
2023-08-14 11:32:12 +00:00
|
|
|
if chatIDChanged {
|
|
|
|
dbMsg.MXID = resp.EventID
|
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
dbMsg.Status.MediaParts[""] = database.MediaPart{PendingMedia: part.PendingMedia}
|
|
|
|
} else if !isEdit {
|
|
|
|
dbMsg.Status.MediaParts[part.ID] = database.MediaPart{EventID: resp.EventID, PendingMedia: part.PendingMedia}
|
2023-08-08 15:45:48 +00:00
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
}
|
|
|
|
if len(eventIDs) > 0 {
|
2024-02-23 19:10:31 +00:00
|
|
|
portal.sendDeliveryReceipt(ctx, eventIDs[len(eventIDs)-1])
|
2023-08-09 14:53:11 +00:00
|
|
|
log.Debug().Interface("event_ids", eventIDs).Msg("Handled update to message")
|
|
|
|
}
|
|
|
|
case !dbMsg.Status.ReadReceiptSent && portal.IsPrivateChat() && newStatus == gmproto.MessageStatusType_OUTGOING_DISPLAYED:
|
|
|
|
dbMsg.Status.ReadReceiptSent = true
|
2023-08-18 17:43:46 +00:00
|
|
|
if !dbMsg.Status.MSSSent {
|
|
|
|
portal.sendCheckpoint(dbMsg, nil, false)
|
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
if !dbMsg.Status.MSSDeliverySent {
|
2023-08-08 15:45:48 +00:00
|
|
|
dbMsg.Status.MSSDeliverySent = true
|
|
|
|
dbMsg.Status.MSSSent = true
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.sendStatusEvent(ctx, dbMsg.MXID, "", nil, &[]id.UserID{portal.MainIntent().UserID})
|
2023-08-18 17:43:46 +00:00
|
|
|
portal.sendCheckpoint(dbMsg, nil, true)
|
2023-08-08 15:45:48 +00:00
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
err := portal.MainIntent().MarkRead(ctx, portal.MXID, dbMsg.MXID)
|
2023-08-08 15:45:48 +00:00
|
|
|
if err != nil {
|
2023-08-09 14:53:11 +00:00
|
|
|
log.Warn().Err(err).Msg("Failed to mark message as read")
|
|
|
|
}
|
|
|
|
case !dbMsg.Status.MSSDeliverySent && portal.IsPrivateChat() && newStatus == gmproto.MessageStatusType_OUTGOING_DELIVERED:
|
2023-08-18 17:43:46 +00:00
|
|
|
if !dbMsg.Status.MSSSent {
|
|
|
|
portal.sendCheckpoint(dbMsg, nil, false)
|
|
|
|
}
|
|
|
|
portal.sendCheckpoint(dbMsg, nil, true)
|
2023-08-09 14:53:11 +00:00
|
|
|
dbMsg.Status.MSSDeliverySent = true
|
|
|
|
dbMsg.Status.MSSSent = true
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.sendStatusEvent(ctx, dbMsg.MXID, "", nil, &[]id.UserID{portal.MainIntent().UserID})
|
2023-08-09 14:53:11 +00:00
|
|
|
case !dbMsg.Status.MSSSent && isSuccessfullySentStatus(newStatus):
|
|
|
|
dbMsg.Status.MSSSent = true
|
|
|
|
var deliveredTo *[]id.UserID
|
|
|
|
// TODO SMSes can enable delivery receipts too, but can it be detected?
|
|
|
|
if portal.IsPrivateChat() && portal.Type == gmproto.ConversationType_RCS {
|
|
|
|
deliveredTo = &[]id.UserID{}
|
2023-08-08 15:45:48 +00:00
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.sendStatusEvent(ctx, dbMsg.MXID, "", nil, deliveredTo)
|
2023-08-18 17:43:46 +00:00
|
|
|
portal.sendCheckpoint(dbMsg, nil, false)
|
2023-08-09 14:53:11 +00:00
|
|
|
case !dbMsg.Status.MSSFailSent && !dbMsg.Status.MSSSent && isFailSendStatus(newStatus):
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.sendStatusEvent(ctx, dbMsg.MXID, "", OutgoingStatusError(newStatus), nil)
|
2023-08-18 17:43:46 +00:00
|
|
|
portal.sendCheckpoint(dbMsg, OutgoingStatusError(newStatus), false)
|
2023-08-09 14:53:11 +00:00
|
|
|
// TODO error notice
|
|
|
|
default:
|
|
|
|
log.Debug().Msg("Ignored message update")
|
|
|
|
// TODO do something?
|
|
|
|
}
|
|
|
|
dbMsg.Status.Type = newStatus
|
2023-08-09 16:49:36 +00:00
|
|
|
dbMsg.Status.PartCount = len(evt.MessageInfo)
|
2023-08-09 14:53:11 +00:00
|
|
|
dbMsg.Timestamp = time.UnixMicro(evt.GetTimestamp())
|
2023-08-14 11:32:12 +00:00
|
|
|
var err error
|
|
|
|
if chatIDChanged {
|
|
|
|
dbMsg.Chat = portal.Key
|
|
|
|
dbMsg.RoomID = portal.MXID
|
|
|
|
err = dbMsg.Update(ctx)
|
|
|
|
} else {
|
|
|
|
err = dbMsg.UpdateStatus(ctx)
|
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Warn().Err(err).Msg("Failed to save updated message status to database")
|
2023-08-08 15:45:48 +00:00
|
|
|
}
|
2023-08-14 11:32:12 +00:00
|
|
|
portal.syncReactions(ctx, source, dbMsg, evt.Reactions)
|
2023-08-08 15:45:48 +00:00
|
|
|
}
|
|
|
|
|
2023-08-24 11:48:03 +00:00
|
|
|
func (portal *Portal) handleExistingMessage(ctx context.Context, source *User, evt *gmproto.Message, outgoingOnly bool, raw []byte) bool {
|
2023-08-10 08:35:20 +00:00
|
|
|
log := zerolog.Ctx(ctx)
|
|
|
|
if existingMsg := portal.isOutgoingMessage(evt); existingMsg != nil {
|
|
|
|
log.Debug().Str("event_id", existingMsg.MXID.String()).Msg("Got echo for outgoing message")
|
2023-08-24 11:48:03 +00:00
|
|
|
portal.handleExistingMessageUpdate(ctx, source, existingMsg, evt, raw)
|
2023-08-10 08:35:20 +00:00
|
|
|
return true
|
|
|
|
} else if outgoingOnly {
|
|
|
|
return false
|
|
|
|
}
|
2023-08-14 11:32:12 +00:00
|
|
|
existingMsg, err := portal.bridge.DB.Message.GetByID(ctx, portal.Receiver, evt.MessageID)
|
2023-08-10 08:35:20 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to check if message is duplicate")
|
|
|
|
} else if existingMsg != nil {
|
2023-08-24 11:48:03 +00:00
|
|
|
portal.handleExistingMessageUpdate(ctx, source, existingMsg, evt, raw)
|
2023-08-10 08:35:20 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-08-10 13:25:42 +00:00
|
|
|
func idToInt(id string) int {
|
|
|
|
i, err := strconv.Atoi(id)
|
|
|
|
if err != nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
return i
|
|
|
|
}
|
|
|
|
|
2023-08-24 11:48:03 +00:00
|
|
|
func (portal *Portal) handleMessage(source *User, evt *gmproto.Message, raw []byte) {
|
2023-07-11 22:57:07 +00:00
|
|
|
eventTS := time.UnixMicro(evt.GetTimestamp())
|
2023-07-12 21:44:57 +00:00
|
|
|
if eventTS.After(portal.lastMessageTS) {
|
|
|
|
portal.lastMessageTS = eventTS
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
log := portal.zlog.With().
|
|
|
|
Str("message_id", evt.MessageID).
|
|
|
|
Str("participant_id", evt.ParticipantID).
|
2023-07-14 22:22:20 +00:00
|
|
|
Str("status", evt.GetMessageStatus().GetStatus().String()).
|
2023-08-10 13:25:42 +00:00
|
|
|
Time("message_timestamp", eventTS).
|
2023-07-14 22:22:20 +00:00
|
|
|
Str("action", "handle google message").
|
2023-07-02 14:21:55 +00:00
|
|
|
Logger()
|
2023-07-09 17:48:27 +00:00
|
|
|
ctx := log.WithContext(context.TODO())
|
2023-08-24 11:48:03 +00:00
|
|
|
if portal.handleExistingMessage(ctx, source, evt, false, raw) {
|
2023-07-02 14:21:55 +00:00
|
|
|
return
|
|
|
|
}
|
2023-08-13 14:10:37 +00:00
|
|
|
switch evt.GetMessageStatus().GetStatus() {
|
|
|
|
case gmproto.MessageStatusType_MESSAGE_DELETED:
|
2023-08-09 14:53:11 +00:00
|
|
|
log.Debug().Msg("Not handling unknown deleted message")
|
|
|
|
return
|
2023-08-13 14:10:37 +00:00
|
|
|
case gmproto.MessageStatusType_INCOMING_AUTO_DOWNLOADING, gmproto.MessageStatusType_INCOMING_RETRYING_AUTO_DOWNLOAD:
|
|
|
|
log.Debug().Msg("Not handling incoming auto-downloading MMS")
|
|
|
|
return
|
2024-06-13 17:46:18 +00:00
|
|
|
case gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_PROTOCOL_SWITCH_RCS_TO_E2EE, gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_PROTOCOL_SWITCH_TEXT_TO_E2EE:
|
|
|
|
if !portal.ForceRCS {
|
|
|
|
portal.ForceRCS = true
|
|
|
|
err := portal.Update(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.Warn().Err(err).Msg("Failed to update portal to force RCS")
|
|
|
|
} else {
|
|
|
|
log.Debug().Msg("Setting force RCS flag for portal due to E2EE tombstone")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_PROTOCOL_SWITCH_E2EE_TO_RCS, gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_PROTOCOL_SWITCH_E2EE_TO_TEXT:
|
|
|
|
if portal.ForceRCS {
|
|
|
|
portal.ForceRCS = false
|
|
|
|
err := portal.Update(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.Warn().Err(err).Msg("Failed to update portal to unforce RCS")
|
|
|
|
} else {
|
|
|
|
log.Debug().Msg("Removing force RCS flag for portal due to non-E2EE tombstone")
|
|
|
|
}
|
|
|
|
}
|
2023-08-13 14:10:37 +00:00
|
|
|
}
|
2024-04-08 12:21:08 +00:00
|
|
|
if time.Since(eventTS) > 24*time.Hour {
|
2023-08-10 13:25:42 +00:00
|
|
|
lastMessage, err := portal.bridge.DB.Message.GetLastInChat(ctx, portal.Key)
|
|
|
|
if err != nil {
|
|
|
|
log.Warn().Err(err).Msg("Failed to get last message to check if received old message is too old")
|
2024-04-08 12:21:08 +00:00
|
|
|
} else if lastMessage != nil && lastMessage.Timestamp.After(eventTS) {
|
|
|
|
if idToInt(lastMessage.ID) > idToInt(evt.MessageID) {
|
|
|
|
log.Warn().
|
|
|
|
Str("last_message_id", lastMessage.ID).
|
|
|
|
Time("last_message_ts", lastMessage.Timestamp).
|
|
|
|
Msg("Not handling old message even though it has higher ID than last new one")
|
|
|
|
} else {
|
|
|
|
log.Debug().
|
|
|
|
Str("last_message_id", lastMessage.ID).
|
|
|
|
Time("last_message_ts", lastMessage.Timestamp).
|
|
|
|
Msg("Not handling old message")
|
|
|
|
}
|
2023-08-10 13:25:42 +00:00
|
|
|
return
|
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
|
2023-08-24 11:48:03 +00:00
|
|
|
converted := portal.convertGoogleMessage(ctx, source, evt, false, raw)
|
2023-08-10 08:30:17 +00:00
|
|
|
eventIDs := portal.sendMessageParts(ctx, converted, nil)
|
|
|
|
if len(eventIDs) > 0 {
|
2024-02-23 19:10:31 +00:00
|
|
|
portal.sendDeliveryReceipt(ctx, eventIDs[len(eventIDs)-1])
|
2023-08-10 08:30:17 +00:00
|
|
|
log.Debug().Interface("event_ids", eventIDs).Msg("Handled message")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) sendMessageParts(ctx context.Context, converted *ConvertedMessage, replyToMap map[string]id.EventID) []id.EventID {
|
2023-07-11 22:57:07 +00:00
|
|
|
if converted == nil {
|
2023-08-10 08:30:17 +00:00
|
|
|
return nil
|
2023-08-09 14:53:11 +00:00
|
|
|
} else if len(converted.Parts) == 0 {
|
2023-08-10 08:30:17 +00:00
|
|
|
zerolog.Ctx(ctx).Debug().Msg("Didn't get any converted parts from message")
|
|
|
|
return nil
|
|
|
|
} else if converted.DontBridge {
|
|
|
|
zerolog.Ctx(ctx).Debug().Msg("Ignored incoming tombstone message")
|
|
|
|
portal.markHandled(converted, id.EventID(fmt.Sprintf("$fake::%s", random.String(37))), nil, true)
|
|
|
|
return nil
|
2023-07-11 22:57:07 +00:00
|
|
|
}
|
|
|
|
eventIDs := make([]id.EventID, 0, len(converted.Parts))
|
2023-08-09 14:53:11 +00:00
|
|
|
mediaParts := make(map[string]database.MediaPart, len(converted.Parts)-1)
|
2023-08-10 08:30:17 +00:00
|
|
|
for i, part := range converted.Parts {
|
|
|
|
if replyToMap != nil && converted.ReplyTo != "" && part.Content.RelatesTo == nil {
|
|
|
|
replyToEvent, ok := replyToMap[converted.ReplyTo]
|
|
|
|
if ok {
|
|
|
|
part.Content.RelatesTo = &event.RelatesTo{
|
|
|
|
InReplyTo: &event.InReplyTo{EventID: replyToEvent},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
resp, err := portal.sendMessage(ctx, converted.Intent, event.EventMessage, part.Content, part.Extra, converted.Timestamp.UnixMilli())
|
2023-07-11 22:57:07 +00:00
|
|
|
if err != nil {
|
2023-08-10 08:30:17 +00:00
|
|
|
zerolog.Ctx(ctx).Err(err).Int("part_index", i).Str("part_id", part.ID).Msg("Failed to send message")
|
2023-07-11 22:57:07 +00:00
|
|
|
} else {
|
|
|
|
eventIDs = append(eventIDs, resp.EventID)
|
2023-08-09 14:53:11 +00:00
|
|
|
if len(eventIDs) > 1 {
|
|
|
|
mediaParts[part.ID] = database.MediaPart{
|
|
|
|
EventID: resp.EventID,
|
|
|
|
PendingMedia: part.PendingMedia,
|
|
|
|
}
|
|
|
|
} else if part.PendingMedia {
|
|
|
|
mediaParts[""] = database.MediaPart{PendingMedia: true}
|
|
|
|
}
|
2023-07-11 22:57:07 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-30 14:16:24 +00:00
|
|
|
if len(eventIDs) > 0 {
|
|
|
|
portal.markHandled(converted, eventIDs[0], mediaParts, true)
|
|
|
|
} else {
|
|
|
|
zerolog.Ctx(ctx).Warn().Msg("All parts of message failed to send")
|
|
|
|
}
|
2023-08-10 08:30:17 +00:00
|
|
|
return eventIDs
|
2023-07-11 22:57:07 +00:00
|
|
|
}
|
|
|
|
|
2023-07-17 23:57:20 +00:00
|
|
|
func (portal *Portal) syncReactions(ctx context.Context, source *User, message *database.Message, reactions []*gmproto.ReactionEntry) {
|
2023-07-12 21:44:57 +00:00
|
|
|
log := zerolog.Ctx(ctx)
|
2023-08-14 11:32:12 +00:00
|
|
|
existing, err := portal.bridge.DB.Reaction.GetAllByMessage(ctx, portal.Receiver, message.ID)
|
2023-07-12 21:44:57 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to get existing reactions from db to sync reactions")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
remove := make(map[string]*database.Reaction, len(existing))
|
|
|
|
for _, reaction := range existing {
|
|
|
|
remove[reaction.Sender] = reaction
|
|
|
|
}
|
|
|
|
for _, reaction := range reactions {
|
2024-02-29 13:49:36 +00:00
|
|
|
var emoji string
|
|
|
|
switch reaction.GetData().GetType() {
|
|
|
|
case gmproto.EmojiType_EMOTIFY:
|
|
|
|
emoji = ":custom:"
|
|
|
|
case gmproto.EmojiType_CUSTOM:
|
|
|
|
emoji = reaction.GetData().GetUnicode()
|
|
|
|
default:
|
2023-07-12 21:44:57 +00:00
|
|
|
emoji = reaction.GetData().GetType().Unicode()
|
|
|
|
if emoji == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, participant := range reaction.GetParticipantIDs() {
|
|
|
|
dbReaction, ok := remove[participant]
|
|
|
|
if ok {
|
|
|
|
delete(remove, participant)
|
|
|
|
if dbReaction.Reaction == emoji {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
intent := portal.getIntent(ctx, source, participant)
|
|
|
|
if intent == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
var resp *mautrix.RespSendEvent
|
2024-02-23 19:10:31 +00:00
|
|
|
resp, err = intent.SendMessageEvent(ctx, portal.MXID, event.EventReaction, &event.ReactionEventContent{
|
2023-08-21 06:51:48 +00:00
|
|
|
RelatesTo: event.RelatesTo{
|
|
|
|
EventID: message.MXID,
|
|
|
|
Type: event.RelAnnotation,
|
|
|
|
Key: variationselector.Add(emoji),
|
|
|
|
},
|
|
|
|
})
|
2023-07-12 21:44:57 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Str("reaction_sender_id", participant).Msg("Failed to send reaction")
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if dbReaction == nil {
|
|
|
|
dbReaction = portal.bridge.DB.Reaction.New()
|
|
|
|
dbReaction.Chat = portal.Key
|
|
|
|
dbReaction.Sender = participant
|
|
|
|
dbReaction.MessageID = message.ID
|
2024-02-23 19:10:31 +00:00
|
|
|
} else if _, err = intent.RedactEvent(ctx, portal.MXID, dbReaction.MXID); err != nil {
|
2023-07-12 21:44:57 +00:00
|
|
|
log.Err(err).Str("reaction_sender_id", participant).Msg("Failed to redact old reaction after adding new one")
|
|
|
|
}
|
|
|
|
dbReaction.Reaction = emoji
|
|
|
|
dbReaction.MXID = resp.EventID
|
|
|
|
err = dbReaction.Insert(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Str("reaction_sender_id", participant).Msg("Failed to insert added reaction into db")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, reaction := range remove {
|
|
|
|
intent := portal.getIntent(ctx, source, reaction.Sender)
|
|
|
|
if intent == nil {
|
|
|
|
continue
|
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = intent.RedactEvent(ctx, portal.MXID, reaction.MXID)
|
2023-07-12 21:44:57 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Str("reaction_sender_id", reaction.Sender).Msg("Failed to redact removed reaction")
|
|
|
|
} else if err = reaction.Delete(ctx); err != nil {
|
|
|
|
log.Err(err).Str("reaction_sender_id", reaction.Sender).Msg("Failed to remove reaction from db")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-11 22:57:07 +00:00
|
|
|
type ConvertedMessagePart struct {
|
2023-08-09 14:53:11 +00:00
|
|
|
ID string
|
|
|
|
PendingMedia bool
|
|
|
|
Content *event.MessageEventContent
|
|
|
|
Extra map[string]any
|
2023-07-11 22:57:07 +00:00
|
|
|
}
|
|
|
|
|
2023-09-04 08:33:03 +00:00
|
|
|
func (cmp *ConvertedMessagePart) SetEdit(eventID id.EventID) {
|
|
|
|
cmp.Content.SetEdit(eventID)
|
|
|
|
if cmp.Extra != nil {
|
|
|
|
cmp.Extra = map[string]any{
|
|
|
|
"m.new_content": cmp.Extra,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-11 22:57:07 +00:00
|
|
|
type ConvertedMessage struct {
|
|
|
|
ID string
|
|
|
|
SenderID string
|
|
|
|
|
|
|
|
Intent *appservice.IntentAPI
|
|
|
|
Timestamp time.Time
|
|
|
|
ReplyTo string
|
|
|
|
Parts []ConvertedMessagePart
|
2023-08-09 16:49:36 +00:00
|
|
|
PartCount int
|
2023-08-09 14:53:11 +00:00
|
|
|
|
2023-08-10 08:30:17 +00:00
|
|
|
DontBridge bool
|
|
|
|
|
2023-08-09 14:53:11 +00:00
|
|
|
Status gmproto.MessageStatusType
|
|
|
|
MediaStatus string
|
2023-07-11 22:57:07 +00:00
|
|
|
}
|
|
|
|
|
2023-07-12 21:44:57 +00:00
|
|
|
func (portal *Portal) getIntent(ctx context.Context, source *User, participant string) *appservice.IntentAPI {
|
2023-07-19 17:32:01 +00:00
|
|
|
if source.IsSelfParticipantID(participant) {
|
2023-07-12 21:44:57 +00:00
|
|
|
intent := source.DoublePuppetIntent
|
|
|
|
if intent == nil {
|
|
|
|
zerolog.Ctx(ctx).Debug().Msg("Dropping message from self as double puppeting is not enabled")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return intent
|
2024-03-18 12:54:21 +00:00
|
|
|
} else if portal.IsPrivateChat() {
|
|
|
|
if participant != portal.OtherUserID {
|
|
|
|
zerolog.Ctx(ctx).Warn().
|
|
|
|
Str("participant_id", participant).
|
|
|
|
Str("portal_other_user_id", portal.OtherUserID).
|
|
|
|
Msg("Got unexpected participant ID for message in DM portal, forcing to main intent")
|
|
|
|
}
|
|
|
|
return portal.MainIntent()
|
2023-07-12 21:44:57 +00:00
|
|
|
} else {
|
|
|
|
puppet := source.GetPuppetByID(participant, "")
|
|
|
|
if puppet == nil {
|
|
|
|
zerolog.Ctx(ctx).Debug().Str("participant_id", participant).Msg("Dropping message from unknown participant")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return puppet.IntentFor(portal)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-09 14:53:11 +00:00
|
|
|
func addSubject(content *event.MessageEventContent, subject string) {
|
|
|
|
content.Format = event.FormatHTML
|
|
|
|
content.FormattedBody = fmt.Sprintf("<strong>%s</strong><br>%s", event.TextToHTML(subject), event.TextToHTML(content.Body))
|
|
|
|
content.Body = fmt.Sprintf("**%s**\n%s", subject, content.Body)
|
|
|
|
}
|
|
|
|
|
|
|
|
func addDownloadStatus(content *event.MessageEventContent, status string) {
|
|
|
|
content.Body = fmt.Sprintf("%s\n\n%s", content.Body, status)
|
|
|
|
if content.Format == event.FormatHTML {
|
|
|
|
content.FormattedBody = fmt.Sprintf("<p>%s</p><p>%s</p>", content.FormattedBody, event.TextToHTML(status))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-21 08:09:36 +00:00
|
|
|
func (portal *Portal) shouldIgnoreStatus(status gmproto.MessageStatusType) bool {
|
2023-08-10 08:30:17 +00:00
|
|
|
switch status {
|
|
|
|
case gmproto.MessageStatusType_TOMBSTONE_PROTOCOL_SWITCH_TO_TEXT,
|
|
|
|
gmproto.MessageStatusType_TOMBSTONE_PROTOCOL_SWITCH_TO_RCS,
|
2023-08-15 08:28:59 +00:00
|
|
|
gmproto.MessageStatusType_TOMBSTONE_PROTOCOL_SWITCH_TO_ENCRYPTED_RCS,
|
2023-08-19 08:41:51 +00:00
|
|
|
gmproto.MessageStatusType_TOMBSTONE_PROTOCOL_SWITCH_TO_ENCRYPTED_RCS_INFO,
|
2023-08-25 17:44:40 +00:00
|
|
|
gmproto.MessageStatusType_TOMBSTONE_ONE_ON_ONE_SMS_CREATED,
|
|
|
|
gmproto.MessageStatusType_TOMBSTONE_ONE_ON_ONE_RCS_CREATED,
|
|
|
|
gmproto.MessageStatusType_TOMBSTONE_ENCRYPTED_ONE_ON_ONE_RCS_CREATED,
|
2023-08-19 08:41:51 +00:00
|
|
|
gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_PROTOCOL_SWITCH_TEXT_TO_E2EE,
|
|
|
|
gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_PROTOCOL_SWITCH_E2EE_TO_TEXT,
|
|
|
|
gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_PROTOCOL_SWITCH_RCS_TO_E2EE,
|
2023-08-21 08:09:36 +00:00
|
|
|
gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_PROTOCOL_SWITCH_E2EE_TO_RCS:
|
|
|
|
return portal.IsPrivateChat()
|
2023-08-25 17:44:40 +00:00
|
|
|
case gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_ENCRYPTED_GROUP_CREATED,
|
|
|
|
gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_GROUP_PROTOCOL_SWITCH_E2EE_TO_RCS,
|
|
|
|
gmproto.MessageStatusType_MESSAGE_STATUS_TOMBSTONE_GROUP_PROTOCOL_SWITCH_RCS_TO_E2EE,
|
|
|
|
gmproto.MessageStatusType_TOMBSTONE_RCS_GROUP_CREATED,
|
|
|
|
gmproto.MessageStatusType_TOMBSTONE_MMS_GROUP_CREATED,
|
|
|
|
gmproto.MessageStatusType_TOMBSTONE_SMS_BROADCAST_CREATED:
|
|
|
|
return true
|
2023-08-21 08:09:36 +00:00
|
|
|
case gmproto.MessageStatusType_TOMBSTONE_SHOW_LINK_PREVIEWS:
|
2023-08-10 08:30:17 +00:00
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-24 11:48:03 +00:00
|
|
|
func (portal *Portal) convertGoogleMessage(ctx context.Context, source *User, evt *gmproto.Message, backfill bool, raw []byte) *ConvertedMessage {
|
2023-07-11 22:57:07 +00:00
|
|
|
log := zerolog.Ctx(ctx)
|
|
|
|
|
|
|
|
var cm ConvertedMessage
|
2023-08-09 14:53:11 +00:00
|
|
|
cm.Status = evt.GetMessageStatus().GetStatus()
|
2023-07-11 22:57:07 +00:00
|
|
|
cm.SenderID = evt.ParticipantID
|
|
|
|
cm.ID = evt.MessageID
|
2023-08-09 16:49:36 +00:00
|
|
|
cm.PartCount = len(evt.GetMessageInfo())
|
2023-07-11 22:57:07 +00:00
|
|
|
cm.Timestamp = time.UnixMicro(evt.Timestamp)
|
2023-08-21 08:09:36 +00:00
|
|
|
cm.DontBridge = portal.shouldIgnoreStatus(cm.Status)
|
2023-08-09 14:53:11 +00:00
|
|
|
if cm.Status >= 200 && cm.Status < 300 {
|
2023-08-08 20:18:00 +00:00
|
|
|
cm.Intent = portal.bridge.Bot
|
|
|
|
if !portal.Encrypted && portal.IsPrivateChat() {
|
|
|
|
cm.Intent = portal.MainIntent()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
cm.Intent = portal.getIntent(ctx, source, evt.ParticipantID)
|
|
|
|
if cm.Intent == nil {
|
|
|
|
return nil
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
2023-07-09 17:48:27 +00:00
|
|
|
var replyTo id.EventID
|
|
|
|
if evt.GetReplyMessage() != nil {
|
2023-07-11 22:57:07 +00:00
|
|
|
cm.ReplyTo = evt.GetReplyMessage().GetMessageID()
|
2023-08-14 11:32:12 +00:00
|
|
|
msg, err := portal.bridge.DB.Message.GetByID(ctx, portal.Receiver, cm.ReplyTo)
|
2023-07-09 17:48:27 +00:00
|
|
|
if err != nil {
|
2023-07-11 22:57:07 +00:00
|
|
|
log.Err(err).Str("reply_to_id", cm.ReplyTo).Msg("Failed to get reply target message")
|
2023-07-09 17:48:27 +00:00
|
|
|
} else if msg == nil {
|
2023-07-11 22:57:07 +00:00
|
|
|
if backfill {
|
|
|
|
replyTo = portal.deterministicEventID(cm.ReplyTo, 0)
|
|
|
|
} else {
|
|
|
|
log.Warn().Str("reply_to_id", cm.ReplyTo).Msg("Reply target message not found")
|
|
|
|
}
|
2023-08-10 08:30:17 +00:00
|
|
|
} else if msg.IsFakeMXID() {
|
|
|
|
log.Debug().Str("reply_to_id", msg.ID).Msg("Ignoring reply to non-bridged message")
|
2023-07-09 17:48:27 +00:00
|
|
|
} else {
|
|
|
|
replyTo = msg.MXID
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-15 16:55:46 +00:00
|
|
|
subject := evt.GetSubject()
|
2023-08-09 14:53:11 +00:00
|
|
|
downloadStatus := downloadPendingStatusMessage(evt.GetMessageStatus().GetStatus())
|
|
|
|
cm.MediaStatus = downloadStatus
|
2023-07-02 14:21:55 +00:00
|
|
|
for _, part := range evt.MessageInfo {
|
|
|
|
var content event.MessageEventContent
|
2024-03-11 14:55:11 +00:00
|
|
|
var extra map[string]any
|
2023-08-09 14:53:11 +00:00
|
|
|
pendingMedia := false
|
2023-07-02 14:21:55 +00:00
|
|
|
switch data := part.GetData().(type) {
|
2023-07-17 13:51:31 +00:00
|
|
|
case *gmproto.MessageInfo_MessageContent:
|
2023-07-02 14:21:55 +00:00
|
|
|
content = event.MessageEventContent{
|
|
|
|
MsgType: event.MsgText,
|
|
|
|
Body: data.MessageContent.GetContent(),
|
|
|
|
}
|
2023-07-15 16:55:46 +00:00
|
|
|
if subject != "" {
|
2023-08-09 14:53:11 +00:00
|
|
|
addSubject(&content, subject)
|
2023-07-15 16:55:46 +00:00
|
|
|
subject = ""
|
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
if downloadStatus != "" {
|
|
|
|
addDownloadStatus(&content, downloadStatus)
|
|
|
|
downloadStatus = ""
|
|
|
|
}
|
2023-07-17 13:51:31 +00:00
|
|
|
case *gmproto.MessageInfo_MediaContent:
|
2023-09-02 09:09:59 +00:00
|
|
|
if data.MediaContent.MediaID == "" && data.MediaContent.MediaID2 == "" {
|
2023-08-09 14:53:11 +00:00
|
|
|
pendingMedia = true
|
|
|
|
content = event.MessageEventContent{
|
|
|
|
MsgType: event.MsgNotice,
|
|
|
|
Body: fmt.Sprintf("Waiting for attachment %s", data.MediaContent.GetMediaName()),
|
|
|
|
}
|
2024-03-11 14:55:11 +00:00
|
|
|
} else if contentPtr, extraMap, err := portal.convertGoogleMedia(ctx, source, cm.Intent, data.MediaContent); err != nil {
|
2023-08-09 14:53:11 +00:00
|
|
|
pendingMedia = true
|
2023-07-09 17:28:20 +00:00
|
|
|
log.Err(err).Msg("Failed to copy attachment")
|
|
|
|
content = event.MessageEventContent{
|
|
|
|
MsgType: event.MsgNotice,
|
|
|
|
Body: fmt.Sprintf("Failed to transfer attachment %s", data.MediaContent.GetMediaName()),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
content = *contentPtr
|
2024-03-11 14:55:11 +00:00
|
|
|
extra = extraMap
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
default:
|
|
|
|
continue
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-07-09 17:48:27 +00:00
|
|
|
if replyTo != "" {
|
|
|
|
content.RelatesTo = &event.RelatesTo{InReplyTo: &event.InReplyTo{EventID: replyTo}}
|
|
|
|
}
|
2023-07-11 22:57:07 +00:00
|
|
|
cm.Parts = append(cm.Parts, ConvertedMessagePart{
|
2023-08-09 14:53:11 +00:00
|
|
|
ID: part.GetActionMessageID(),
|
|
|
|
PendingMedia: pendingMedia,
|
|
|
|
Content: &content,
|
2024-03-11 14:55:11 +00:00
|
|
|
Extra: extra,
|
2023-07-11 22:57:07 +00:00
|
|
|
})
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-08-09 14:53:11 +00:00
|
|
|
if downloadStatus != "" {
|
|
|
|
content := event.MessageEventContent{
|
2023-08-13 14:10:37 +00:00
|
|
|
MsgType: event.MsgNotice,
|
2023-08-09 14:53:11 +00:00
|
|
|
Body: downloadStatus,
|
|
|
|
}
|
|
|
|
if subject != "" {
|
|
|
|
addSubject(&content, subject)
|
|
|
|
subject = ""
|
|
|
|
}
|
|
|
|
cm.Parts = append(cm.Parts, ConvertedMessagePart{Content: &content})
|
|
|
|
}
|
2023-07-15 16:55:46 +00:00
|
|
|
if subject != "" {
|
|
|
|
cm.Parts = append(cm.Parts, ConvertedMessagePart{
|
|
|
|
Content: &event.MessageEventContent{
|
|
|
|
MsgType: event.MsgText,
|
|
|
|
Body: subject,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2023-09-01 14:29:36 +00:00
|
|
|
if portal.bridge.Config.Bridge.BeeperGalleries {
|
|
|
|
cm.MergeGallery()
|
|
|
|
}
|
2023-07-11 22:57:07 +00:00
|
|
|
if portal.bridge.Config.Bridge.CaptionInMessage {
|
|
|
|
cm.MergeCaption()
|
|
|
|
}
|
2023-09-05 09:57:02 +00:00
|
|
|
if raw != nil && base64.StdEncoding.EncodedLen(len(raw)) < 8192 && len(cm.Parts) > 0 {
|
2023-08-24 11:48:03 +00:00
|
|
|
extra := cm.Parts[0].Extra
|
|
|
|
if extra == nil {
|
|
|
|
extra = make(map[string]any)
|
|
|
|
}
|
|
|
|
extra["fi.mau.gmessages.raw_debug_data"] = base64.StdEncoding.EncodeToString(raw)
|
|
|
|
cm.Parts[0].Extra = extra
|
|
|
|
}
|
2023-07-11 22:57:07 +00:00
|
|
|
return &cm
|
|
|
|
}
|
|
|
|
|
2023-09-01 14:29:36 +00:00
|
|
|
func (msg *ConvertedMessage) MergeGallery() {
|
|
|
|
var textPart *ConvertedMessagePart
|
|
|
|
var pendingImageParts, pendingImagePartsHTML []string
|
|
|
|
var imageParts []*event.MessageEventContent
|
|
|
|
var pendingMedia bool
|
|
|
|
|
|
|
|
for _, part := range msg.Parts {
|
|
|
|
pendingMedia = pendingMedia || part.PendingMedia
|
|
|
|
switch part.Content.MsgType {
|
|
|
|
case event.MsgText:
|
|
|
|
textPart = &part
|
|
|
|
case event.MsgNotice:
|
|
|
|
// TODO this doesn't handle formatted bodies in pending/failed media parts
|
|
|
|
pendingImageParts = append(pendingImageParts, part.Content.Body)
|
|
|
|
pendingImagePartsHTML = append(pendingImagePartsHTML, fmt.Sprintf("<p>%s</p>", event.TextToHTML(part.Content.Body)))
|
|
|
|
case event.MsgImage, event.MsgVideo, event.MsgAudio, event.MsgFile:
|
|
|
|
// TODO this ignores extra content in media parts
|
|
|
|
imageParts = append(imageParts, part.Content)
|
|
|
|
default:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(imageParts)+len(pendingImageParts) < 2 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var caption, captionHTML string
|
|
|
|
if textPart != nil {
|
|
|
|
caption = textPart.Content.Body
|
|
|
|
captionHTML = textPart.Content.FormattedBody
|
|
|
|
if captionHTML == "" {
|
|
|
|
captionHTML = event.TextToHTML(caption)
|
|
|
|
}
|
|
|
|
if len(pendingImageParts) > 0 {
|
|
|
|
caption = fmt.Sprintf("%s\n\n%s", caption, strings.Join(pendingImageParts, "\n\n"))
|
|
|
|
captionHTML = fmt.Sprintf("%s%s", ensureParagraph(captionHTML), strings.Join(pendingImagePartsHTML, ""))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(imageParts) == 0 {
|
|
|
|
msg.Parts = []ConvertedMessagePart{{
|
|
|
|
ID: msg.Parts[0].ID,
|
|
|
|
PendingMedia: pendingMedia,
|
|
|
|
Content: &event.MessageEventContent{
|
|
|
|
MsgType: event.MsgText,
|
|
|
|
Body: caption,
|
|
|
|
Format: event.FormatHTML,
|
|
|
|
FormattedBody: captionHTML,
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
} else {
|
|
|
|
msg.Parts = []ConvertedMessagePart{{
|
|
|
|
ID: msg.Parts[0].ID,
|
|
|
|
PendingMedia: pendingMedia,
|
|
|
|
Content: &event.MessageEventContent{
|
2023-09-02 10:29:48 +00:00
|
|
|
MsgType: event.MsgBeeperGallery,
|
2023-09-01 14:29:36 +00:00
|
|
|
Body: "Sent a gallery",
|
2023-09-02 10:29:48 +00:00
|
|
|
|
|
|
|
BeeperGalleryImages: imageParts,
|
|
|
|
BeeperGalleryCaption: caption,
|
|
|
|
BeeperGalleryCaptionHTML: captionHTML,
|
2023-09-01 14:29:36 +00:00
|
|
|
},
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func ensureParagraph(html string) string {
|
|
|
|
if !strings.HasPrefix(html, "<p>") {
|
|
|
|
return fmt.Sprintf("<p>%s</p>", html)
|
|
|
|
}
|
|
|
|
return html
|
|
|
|
}
|
|
|
|
|
2023-07-11 22:57:07 +00:00
|
|
|
func (msg *ConvertedMessage) MergeCaption() {
|
|
|
|
if len(msg.Parts) != 2 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var textPart, filePart ConvertedMessagePart
|
|
|
|
if msg.Parts[0].Content.MsgType == event.MsgText {
|
|
|
|
textPart = msg.Parts[0]
|
|
|
|
filePart = msg.Parts[1]
|
|
|
|
} else {
|
|
|
|
textPart = msg.Parts[1]
|
|
|
|
filePart = msg.Parts[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
if textPart.Content.MsgType != event.MsgText {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
switch filePart.Content.MsgType {
|
|
|
|
case event.MsgImage, event.MsgVideo, event.MsgAudio, event.MsgFile:
|
2023-08-09 14:53:11 +00:00
|
|
|
filePart.Content.FileName = filePart.Content.Body
|
|
|
|
filePart.Content.Body = textPart.Content.Body
|
|
|
|
filePart.Content.Format = textPart.Content.Format
|
|
|
|
filePart.Content.FormattedBody = textPart.Content.FormattedBody
|
|
|
|
case event.MsgNotice: // If it's a notice, the media failed or is pending
|
|
|
|
if textPart.Content.Format == event.FormatHTML {
|
|
|
|
filePart.Content.Format = event.FormatHTML
|
2023-09-01 14:29:36 +00:00
|
|
|
filePart.Content.FormattedBody = fmt.Sprintf("<p>%s</p>%s", event.TextToHTML(filePart.Content.Body), ensureParagraph(textPart.Content.FormattedBody))
|
2023-08-09 14:53:11 +00:00
|
|
|
}
|
|
|
|
filePart.Content.Body = fmt.Sprintf("%s\n\n%s", filePart.Content.Body, textPart.Content.Body)
|
|
|
|
filePart.Content.MsgType = event.MsgText
|
2023-07-11 22:57:07 +00:00
|
|
|
default:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
msg.Parts = []ConvertedMessagePart{filePart}
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
2024-03-11 14:55:11 +00:00
|
|
|
func (portal *Portal) convertGoogleMedia(ctx context.Context, source *User, intent *appservice.IntentAPI, msg *gmproto.MediaContent) (*event.MessageEventContent, map[string]any, error) {
|
2023-07-09 17:28:20 +00:00
|
|
|
var data []byte
|
|
|
|
var err error
|
2023-09-02 09:09:59 +00:00
|
|
|
if msg.MediaID != "" {
|
|
|
|
data, err = source.Client.DownloadMedia(msg.MediaID, msg.DecryptionKey)
|
|
|
|
} else if msg.MediaID2 != "" {
|
|
|
|
data, err = source.Client.DownloadMedia(msg.MediaID2, msg.DecryptionKey2)
|
|
|
|
} else {
|
|
|
|
err = fmt.Errorf("no media ID found")
|
|
|
|
}
|
2023-07-09 17:28:20 +00:00
|
|
|
if err != nil {
|
2024-03-11 14:55:11 +00:00
|
|
|
return nil, nil, err
|
2023-07-09 17:28:20 +00:00
|
|
|
}
|
2023-07-15 17:08:11 +00:00
|
|
|
mime := libgm.FormatToMediaType[msg.GetFormat()].Format
|
2023-07-09 17:28:20 +00:00
|
|
|
if mime == "" {
|
|
|
|
mime = mimetype.Detect(data).String()
|
|
|
|
}
|
2024-03-11 14:55:11 +00:00
|
|
|
fileName := msg.MediaName
|
|
|
|
extra := make(map[string]any)
|
2023-07-09 17:28:20 +00:00
|
|
|
msgtype := event.MsgFile
|
|
|
|
switch strings.Split(mime, "/")[0] {
|
|
|
|
case "image":
|
|
|
|
msgtype = event.MsgImage
|
|
|
|
case "video":
|
|
|
|
msgtype = event.MsgVideo
|
|
|
|
// TODO convert weird formats to mp4
|
|
|
|
case "audio":
|
|
|
|
msgtype = event.MsgAudio
|
2024-03-18 13:55:36 +00:00
|
|
|
if mime != "audio/ogg" {
|
|
|
|
data, err = ffmpeg.ConvertBytes(ctx, data, ".ogg", []string{}, []string{"-c:a", "libopus"}, mime)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("%w (%s to ogg): %w", errMediaConvertFailed, mime, err)
|
|
|
|
}
|
|
|
|
fileName += ".ogg"
|
|
|
|
mime = "audio/ogg"
|
2024-03-11 14:55:11 +00:00
|
|
|
}
|
|
|
|
extra["org.matrix.msc3245.voice"] = map[string]any{}
|
2023-07-09 17:28:20 +00:00
|
|
|
}
|
|
|
|
content := &event.MessageEventContent{
|
|
|
|
MsgType: msgtype,
|
2024-03-11 14:55:11 +00:00
|
|
|
Body: fileName,
|
2023-07-09 17:28:20 +00:00
|
|
|
Info: &event.FileInfo{
|
|
|
|
MimeType: mime,
|
|
|
|
Size: len(data),
|
|
|
|
},
|
|
|
|
}
|
2024-03-11 14:55:11 +00:00
|
|
|
return content, extra, portal.uploadMedia(ctx, intent, data, content)
|
2023-07-09 17:28:20 +00:00
|
|
|
}
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
func (portal *Portal) isRecentlyHandled(id string) bool {
|
|
|
|
start := portal.recentlyHandledIndex
|
|
|
|
for i := start; i != start; i = (i - 1) % recentlyHandledLength {
|
|
|
|
if portal.recentlyHandled[i] == id {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-08-09 14:53:11 +00:00
|
|
|
func (portal *Portal) markHandled(cm *ConvertedMessage, eventID id.EventID, mediaParts map[string]database.MediaPart, recent bool) *database.Message {
|
2023-07-02 14:21:55 +00:00
|
|
|
msg := portal.bridge.DB.Message.New()
|
|
|
|
msg.Chat = portal.Key
|
2023-08-14 11:32:12 +00:00
|
|
|
msg.RoomID = portal.MXID
|
2023-07-11 22:57:07 +00:00
|
|
|
msg.ID = cm.ID
|
|
|
|
msg.MXID = eventID
|
|
|
|
msg.Timestamp = cm.Timestamp
|
|
|
|
msg.Sender = cm.SenderID
|
2023-08-09 14:53:11 +00:00
|
|
|
msg.Status.Type = cm.Status
|
2023-08-09 16:49:36 +00:00
|
|
|
msg.Status.PartCount = cm.PartCount
|
2023-08-09 14:53:11 +00:00
|
|
|
msg.Status.MediaStatus = cm.MediaStatus
|
|
|
|
msg.Status.MediaParts = mediaParts
|
2023-07-02 14:21:55 +00:00
|
|
|
err := msg.Insert(context.TODO())
|
|
|
|
if err != nil {
|
2023-07-11 22:57:07 +00:00
|
|
|
portal.zlog.Err(err).Str("message_id", cm.ID).Msg("Failed to insert message to database")
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if recent {
|
|
|
|
portal.recentlyHandledLock.Lock()
|
|
|
|
index := portal.recentlyHandledIndex
|
|
|
|
portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength
|
|
|
|
portal.recentlyHandledLock.Unlock()
|
2023-07-11 22:57:07 +00:00
|
|
|
portal.recentlyHandled[index] = cm.ID
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
return msg
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) SyncParticipants(ctx context.Context, source *User, metadata *gmproto.Conversation) (userIDs []id.UserID, changed bool) {
|
2024-02-29 15:19:20 +00:00
|
|
|
filteredParticipants := make([]*gmproto.Participant, 0, len(metadata.Participants))
|
2023-07-02 14:21:55 +00:00
|
|
|
for _, participant := range metadata.Participants {
|
|
|
|
if participant.IsMe {
|
2024-02-23 19:10:31 +00:00
|
|
|
err := source.AddSelfParticipantID(ctx, participant.ID.ParticipantID)
|
2023-07-19 17:32:01 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Warn().Err(err).
|
|
|
|
Str("participant_id", participant.ID.ParticipantID).
|
|
|
|
Msg("Failed to save self participant ID")
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
continue
|
2023-07-09 11:16:52 +00:00
|
|
|
} else if participant.ID.Number == "" {
|
2024-03-18 12:54:21 +00:00
|
|
|
portal.zlog.Warn().Any("participant", participant).Msg("No number found in non-self participant entry")
|
|
|
|
continue
|
|
|
|
} else if !participant.IsVisible {
|
|
|
|
portal.zlog.Debug().Any("participant", participant).Msg("Ignoring fake participant")
|
2023-07-02 14:21:55 +00:00
|
|
|
continue
|
|
|
|
}
|
2024-02-29 15:19:20 +00:00
|
|
|
filteredParticipants = append(filteredParticipants, participant)
|
|
|
|
}
|
|
|
|
for _, participant := range filteredParticipants {
|
2023-07-09 11:16:52 +00:00
|
|
|
puppet := source.GetPuppetByID(participant.ID.ParticipantID, participant.ID.Number)
|
2023-07-19 22:54:30 +00:00
|
|
|
if puppet == nil {
|
|
|
|
portal.zlog.Error().Any("participant_id", participant.ID).Msg("Failed to get puppet for participant")
|
|
|
|
continue
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
userIDs = append(userIDs, puppet.MXID)
|
2024-02-23 19:10:31 +00:00
|
|
|
puppet.Sync(ctx, source, participant)
|
2023-07-02 14:21:55 +00:00
|
|
|
if portal.MXID != "" {
|
2024-02-23 19:10:31 +00:00
|
|
|
err := puppet.IntentFor(portal).EnsureJoined(ctx, portal.MXID)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).
|
|
|
|
Str("user_id", puppet.MXID.String()).
|
|
|
|
Msg("Failed to ensure ghost is joined to portal")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-02-29 15:19:20 +00:00
|
|
|
if !metadata.IsGroupChat && len(filteredParticipants) == 1 && portal.OtherUserID != filteredParticipants[0].ID.ParticipantID {
|
2023-07-02 14:21:55 +00:00
|
|
|
portal.zlog.Info().
|
|
|
|
Str("old_other_user_id", portal.OtherUserID).
|
2024-02-29 15:19:20 +00:00
|
|
|
Str("new_other_user_id", filteredParticipants[0].ID.ParticipantID).
|
2023-07-02 14:21:55 +00:00
|
|
|
Msg("Found other user ID in DM")
|
2024-02-29 15:19:20 +00:00
|
|
|
portal.OtherUserID = filteredParticipants[0].ID.ParticipantID
|
2023-09-04 22:34:47 +00:00
|
|
|
portal.bridge.portalsLock.Lock()
|
|
|
|
portal.bridge.portalsByOtherUser[database.Key{
|
|
|
|
ID: portal.OtherUserID,
|
|
|
|
Receiver: portal.Receiver,
|
|
|
|
}] = portal
|
|
|
|
portal.bridge.portalsLock.Unlock()
|
2023-07-02 21:13:36 +00:00
|
|
|
changed = true
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2024-02-29 15:19:20 +00:00
|
|
|
if !metadata.IsGroupChat && portal.OtherUserID == "" {
|
|
|
|
portal.zlog.Warn().Msg("No other user ID found in DM")
|
|
|
|
}
|
2023-08-02 11:58:27 +00:00
|
|
|
if portal.MXID != "" {
|
2024-02-23 19:10:31 +00:00
|
|
|
members, err := portal.MainIntent().JoinedMembers(ctx, portal.MXID)
|
2023-08-02 11:58:27 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Warn().Err(err).Msg("Failed to get joined members")
|
|
|
|
} else {
|
|
|
|
delete(members.Joined, portal.bridge.Bot.UserID)
|
|
|
|
delete(members.Joined, source.MXID)
|
|
|
|
for _, userID := range userIDs {
|
|
|
|
delete(members.Joined, userID)
|
|
|
|
}
|
|
|
|
for userID := range members.Joined {
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = portal.MainIntent().KickUser(ctx, portal.MXID, &mautrix.ReqKickUser{
|
2023-08-02 11:58:27 +00:00
|
|
|
UserID: userID,
|
|
|
|
Reason: "User is not participating in chat",
|
|
|
|
})
|
2024-03-01 11:34:27 +00:00
|
|
|
if errors.Is(err, mautrix.MForbidden) && portal.MainIntent() != portal.bridge.Bot {
|
|
|
|
_, err = portal.bridge.Bot.KickUser(ctx, portal.MXID, &mautrix.ReqKickUser{
|
|
|
|
UserID: userID,
|
|
|
|
Reason: "User is not participating in chat",
|
|
|
|
})
|
|
|
|
}
|
2023-08-02 11:58:27 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Warn().Err(err).
|
|
|
|
Str("user_id", userID.String()).
|
|
|
|
Msg("Failed to kick extra user from portal")
|
|
|
|
} else {
|
|
|
|
portal.zlog.Debug().
|
|
|
|
Str("user_id", userID.String()).
|
|
|
|
Msg("Kicked extra user from portal")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-07-02 21:13:36 +00:00
|
|
|
return userIDs, changed
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) UpdateName(ctx context.Context, name string, updateInfo bool) bool {
|
2023-07-02 14:21:55 +00:00
|
|
|
if portal.Name != name || (!portal.NameSet && len(portal.MXID) > 0 && portal.shouldSetDMRoomMetadata()) {
|
|
|
|
portal.zlog.Debug().Str("old_name", portal.Name).Str("new_name", name).Msg("Updating name")
|
|
|
|
portal.Name = name
|
|
|
|
portal.NameSet = false
|
|
|
|
if updateInfo {
|
|
|
|
defer func() {
|
2024-02-23 19:10:31 +00:00
|
|
|
err := portal.Update(ctx)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to save portal after updating name")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(portal.MXID) > 0 && !portal.shouldSetDMRoomMetadata() {
|
2024-02-23 19:10:31 +00:00
|
|
|
portal.UpdateBridgeInfo(ctx)
|
2023-07-02 14:21:55 +00:00
|
|
|
} else if len(portal.MXID) > 0 {
|
|
|
|
intent := portal.MainIntent()
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err := intent.SetRoomName(ctx, portal.MXID, name)
|
2023-07-02 14:21:55 +00:00
|
|
|
if errors.Is(err, mautrix.MForbidden) && intent != portal.MainIntent() {
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = portal.MainIntent().SetRoomName(ctx, portal.MXID, name)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
if err == nil {
|
|
|
|
portal.NameSet = true
|
|
|
|
if updateInfo {
|
2024-02-23 19:10:31 +00:00
|
|
|
portal.UpdateBridgeInfo(ctx)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
return true
|
|
|
|
} else {
|
|
|
|
portal.zlog.Warn().Err(err).Msg("Failed to set room name")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) UpdateMetadata(ctx context.Context, user *User, info *gmproto.Conversation) []id.UserID {
|
|
|
|
participants, update := portal.SyncParticipants(ctx, user, info)
|
2023-07-22 16:21:29 +00:00
|
|
|
if portal.Type != info.Type {
|
|
|
|
portal.zlog.Debug().
|
|
|
|
Str("old_type", portal.Type.String()).
|
|
|
|
Str("new_type", info.Type.String()).
|
|
|
|
Msg("Conversation type changed")
|
|
|
|
portal.Type = info.Type
|
|
|
|
update = true
|
|
|
|
}
|
2024-06-13 17:46:18 +00:00
|
|
|
if portal.SendMode != info.SendMode {
|
|
|
|
portal.zlog.Debug().
|
|
|
|
Str("old_send_mode", portal.SendMode.String()).
|
|
|
|
Str("new_send_mode", info.SendMode.String()).
|
|
|
|
Msg("Conversation send mode changed")
|
|
|
|
portal.SendMode = info.SendMode
|
|
|
|
update = true
|
|
|
|
}
|
2023-07-19 17:32:01 +00:00
|
|
|
if portal.OutgoingID != info.DefaultOutgoingID {
|
2023-07-22 16:21:29 +00:00
|
|
|
portal.zlog.Debug().
|
|
|
|
Str("old_id", portal.OutgoingID).
|
|
|
|
Str("new_id", info.DefaultOutgoingID).
|
|
|
|
Msg("Default outgoing participant ID changed")
|
2023-07-19 17:32:01 +00:00
|
|
|
portal.OutgoingID = info.DefaultOutgoingID
|
2023-07-02 20:47:31 +00:00
|
|
|
update = true
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
if portal.MXID != "" {
|
2024-02-23 19:10:31 +00:00
|
|
|
update = portal.addToPersonalSpace(ctx, user, false) || update
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-07-02 20:47:31 +00:00
|
|
|
if portal.shouldSetDMRoomMetadata() {
|
2024-02-23 19:10:31 +00:00
|
|
|
update = portal.UpdateName(ctx, info.Name, false) || update
|
2023-07-02 20:47:31 +00:00
|
|
|
}
|
2023-07-19 23:03:07 +00:00
|
|
|
if portal.MXID != "" {
|
2024-02-23 19:10:31 +00:00
|
|
|
pls, err := portal.MainIntent().PowerLevels(ctx, portal.MXID)
|
2023-07-19 18:38:26 +00:00
|
|
|
if err != nil {
|
2023-07-19 23:03:07 +00:00
|
|
|
portal.zlog.Warn().Err(err).Msg("Failed to get power levels")
|
|
|
|
} else if portal.updatePowerLevels(info, pls) {
|
2024-02-23 19:10:31 +00:00
|
|
|
resp, err := portal.MainIntent().SetPowerLevels(ctx, portal.MXID, pls)
|
2024-02-29 15:32:57 +00:00
|
|
|
if errors.Is(err, mautrix.MForbidden) && portal.MainIntent() != portal.bridge.Bot {
|
|
|
|
resp, err = portal.bridge.Bot.SetPowerLevels(ctx, portal.MXID, pls)
|
|
|
|
}
|
2023-07-19 23:03:07 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Warn().Err(err).Msg("Failed to update power levels")
|
|
|
|
} else {
|
|
|
|
portal.zlog.Debug().Str("event_id", resp.EventID.String()).Msg("Updated power levels")
|
|
|
|
}
|
2023-07-19 18:38:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
// TODO avatar
|
|
|
|
if update {
|
2024-02-23 19:10:31 +00:00
|
|
|
err := portal.Update(ctx)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to save portal after updating metadata")
|
|
|
|
}
|
|
|
|
if portal.MXID != "" {
|
2024-02-23 19:10:31 +00:00
|
|
|
portal.UpdateBridgeInfo(ctx)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return participants
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) ensureUserInvited(ctx context.Context, user *User) bool {
|
|
|
|
return user.ensureInvited(ctx, portal.MainIntent(), portal.MXID, portal.IsPrivateChat())
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent {
|
|
|
|
anyone := 0
|
|
|
|
nope := 99
|
|
|
|
return &event.PowerLevelsEventContent{
|
|
|
|
UsersDefault: anyone,
|
|
|
|
EventsDefault: anyone,
|
|
|
|
RedactPtr: &anyone,
|
|
|
|
StateDefaultPtr: &nope,
|
|
|
|
BanPtr: &nope,
|
2023-07-19 18:38:26 +00:00
|
|
|
KickPtr: &nope,
|
|
|
|
InvitePtr: &nope,
|
2023-07-02 14:21:55 +00:00
|
|
|
Users: map[id.UserID]int{
|
|
|
|
portal.MainIntent().UserID: 100,
|
2023-07-19 18:38:26 +00:00
|
|
|
portal.bridge.Bot.UserID: 100,
|
2023-07-02 14:21:55 +00:00
|
|
|
},
|
|
|
|
Events: map[string]int{
|
|
|
|
event.StateRoomName.Type: anyone,
|
|
|
|
event.StateRoomAvatar.Type: anyone,
|
2023-07-20 14:12:10 +00:00
|
|
|
event.EventReaction.Type: anyone,
|
2023-07-19 23:10:46 +00:00
|
|
|
event.EventRedaction.Type: anyone,
|
2023-07-02 14:21:55 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-19 18:38:26 +00:00
|
|
|
func (portal *Portal) updatePowerLevels(conv *gmproto.Conversation, pl *event.PowerLevelsEventContent) bool {
|
|
|
|
expectedEventsDefault := 0
|
|
|
|
if conv.GetReadOnly() {
|
|
|
|
expectedEventsDefault = 99
|
|
|
|
}
|
|
|
|
|
|
|
|
changed := false
|
|
|
|
if pl.EventsDefault != expectedEventsDefault {
|
|
|
|
pl.EventsDefault = expectedEventsDefault
|
|
|
|
changed = true
|
|
|
|
}
|
2023-07-20 14:12:10 +00:00
|
|
|
changed = pl.EnsureEventLevel(event.EventReaction, expectedEventsDefault) || changed
|
2023-07-19 23:10:46 +00:00
|
|
|
// Explicitly set m.room.redaction level to 0 so redactions work even if sending is disabled
|
|
|
|
changed = pl.EnsureEventLevel(event.EventRedaction, 0) || changed
|
2024-02-29 15:32:57 +00:00
|
|
|
changed = pl.EnsureUserLevel(portal.MainIntent().UserID, 100) || changed
|
2023-07-19 18:38:26 +00:00
|
|
|
changed = pl.EnsureUserLevel(portal.bridge.Bot.UserID, 100) || changed
|
|
|
|
return changed
|
|
|
|
}
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
func (portal *Portal) getBridgeInfoStateKey() string {
|
|
|
|
return fmt.Sprintf("fi.mau.gmessages://gmessages/%s", portal.ID)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) getBridgeInfo() (string, event.BridgeEventContent) {
|
2023-07-22 16:21:29 +00:00
|
|
|
content := event.BridgeEventContent{
|
2023-07-02 14:21:55 +00:00
|
|
|
BridgeBot: portal.bridge.Bot.UserID,
|
|
|
|
Creator: portal.MainIntent().UserID,
|
|
|
|
Protocol: event.BridgeInfoSection{
|
|
|
|
ID: "gmessages",
|
|
|
|
DisplayName: "Google Messages",
|
|
|
|
AvatarURL: portal.bridge.Config.AppService.Bot.ParsedAvatar.CUString(),
|
|
|
|
ExternalURL: "https://messages.google.com/",
|
|
|
|
},
|
|
|
|
Channel: event.BridgeInfoSection{
|
|
|
|
ID: portal.ID,
|
|
|
|
DisplayName: portal.Name,
|
|
|
|
},
|
|
|
|
}
|
2023-07-22 16:21:29 +00:00
|
|
|
if portal.Type == gmproto.ConversationType_SMS {
|
|
|
|
content.Protocol.ID = "gmessages-sms"
|
|
|
|
content.Protocol.DisplayName = "Google Messages (SMS)"
|
|
|
|
} else if portal.Type == gmproto.ConversationType_RCS {
|
|
|
|
content.Protocol.ID = "gmessages-rcs"
|
|
|
|
content.Protocol.DisplayName = "Google Messages (RCS)"
|
|
|
|
}
|
|
|
|
return portal.getBridgeInfoStateKey(), content
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) UpdateBridgeInfo(ctx context.Context) {
|
2023-07-02 14:21:55 +00:00
|
|
|
if len(portal.MXID) == 0 {
|
|
|
|
portal.zlog.Debug().Msg("Not updating bridge info: no Matrix room created")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
portal.zlog.Debug().Msg("Updating bridge info...")
|
|
|
|
stateKey, content := portal.getBridgeInfo()
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err := portal.MainIntent().SendStateEvent(ctx, portal.MXID, event.StateBridge, stateKey, content)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Warn().Err(err).Msg("Failed to update m.bridge")
|
|
|
|
}
|
|
|
|
// TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = portal.MainIntent().SendStateEvent(ctx, portal.MXID, event.StateHalfShotBridge, stateKey, content)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Warn().Err(err).Msg("Failed to update uk.half-shot.bridge")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) shouldSetDMRoomMetadata() bool {
|
|
|
|
return !portal.IsPrivateChat() ||
|
|
|
|
portal.bridge.Config.Bridge.PrivateChatPortalMeta == "always" ||
|
2023-07-09 11:57:54 +00:00
|
|
|
((portal.IsEncrypted() || (portal.MXID == "" && portal.bridge.Config.Bridge.Encryption.Default)) &&
|
|
|
|
portal.bridge.Config.Bridge.PrivateChatPortalMeta != "never")
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) GetEncryptionEventContent() (evt *event.EncryptionEventContent) {
|
|
|
|
evt = &event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}
|
|
|
|
if rot := portal.bridge.Config.Bridge.Encryption.Rotation; rot.EnableCustom {
|
|
|
|
evt.RotationPeriodMillis = rot.Milliseconds
|
|
|
|
evt.RotationPeriodMessages = rot.Messages
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) CreateMatrixRoom(ctx context.Context, user *User, conv *gmproto.Conversation, isFromSync bool) error {
|
2023-07-02 14:21:55 +00:00
|
|
|
portal.roomCreateLock.Lock()
|
|
|
|
defer portal.roomCreateLock.Unlock()
|
|
|
|
if len(portal.MXID) > 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-08-10 11:09:08 +00:00
|
|
|
var err error
|
|
|
|
if conv == nil {
|
|
|
|
portal.zlog.Debug().Msg("CreateMatrixRoom called without conversation info, requesting from phone")
|
|
|
|
conv, err = user.Client.GetConversation(portal.ID)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to get conversation info: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
members := portal.UpdateMetadata(ctx, user, conv)
|
2023-09-04 22:34:47 +00:00
|
|
|
var avatarURL id.ContentURI
|
2023-07-02 14:21:55 +00:00
|
|
|
|
2023-09-04 22:34:47 +00:00
|
|
|
if portal.IsPrivateChat() {
|
|
|
|
puppet := portal.GetDMPuppet()
|
|
|
|
if puppet == nil {
|
|
|
|
portal.zlog.Error().Msg("Didn't find ghost of other user in DM :(")
|
|
|
|
return fmt.Errorf("ghost not found")
|
|
|
|
}
|
|
|
|
if portal.shouldSetDMRoomMetadata() {
|
|
|
|
avatarURL = puppet.AvatarMXC
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
intent := portal.MainIntent()
|
2024-02-23 19:10:31 +00:00
|
|
|
if err = intent.EnsureRegistered(ctx); err != nil {
|
2023-07-02 14:21:55 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
portal.zlog.Info().Msg("Creating Matrix room")
|
|
|
|
|
|
|
|
bridgeInfoStateKey, bridgeInfo := portal.getBridgeInfo()
|
|
|
|
|
2023-07-19 18:38:26 +00:00
|
|
|
pl := portal.GetBasePowerLevels()
|
|
|
|
portal.updatePowerLevels(conv, pl)
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
initialState := []*event.Event{{
|
2023-07-19 18:38:26 +00:00
|
|
|
Type: event.StatePowerLevels,
|
|
|
|
Content: event.Content{Parsed: pl},
|
2023-07-02 14:21:55 +00:00
|
|
|
}, {
|
|
|
|
Type: event.StateBridge,
|
|
|
|
Content: event.Content{Parsed: bridgeInfo},
|
|
|
|
StateKey: &bridgeInfoStateKey,
|
|
|
|
}, {
|
|
|
|
// TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
|
|
|
|
Type: event.StateHalfShotBridge,
|
|
|
|
Content: event.Content{Parsed: bridgeInfo},
|
|
|
|
StateKey: &bridgeInfoStateKey,
|
|
|
|
}}
|
|
|
|
var invite []id.UserID
|
|
|
|
if portal.bridge.Config.Bridge.Encryption.Default {
|
|
|
|
initialState = append(initialState, &event.Event{
|
|
|
|
Type: event.StateEncryption,
|
|
|
|
Content: event.Content{
|
|
|
|
Parsed: portal.GetEncryptionEventContent(),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
portal.Encrypted = true
|
|
|
|
if portal.IsPrivateChat() {
|
|
|
|
invite = append(invite, portal.bridge.Bot.UserID)
|
|
|
|
}
|
|
|
|
}
|
2023-09-04 22:34:47 +00:00
|
|
|
if !avatarURL.IsEmpty() {
|
|
|
|
initialState = append(initialState, &event.Event{
|
|
|
|
Type: event.StateRoomAvatar,
|
|
|
|
Content: event.Content{
|
2024-06-16 20:58:05 +00:00
|
|
|
Parsed: &event.RoomAvatarEventContent{URL: avatarURL.CUString()},
|
2023-09-04 22:34:47 +00:00
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
|
|
|
|
creationContent := make(map[string]interface{})
|
|
|
|
if !portal.bridge.Config.Bridge.FederateRooms {
|
|
|
|
creationContent["m.federate"] = false
|
|
|
|
}
|
|
|
|
autoJoinInvites := portal.bridge.SpecVersions.Supports(mautrix.BeeperFeatureAutojoinInvites)
|
|
|
|
if autoJoinInvites {
|
|
|
|
portal.zlog.Debug().Msg("Hungryserv mode: adding all group members in create request")
|
|
|
|
invite = append(invite, members...)
|
|
|
|
invite = append(invite, user.MXID)
|
|
|
|
}
|
|
|
|
req := &mautrix.ReqCreateRoom{
|
|
|
|
Visibility: "private",
|
|
|
|
Name: portal.Name,
|
|
|
|
Invite: invite,
|
|
|
|
Preset: "private_chat",
|
|
|
|
IsDirect: portal.IsPrivateChat(),
|
|
|
|
InitialState: initialState,
|
|
|
|
CreationContent: creationContent,
|
|
|
|
|
|
|
|
BeeperAutoJoinInvites: autoJoinInvites,
|
|
|
|
}
|
|
|
|
if !portal.shouldSetDMRoomMetadata() {
|
|
|
|
req.Name = ""
|
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
resp, err := intent.CreateRoom(ctx, req)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-08-02 12:01:50 +00:00
|
|
|
portal.zlog.Info().Str("new_room_id", resp.RoomID.String()).Msg("Matrix room created")
|
2023-07-02 14:21:55 +00:00
|
|
|
portal.InSpace = false
|
|
|
|
portal.NameSet = len(req.Name) > 0
|
2023-07-11 22:57:07 +00:00
|
|
|
portal.forwardBackfillLock.Lock()
|
2023-07-02 14:21:55 +00:00
|
|
|
portal.MXID = resp.RoomID
|
2023-08-02 12:01:50 +00:00
|
|
|
portal.updateLogger()
|
2023-07-02 14:21:55 +00:00
|
|
|
portal.bridge.portalsLock.Lock()
|
|
|
|
portal.bridge.portalsByMXID[portal.MXID] = portal
|
|
|
|
portal.bridge.portalsLock.Unlock()
|
|
|
|
err = portal.Update(context.TODO())
|
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to save portal after creating room")
|
|
|
|
}
|
|
|
|
|
|
|
|
// We set the memberships beforehand to make sure the encryption key exchange in initial backfill knows the users are here.
|
|
|
|
inviteMembership := event.MembershipInvite
|
|
|
|
if autoJoinInvites {
|
|
|
|
inviteMembership = event.MembershipJoin
|
|
|
|
}
|
|
|
|
for _, userID := range invite {
|
2024-02-23 19:10:31 +00:00
|
|
|
// TODO handle errors
|
|
|
|
portal.bridge.StateStore.SetMembership(ctx, portal.MXID, userID, inviteMembership)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if !autoJoinInvites {
|
|
|
|
if !portal.IsPrivateChat() {
|
2024-02-23 19:10:31 +00:00
|
|
|
portal.SyncParticipants(ctx, user, conv)
|
2023-07-02 14:21:55 +00:00
|
|
|
} else {
|
|
|
|
if portal.bridge.Config.Bridge.Encryption.Default {
|
2024-02-23 19:10:31 +00:00
|
|
|
err = portal.bridge.Bot.EnsureJoined(ctx, portal.MXID)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
2023-07-19 21:01:05 +00:00
|
|
|
portal.zlog.Err(err).Msg("Failed to join created portal with bridge bot for e2be")
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
user.UpdateDirectChats(ctx, map[id.UserID][]id.RoomID{portal.GetDMPuppet().MXID: {portal.MXID}})
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
portal.ensureUserInvited(ctx, user)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
user.syncChatDoublePuppetDetails(ctx, portal, conv, true)
|
2023-08-15 08:05:21 +00:00
|
|
|
allowNotify := !isFromSync
|
|
|
|
go portal.initialForwardBackfill(user, !conv.GetUnread(), allowNotify)
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.addToPersonalSpace(context.TODO(), user, true)
|
2023-07-02 14:21:55 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) addToPersonalSpace(ctx context.Context, user *User, updateInfo bool) bool {
|
|
|
|
spaceID := user.GetSpaceRoom(ctx)
|
2023-07-02 14:21:55 +00:00
|
|
|
if len(spaceID) == 0 || portal.InSpace {
|
|
|
|
return false
|
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err := portal.bridge.Bot.SendStateEvent(ctx, spaceID, event.StateSpaceChild, portal.MXID.String(), &event.SpaceChildEventContent{
|
2023-07-02 14:21:55 +00:00
|
|
|
Via: []string{portal.bridge.Config.Homeserver.Domain},
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Str("space_id", spaceID.String()).Msg("Failed to add room to user's personal filtering space")
|
|
|
|
portal.InSpace = false
|
|
|
|
} else {
|
|
|
|
portal.zlog.Debug().Str("space_id", spaceID.String()).Msg("Added room to user's personal filtering space")
|
|
|
|
portal.InSpace = true
|
|
|
|
}
|
2023-07-02 20:49:50 +00:00
|
|
|
if updateInfo {
|
2024-02-23 19:10:31 +00:00
|
|
|
err = portal.Update(ctx)
|
2023-07-02 20:49:50 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to update portal after adding to personal space")
|
|
|
|
}
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) IsPrivateChat() bool {
|
|
|
|
return portal.OtherUserID != ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) GetDMPuppet() *Puppet {
|
|
|
|
if portal.IsPrivateChat() {
|
|
|
|
puppet := portal.bridge.GetPuppetByKey(database.Key{Receiver: portal.Receiver, ID: portal.OtherUserID}, "")
|
|
|
|
return puppet
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) MainIntent() *appservice.IntentAPI {
|
|
|
|
if puppet := portal.GetDMPuppet(); puppet != nil {
|
|
|
|
return puppet.DefaultIntent()
|
|
|
|
}
|
|
|
|
return portal.bridge.Bot
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) sendMainIntentMessage(ctx context.Context, content *event.MessageEventContent) (*mautrix.RespSendEvent, error) {
|
|
|
|
return portal.sendMessage(ctx, portal.MainIntent(), event.EventMessage, content, nil, 0)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) encrypt(ctx context.Context, intent *appservice.IntentAPI, content *event.Content, eventType event.Type) (event.Type, error) {
|
2023-07-02 14:21:55 +00:00
|
|
|
if !portal.Encrypted || portal.bridge.Crypto == nil {
|
|
|
|
return eventType, nil
|
|
|
|
}
|
|
|
|
intent.AddDoublePuppetValue(content)
|
|
|
|
// TODO maybe the locking should be inside mautrix-go?
|
|
|
|
portal.encryptLock.Lock()
|
|
|
|
defer portal.encryptLock.Unlock()
|
2024-02-23 19:10:31 +00:00
|
|
|
err := portal.bridge.Crypto.Encrypt(ctx, portal.MXID, eventType, content)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return eventType, fmt.Errorf("failed to encrypt event: %w", err)
|
|
|
|
}
|
|
|
|
return event.EventEncrypted, nil
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) sendMessage(ctx context.Context, intent *appservice.IntentAPI, eventType event.Type, content *event.MessageEventContent, extraContent map[string]interface{}, timestamp int64) (*mautrix.RespSendEvent, error) {
|
2023-07-02 14:21:55 +00:00
|
|
|
wrappedContent := event.Content{Parsed: content, Raw: extraContent}
|
|
|
|
var err error
|
2024-02-23 19:10:31 +00:00
|
|
|
eventType, err = portal.encrypt(ctx, intent, &wrappedContent, eventType)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
_, _ = intent.UserTyping(ctx, portal.MXID, false, 0)
|
2023-07-02 14:21:55 +00:00
|
|
|
if timestamp == 0 {
|
2024-02-23 19:10:31 +00:00
|
|
|
return intent.SendMessageEvent(ctx, portal.MXID, eventType, &wrappedContent)
|
2023-07-02 14:21:55 +00:00
|
|
|
} else {
|
2024-02-23 19:10:31 +00:00
|
|
|
return intent.SendMassagedMessageEvent(ctx, portal.MXID, eventType, &wrappedContent, timestamp)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) encryptFileInPlace(data []byte, mimeType string) (string, *event.EncryptedFileInfo) {
|
|
|
|
if !portal.Encrypted {
|
|
|
|
return mimeType, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
file := &event.EncryptedFileInfo{
|
|
|
|
EncryptedFile: *attachment.NewEncryptedFile(),
|
|
|
|
URL: "",
|
|
|
|
}
|
|
|
|
file.EncryptInPlace(data)
|
|
|
|
return "application/octet-stream", file
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) uploadMedia(ctx context.Context, intent *appservice.IntentAPI, data []byte, content *event.MessageEventContent) error {
|
2023-07-02 14:21:55 +00:00
|
|
|
uploadMimeType, file := portal.encryptFileInPlace(data, content.Info.MimeType)
|
|
|
|
|
|
|
|
req := mautrix.ReqUploadMedia{
|
|
|
|
ContentBytes: data,
|
|
|
|
ContentType: uploadMimeType,
|
|
|
|
}
|
|
|
|
var mxc id.ContentURI
|
|
|
|
if portal.bridge.Config.Homeserver.AsyncMedia {
|
2024-02-23 19:10:31 +00:00
|
|
|
uploaded, err := intent.UploadAsync(ctx, req)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
mxc = uploaded.ContentURI
|
|
|
|
} else {
|
2024-02-23 19:10:31 +00:00
|
|
|
uploaded, err := intent.UploadMedia(ctx, req)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
mxc = uploaded.ContentURI
|
|
|
|
}
|
|
|
|
|
|
|
|
if file != nil {
|
|
|
|
file.URL = mxc.CUString()
|
|
|
|
content.File = file
|
|
|
|
} else {
|
|
|
|
content.URL = mxc.CUString()
|
|
|
|
}
|
|
|
|
|
|
|
|
content.Info.Size = len(data)
|
|
|
|
if content.Info.Width == 0 && content.Info.Height == 0 && strings.HasPrefix(content.Info.MimeType, "image/") {
|
|
|
|
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
|
|
|
|
content.Info.Width, content.Info.Height = cfg.Width, cfg.Height
|
|
|
|
}
|
|
|
|
|
|
|
|
// This is a hack for bad clients like Element iOS that require a thumbnail (https://github.com/vector-im/element-ios/issues/4004)
|
|
|
|
if strings.HasPrefix(content.Info.MimeType, "image/") && content.Info.ThumbnailInfo == nil {
|
|
|
|
infoCopy := *content.Info
|
|
|
|
content.Info.ThumbnailInfo = &infoCopy
|
|
|
|
if content.File != nil {
|
|
|
|
content.Info.ThumbnailFile = file
|
|
|
|
} else {
|
|
|
|
content.Info.ThumbnailURL = content.URL
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-11 14:55:11 +00:00
|
|
|
func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, content *event.MessageEventContent, raw map[string]any, txnID string) (*gmproto.SendMessageRequest, error) {
|
2023-07-15 17:08:11 +00:00
|
|
|
log := zerolog.Ctx(ctx)
|
2024-06-13 17:46:18 +00:00
|
|
|
sim := sender.GetSIM(portal.OutgoingID)
|
2023-07-17 23:57:20 +00:00
|
|
|
req := &gmproto.SendMessageRequest{
|
2023-07-15 16:48:26 +00:00
|
|
|
ConversationID: portal.ID,
|
|
|
|
TmpID: txnID,
|
2023-07-17 13:51:31 +00:00
|
|
|
MessagePayload: &gmproto.MessagePayload{
|
2023-07-19 17:32:01 +00:00
|
|
|
ConversationID: portal.ID,
|
|
|
|
TmpID: txnID,
|
|
|
|
TmpID2: txnID,
|
|
|
|
ParticipantID: portal.OutgoingID,
|
2023-07-15 16:48:26 +00:00
|
|
|
},
|
2024-06-13 17:46:18 +00:00
|
|
|
SIMPayload: sim.GetSIMData().GetSIMPayload(),
|
2023-07-15 16:48:26 +00:00
|
|
|
}
|
|
|
|
|
2023-07-09 20:59:03 +00:00
|
|
|
replyToMXID := content.RelatesTo.GetReplyTo()
|
|
|
|
if replyToMXID != "" {
|
|
|
|
replyToMsg, err := portal.bridge.DB.Message.GetByMXID(ctx, replyToMXID)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Str("reply_to_mxid", replyToMXID.String()).Msg("Failed to get reply target message")
|
|
|
|
} else if replyToMsg == nil {
|
|
|
|
log.Warn().Str("reply_to_mxid", replyToMXID.String()).Msg("Reply target message not found")
|
|
|
|
} else {
|
2023-07-17 13:51:31 +00:00
|
|
|
req.Reply = &gmproto.ReplyPayload{MessageID: replyToMsg.ID}
|
2023-07-09 20:59:03 +00:00
|
|
|
}
|
|
|
|
}
|
2024-06-13 17:46:18 +00:00
|
|
|
req.ForceRCS = portal.Type == gmproto.ConversationType_RCS &&
|
|
|
|
portal.SendMode == gmproto.ConversationSendMode_SEND_MODE_AUTO &&
|
|
|
|
portal.ForceRCS
|
|
|
|
if req.ForceRCS && !sim.GetRCSChats().GetEnabled() {
|
|
|
|
log.Warn().Msg("Forcing RCS but RCS is disabled on sim")
|
|
|
|
}
|
2023-07-09 20:59:03 +00:00
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
switch content.MsgType {
|
|
|
|
case event.MsgText, event.MsgEmote, event.MsgNotice:
|
|
|
|
text := content.Body
|
|
|
|
if content.MsgType == event.MsgEmote {
|
|
|
|
text = "/me " + text
|
|
|
|
}
|
2023-07-17 13:51:31 +00:00
|
|
|
req.MessagePayload.MessageInfo = []*gmproto.MessageInfo{{
|
|
|
|
Data: &gmproto.MessageInfo_MessageContent{MessageContent: &gmproto.MessageContent{
|
2023-07-15 16:48:26 +00:00
|
|
|
Content: text,
|
|
|
|
}},
|
|
|
|
}}
|
|
|
|
case event.MsgImage, event.MsgVideo, event.MsgAudio, event.MsgFile:
|
2024-03-11 14:55:11 +00:00
|
|
|
resp, err := portal.reuploadMedia(ctx, sender, content, raw)
|
2023-09-01 14:29:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2023-07-15 17:08:11 +00:00
|
|
|
}
|
2023-09-01 14:29:36 +00:00
|
|
|
req.MessagePayload.MessageInfo = []*gmproto.MessageInfo{{
|
|
|
|
Data: &gmproto.MessageInfo_MediaContent{MediaContent: resp},
|
|
|
|
}}
|
2024-04-16 14:44:34 +00:00
|
|
|
if content.FileName != "" && content.FileName != content.Body {
|
|
|
|
req.MessagePayload.MessageInfo = append(req.MessagePayload.MessageInfo, &gmproto.MessageInfo{
|
|
|
|
Data: &gmproto.MessageInfo_MessageContent{MessageContent: &gmproto.MessageContent{
|
|
|
|
Content: content.Body,
|
|
|
|
}},
|
|
|
|
})
|
|
|
|
}
|
2023-09-02 10:29:48 +00:00
|
|
|
case event.MsgBeeperGallery:
|
|
|
|
for i, part := range content.BeeperGalleryImages {
|
2024-03-11 14:55:11 +00:00
|
|
|
convertedPart, err := portal.reuploadMedia(ctx, sender, part, nil)
|
2023-07-15 17:08:11 +00:00
|
|
|
if err != nil {
|
2023-09-01 14:29:36 +00:00
|
|
|
return nil, fmt.Errorf("failed to reupload gallery image #%d: %w", i+1, err)
|
2023-07-15 17:08:11 +00:00
|
|
|
}
|
2023-09-01 14:29:36 +00:00
|
|
|
req.MessagePayload.MessageInfo = append(req.MessagePayload.MessageInfo, &gmproto.MessageInfo{
|
|
|
|
Data: &gmproto.MessageInfo_MediaContent{MediaContent: convertedPart},
|
|
|
|
})
|
2023-07-15 17:08:11 +00:00
|
|
|
}
|
2023-09-02 10:29:48 +00:00
|
|
|
if content.BeeperGalleryCaption != "" {
|
2023-09-01 14:29:36 +00:00
|
|
|
req.MessagePayload.MessageInfo = append(req.MessagePayload.MessageInfo, &gmproto.MessageInfo{
|
|
|
|
Data: &gmproto.MessageInfo_MessageContent{MessageContent: &gmproto.MessageContent{
|
2023-09-02 10:29:48 +00:00
|
|
|
Content: content.BeeperGalleryCaption,
|
2023-09-01 14:29:36 +00:00
|
|
|
}},
|
|
|
|
})
|
2023-07-15 17:08:11 +00:00
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
default:
|
2023-07-15 17:08:11 +00:00
|
|
|
return nil, fmt.Errorf("%w %s", errUnknownMsgType, content.MsgType)
|
|
|
|
}
|
|
|
|
return req, nil
|
|
|
|
}
|
|
|
|
|
2024-03-11 14:55:11 +00:00
|
|
|
func (portal *Portal) reuploadMedia(ctx context.Context, sender *User, content *event.MessageEventContent, raw map[string]any) (*gmproto.MediaContent, error) {
|
2023-09-01 14:29:36 +00:00
|
|
|
var url id.ContentURI
|
|
|
|
if content.File != nil {
|
|
|
|
url = content.File.URL.ParseOrIgnore()
|
|
|
|
} else {
|
|
|
|
url = content.URL.ParseOrIgnore()
|
|
|
|
}
|
|
|
|
if url.IsEmpty() {
|
|
|
|
return nil, errMissingMediaURL
|
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
data, err := portal.MainIntent().DownloadBytes(ctx, url)
|
2023-09-01 14:29:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, exerrors.NewDualError(errMediaDownloadFailed, err)
|
|
|
|
}
|
|
|
|
if content.File != nil {
|
|
|
|
err = content.File.DecryptInPlace(data)
|
|
|
|
if err != nil {
|
|
|
|
return nil, exerrors.NewDualError(errMediaDecryptFailed, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if content.Info.MimeType == "" {
|
|
|
|
content.Info.MimeType = mimetype.Detect(data).String()
|
|
|
|
}
|
|
|
|
fileName := content.Body
|
|
|
|
if content.FileName != "" {
|
|
|
|
fileName = content.FileName
|
|
|
|
}
|
2024-03-11 15:09:02 +00:00
|
|
|
_, isVoice := raw["org.matrix.msc3245.voice"]
|
|
|
|
if isVoice {
|
2024-03-11 14:55:11 +00:00
|
|
|
data, err = ffmpeg.ConvertBytes(ctx, data, ".m4a", []string{}, []string{"-c:a", "aac"}, content.Info.MimeType)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("%w (ogg to m4a): %w", errMediaConvertFailed, err)
|
|
|
|
}
|
|
|
|
fileName += ".m4a"
|
|
|
|
content.Info.MimeType = "audio/mp4"
|
|
|
|
}
|
2023-09-01 14:29:36 +00:00
|
|
|
resp, err := sender.Client.UploadMedia(data, fileName, content.Info.MimeType)
|
|
|
|
if err != nil {
|
|
|
|
return nil, exerrors.NewDualError(errMediaReuploadFailed, err)
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
2024-06-07 15:24:25 +00:00
|
|
|
type responseStatusError gmproto.SendMessageResponse
|
|
|
|
|
|
|
|
func (rse *responseStatusError) Error() string {
|
|
|
|
switch rse.Status {
|
|
|
|
case 0:
|
|
|
|
if rse.GoogleAccountSwitch != nil && strings.ContainsRune(rse.GoogleAccountSwitch.GetAccount(), '@') {
|
|
|
|
return "Switch back to QR pairing or log in with Google account to send messages"
|
|
|
|
}
|
|
|
|
case gmproto.SendMessageResponse_FAILURE_2:
|
|
|
|
return "Unknown permanent error"
|
|
|
|
case gmproto.SendMessageResponse_FAILURE_3:
|
|
|
|
return "Unknown temporary error"
|
|
|
|
case gmproto.SendMessageResponse_FAILURE_4:
|
|
|
|
return "Google Messages is not your default SMS app"
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("Unrecognized response status %d", rse.Status)
|
|
|
|
}
|
|
|
|
|
2023-07-15 17:08:11 +00:00
|
|
|
func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event, timings messageTimings) {
|
|
|
|
ms := metricSender{portal: portal, timings: &timings}
|
|
|
|
|
|
|
|
log := portal.zlog.With().
|
|
|
|
Str("event_id", evt.ID.String()).
|
|
|
|
Str("action", "handle matrix message").
|
|
|
|
Logger()
|
|
|
|
ctx := log.WithContext(context.TODO())
|
|
|
|
log.Debug().Dur("age", timings.totalReceive).Msg("Handling Matrix message")
|
|
|
|
|
|
|
|
content, ok := evt.Content.Parsed.(*event.MessageEventContent)
|
|
|
|
if !ok {
|
2023-07-15 16:48:26 +00:00
|
|
|
return
|
|
|
|
}
|
2023-07-15 17:08:11 +00:00
|
|
|
|
|
|
|
txnID := util.GenerateTmpID()
|
2024-05-24 09:42:28 +00:00
|
|
|
outgoingMsg := &outgoingMessage{Event: evt}
|
2023-07-15 17:08:11 +00:00
|
|
|
portal.outgoingMessagesLock.Lock()
|
2024-05-24 09:42:28 +00:00
|
|
|
portal.outgoingMessages[txnID] = outgoingMsg
|
2023-07-15 17:08:11 +00:00
|
|
|
portal.outgoingMessagesLock.Unlock()
|
|
|
|
if evt.Type == event.EventSticker {
|
|
|
|
content.MsgType = event.MsgImage
|
|
|
|
}
|
|
|
|
|
2023-07-19 21:01:05 +00:00
|
|
|
start := time.Now()
|
2024-03-11 14:55:11 +00:00
|
|
|
req, err := portal.convertMatrixMessage(ctx, sender, content, evt.Content.Raw, txnID)
|
2023-07-19 21:01:05 +00:00
|
|
|
timings.convert = time.Since(start)
|
|
|
|
if err != nil {
|
2024-02-23 19:10:31 +00:00
|
|
|
go ms.sendMessageMetrics(ctx, sender, evt, err, "Error converting", true)
|
2023-07-19 21:01:05 +00:00
|
|
|
return
|
|
|
|
}
|
2023-08-24 11:56:41 +00:00
|
|
|
log.Debug().
|
|
|
|
Str("tmp_id", req.TmpID).
|
|
|
|
Str("participant_id", req.GetMessagePayload().GetParticipantID()).
|
|
|
|
Msg("Sending Matrix message to Google Messages")
|
2023-07-19 21:01:05 +00:00
|
|
|
start = time.Now()
|
2023-08-30 16:37:04 +00:00
|
|
|
resp, err := sender.Client.SendMessage(req)
|
2023-07-19 21:01:05 +00:00
|
|
|
timings.send = time.Since(start)
|
|
|
|
if err != nil {
|
2024-05-24 09:42:28 +00:00
|
|
|
outgoingMsg.Errored = true
|
2024-02-23 19:10:31 +00:00
|
|
|
go ms.sendMessageMetrics(ctx, sender, evt, err, "Error sending", true)
|
2023-09-01 10:42:52 +00:00
|
|
|
} else if resp.Status != gmproto.SendMessageResponse_SUCCESS {
|
2024-05-24 09:42:28 +00:00
|
|
|
outgoingMsg.Errored = true
|
2024-06-08 09:47:41 +00:00
|
|
|
outgoingMsg.AckedAt = time.Now()
|
2024-06-07 15:24:25 +00:00
|
|
|
go ms.sendMessageMetrics(ctx, sender, evt, (*responseStatusError)(resp), "Error sending", true)
|
2023-07-15 16:48:26 +00:00
|
|
|
} else {
|
2024-06-08 09:47:41 +00:00
|
|
|
outgoingMsg.AckedAt = time.Now()
|
2024-02-23 19:10:31 +00:00
|
|
|
go ms.sendMessageMetrics(ctx, sender, evt, nil, "", true)
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-15 11:38:24 +00:00
|
|
|
func (portal *Portal) HandleMatrixReadReceipt(brUser bridge.User, eventID id.EventID, receipt event.ReadReceipt) {
|
|
|
|
user := brUser.(*User)
|
|
|
|
log := portal.zlog.With().
|
|
|
|
Str("event_id", eventID.String()).
|
|
|
|
Time("receipt_ts", receipt.Timestamp).
|
|
|
|
Str("action", "handle matrix read receipt").
|
|
|
|
Logger()
|
2024-01-07 19:37:47 +00:00
|
|
|
if user.Client == nil {
|
|
|
|
log.Debug().Msg("User is not connected, ignoring read receipt")
|
|
|
|
return
|
|
|
|
}
|
2023-07-15 11:38:24 +00:00
|
|
|
ctx := log.WithContext(context.TODO())
|
|
|
|
log.Debug().Msg("Handling Matrix read receipt")
|
|
|
|
targetMessage, err := portal.bridge.DB.Message.GetByMXID(ctx, eventID)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to get target message to handle read receipt")
|
|
|
|
return
|
|
|
|
} else if targetMessage == nil {
|
2023-08-10 08:30:17 +00:00
|
|
|
lastMessage, err := portal.bridge.DB.Message.GetLastInChatWithMXID(ctx, portal.Key)
|
2023-07-15 11:38:24 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to get last message to handle read receipt")
|
|
|
|
return
|
|
|
|
} else if receipt.Timestamp.Before(lastMessage.Timestamp) {
|
|
|
|
log.Debug().Msg("Ignoring read receipt for unknown message with timestamp before last message")
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
log.Debug().Msg("Marking last message in chat as read for receipt targeting unknown message")
|
|
|
|
}
|
|
|
|
targetMessage = lastMessage
|
|
|
|
}
|
|
|
|
log = log.With().Str("message_id", targetMessage.ID).Logger()
|
2023-10-05 10:08:38 +00:00
|
|
|
go func() {
|
|
|
|
err = user.Client.MarkRead(portal.ID, targetMessage.ID)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to mark message as read")
|
|
|
|
} else {
|
|
|
|
log.Debug().Msg("Marked message as read after Matrix read receipt")
|
|
|
|
}
|
|
|
|
}()
|
2023-07-15 11:38:24 +00:00
|
|
|
}
|
|
|
|
|
2023-07-02 14:21:55 +00:00
|
|
|
func (portal *Portal) HandleMatrixReaction(sender *User, evt *event.Event) {
|
2023-07-14 22:06:49 +00:00
|
|
|
err := portal.handleMatrixReaction(sender, evt)
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.sendMessageMetrics(context.TODO(), sender, evt, err, "Error sending", nil)
|
2023-07-14 22:06:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) error {
|
|
|
|
content, ok := evt.Content.Parsed.(*event.ReactionEventContent)
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("unexpected parsed content type %T", evt.Content.Parsed)
|
|
|
|
}
|
|
|
|
log := portal.zlog.With().
|
|
|
|
Str("event_id", evt.ID.String()).
|
|
|
|
Str("target_event_id", content.RelatesTo.EventID.String()).
|
|
|
|
Str("action", "handle matrix reaction").
|
|
|
|
Logger()
|
2023-07-14 22:22:20 +00:00
|
|
|
ctx := log.WithContext(context.TODO())
|
2023-07-14 22:06:49 +00:00
|
|
|
log.Debug().Msg("Handling Matrix reaction")
|
|
|
|
|
|
|
|
msg, err := portal.bridge.DB.Message.GetByMXID(ctx, content.RelatesTo.EventID)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to get reaction target event")
|
|
|
|
return fmt.Errorf("failed to get event from database")
|
|
|
|
} else if msg == nil {
|
|
|
|
return errTargetNotFound
|
|
|
|
}
|
|
|
|
|
2023-08-14 11:32:12 +00:00
|
|
|
existingReaction, err := portal.bridge.DB.Reaction.GetByID(ctx, portal.Receiver, msg.ID, portal.OutgoingID)
|
2023-07-14 22:06:49 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to get existing reaction")
|
|
|
|
return fmt.Errorf("failed to get existing reaction from database")
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
|
2024-03-07 15:05:42 +00:00
|
|
|
emoji := variationselector.FullyQualify(content.RelatesTo.Key)
|
2023-07-17 23:57:20 +00:00
|
|
|
action := gmproto.SendReactionRequest_ADD
|
2023-07-14 22:06:49 +00:00
|
|
|
if existingReaction != nil {
|
2023-07-17 23:57:20 +00:00
|
|
|
action = gmproto.SendReactionRequest_SWITCH
|
2023-07-14 22:06:49 +00:00
|
|
|
}
|
2023-07-17 23:57:20 +00:00
|
|
|
resp, err := sender.Client.SendReaction(&gmproto.SendReactionRequest{
|
2023-07-14 22:22:20 +00:00
|
|
|
MessageID: msg.ID,
|
2023-07-17 13:51:31 +00:00
|
|
|
ReactionData: gmproto.MakeReactionData(emoji),
|
2023-07-14 22:22:20 +00:00
|
|
|
Action: action,
|
2024-03-07 23:45:18 +00:00
|
|
|
SIMPayload: sender.GetSIM(portal.OutgoingID).GetSIMData().GetSIMPayload(),
|
2023-07-14 22:06:49 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to send reaction: %w", err)
|
|
|
|
} else if !resp.Success {
|
|
|
|
return fmt.Errorf("got non-success response")
|
|
|
|
}
|
|
|
|
if existingReaction == nil {
|
|
|
|
existingReaction = portal.bridge.DB.Reaction.New()
|
|
|
|
existingReaction.Chat = portal.Key
|
|
|
|
existingReaction.MessageID = msg.ID
|
2023-07-19 17:32:01 +00:00
|
|
|
existingReaction.Sender = portal.OutgoingID
|
2023-07-14 22:06:49 +00:00
|
|
|
} else if sender.DoublePuppetIntent != nil {
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = sender.DoublePuppetIntent.RedactEvent(ctx, portal.MXID, existingReaction.MXID)
|
2023-07-14 22:06:49 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to redact old reaction with double puppet after new Matrix reaction")
|
|
|
|
}
|
|
|
|
} else {
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = portal.MainIntent().RedactEvent(ctx, portal.MXID, existingReaction.MXID)
|
2023-07-14 22:06:49 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to redact old reaction with main intent after new Matrix reaction")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
existingReaction.Reaction = emoji
|
|
|
|
existingReaction.MXID = evt.ID
|
|
|
|
err = existingReaction.Insert(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to save reaction from Matrix to database")
|
|
|
|
}
|
|
|
|
return nil
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
|
2023-07-14 22:22:20 +00:00
|
|
|
func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) {
|
|
|
|
err := portal.handleMatrixRedaction(sender, evt)
|
2024-02-23 19:10:31 +00:00
|
|
|
go portal.sendMessageMetrics(context.TODO(), sender, evt, err, "Error sending", nil)
|
2023-07-14 22:22:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) handleMatrixMessageRedaction(ctx context.Context, sender *User, redacts id.EventID) error {
|
|
|
|
log := zerolog.Ctx(ctx)
|
|
|
|
msg, err := portal.bridge.DB.Message.GetByMXID(ctx, redacts)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to get redaction target message")
|
|
|
|
return fmt.Errorf("failed to get event from database")
|
|
|
|
} else if msg == nil {
|
|
|
|
return errTargetNotFound
|
|
|
|
}
|
2023-07-15 21:26:22 +00:00
|
|
|
resp, err := sender.Client.DeleteMessage(msg.ID)
|
2023-07-14 22:22:20 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to send message removal: %w", err)
|
|
|
|
} else if !resp.Success {
|
|
|
|
return fmt.Errorf("got non-success response")
|
|
|
|
}
|
|
|
|
err = msg.Delete(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to delete message from database after Matrix redaction")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) handleMatrixReactionRedaction(ctx context.Context, sender *User, redacts id.EventID) error {
|
|
|
|
log := zerolog.Ctx(ctx)
|
|
|
|
existingReaction, err := portal.bridge.DB.Reaction.GetByMXID(ctx, redacts)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to get redaction target reaction")
|
|
|
|
return fmt.Errorf("failed to get event from database")
|
|
|
|
} else if existingReaction == nil {
|
|
|
|
return errTargetNotFound
|
|
|
|
}
|
|
|
|
|
2023-07-17 23:57:20 +00:00
|
|
|
resp, err := sender.Client.SendReaction(&gmproto.SendReactionRequest{
|
2023-07-14 22:22:20 +00:00
|
|
|
MessageID: existingReaction.MessageID,
|
2023-07-17 13:51:31 +00:00
|
|
|
ReactionData: gmproto.MakeReactionData(existingReaction.Reaction),
|
2023-07-17 23:57:20 +00:00
|
|
|
Action: gmproto.SendReactionRequest_REMOVE,
|
2023-07-14 22:22:20 +00:00
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to send reaction removal: %w", err)
|
|
|
|
} else if !resp.Success {
|
|
|
|
return fmt.Errorf("got non-success response")
|
|
|
|
}
|
|
|
|
err = existingReaction.Delete(ctx)
|
|
|
|
if err != nil {
|
|
|
|
log.Err(err).Msg("Failed to remove reaction from database after Matrix redaction")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (portal *Portal) handleMatrixRedaction(sender *User, evt *event.Event) error {
|
|
|
|
log := portal.zlog.With().
|
|
|
|
Str("event_id", evt.ID.String()).
|
|
|
|
Str("target_event_id", evt.Redacts.String()).
|
|
|
|
Str("action", "handle matrix redaction").
|
|
|
|
Logger()
|
|
|
|
ctx := log.WithContext(context.TODO())
|
|
|
|
log.Debug().Msg("Handling Matrix redaction")
|
|
|
|
|
|
|
|
err := portal.handleMatrixMessageRedaction(ctx, sender, evt.Redacts)
|
|
|
|
if err == errTargetNotFound {
|
|
|
|
err = portal.handleMatrixReactionRedaction(ctx, sender, evt.Redacts)
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) Delete(ctx context.Context) {
|
|
|
|
err := portal.Portal.Delete(ctx)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to delete portal from database")
|
|
|
|
}
|
|
|
|
portal.bridge.portalsLock.Lock()
|
|
|
|
delete(portal.bridge.portalsByKey, portal.Key)
|
|
|
|
if len(portal.MXID) > 0 {
|
|
|
|
delete(portal.bridge.portalsByMXID, portal.MXID)
|
|
|
|
}
|
|
|
|
portal.bridge.portalsLock.Unlock()
|
|
|
|
}
|
|
|
|
|
2023-08-21 16:42:57 +00:00
|
|
|
func (portal *Portal) RemoveMXID(ctx context.Context) {
|
|
|
|
portal.bridge.portalsLock.Lock()
|
|
|
|
if len(portal.MXID) == 0 {
|
|
|
|
portal.bridge.portalsLock.Unlock()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
delete(portal.bridge.portalsByMXID, portal.MXID)
|
|
|
|
portal.MXID = ""
|
|
|
|
portal.NameSet = false
|
|
|
|
portal.InSpace = false
|
|
|
|
portal.Encrypted = false
|
|
|
|
portal.bridge.portalsLock.Unlock()
|
|
|
|
err := portal.bridge.DB.Message.DeleteAllInChat(ctx, portal.Key)
|
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to delete messages from database")
|
|
|
|
}
|
|
|
|
err = portal.Update(ctx)
|
|
|
|
if err != nil {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to remove portal mxid from database")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-23 19:10:31 +00:00
|
|
|
func (portal *Portal) Cleanup(ctx context.Context) {
|
2023-07-02 14:21:55 +00:00
|
|
|
if len(portal.MXID) == 0 {
|
|
|
|
return
|
|
|
|
}
|
2023-08-10 12:55:33 +00:00
|
|
|
intent := portal.bridge.Bot
|
|
|
|
if portal.IsPrivateChat() {
|
|
|
|
intent = portal.bridge.AS.Intent(portal.bridge.FormatPuppetMXID(database.Key{
|
|
|
|
ID: portal.OtherUserID,
|
|
|
|
Receiver: portal.Receiver,
|
|
|
|
}))
|
|
|
|
}
|
2023-07-02 14:21:55 +00:00
|
|
|
if portal.bridge.SpecVersions.Supports(mautrix.BeeperFeatureRoomYeeting) {
|
2024-02-23 19:10:31 +00:00
|
|
|
err := intent.BeeperDeleteRoom(ctx, portal.MXID)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil && !errors.Is(err, mautrix.MNotFound) {
|
|
|
|
portal.zlog.Err(err).Msg("Failed to delete room using hungryserv yeet endpoint")
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
members, err := intent.JoinedMembers(ctx, portal.MXID)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
2023-07-19 21:01:05 +00:00
|
|
|
portal.zlog.Err(err).Msg("Failed to get portal members for cleanup")
|
2023-07-02 14:21:55 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
for member := range members.Joined {
|
|
|
|
if member == intent.UserID {
|
|
|
|
continue
|
|
|
|
}
|
2023-08-10 12:55:33 +00:00
|
|
|
if portal.bridge.IsGhost(member) {
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = portal.bridge.AS.Intent(member).LeaveRoom(ctx, portal.MXID)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
2023-07-19 21:01:05 +00:00
|
|
|
portal.zlog.Err(err).Msg("Failed to leave as puppet while cleaning up portal")
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
2023-08-10 12:55:33 +00:00
|
|
|
} else {
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = intent.KickUser(ctx, portal.MXID, &mautrix.ReqKickUser{UserID: member, Reason: "Deleting portal"})
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
2023-07-19 21:01:05 +00:00
|
|
|
portal.zlog.Err(err).Msg("Failed to kick user while cleaning up portal")
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-02-23 19:10:31 +00:00
|
|
|
_, err = intent.LeaveRoom(ctx, portal.MXID)
|
2023-07-02 14:21:55 +00:00
|
|
|
if err != nil {
|
2023-07-19 21:01:05 +00:00
|
|
|
portal.zlog.Err(err).Msg("Failed to leave with main intent while cleaning up portal")
|
2023-07-02 14:21:55 +00:00
|
|
|
}
|
|
|
|
}
|