2018-08-16 14:59:18 +02:00
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
2021-10-22 19:14:34 +02:00
// Copyright (C) 2021 Tulir Asokan
2018-08-16 14:59:18 +02:00
//
// 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 (
2018-08-24 18:46:14 +02:00
"bytes"
2021-10-22 19:14:34 +02:00
"context"
2022-12-23 14:17:57 +01:00
"crypto/rand"
2022-11-17 22:30:42 +01:00
"crypto/sha256"
"encoding/hex"
2022-02-10 18:18:49 +01:00
"encoding/json"
2020-10-05 21:38:34 +02:00
"errors"
2018-08-16 14:59:18 +02:00
"fmt"
2018-08-24 18:46:14 +02:00
"image"
2022-08-24 20:06:27 +02:00
"image/color"
2021-11-11 19:33:22 +01:00
_ "image/gif"
2018-08-25 23:26:24 +02:00
"image/jpeg"
"image/png"
2022-09-28 13:45:36 +02:00
"io"
2020-06-10 13:58:57 +02:00
"math"
2018-08-24 18:46:14 +02:00
"mime"
"net/http"
2022-11-17 23:20:14 +01:00
"reflect"
2022-09-29 13:39:58 +02:00
"runtime/debug"
2021-08-02 11:53:38 +02:00
"strconv"
2018-08-18 21:57:08 +02:00
"strings"
2018-08-23 00:12:26 +02:00
"sync"
2019-05-22 15:46:18 +02:00
"time"
2018-08-24 18:46:14 +02:00
2022-08-22 14:00:01 +02:00
"github.com/chai2010/webp"
2022-02-10 18:18:49 +01:00
"github.com/tidwall/gjson"
2023-05-24 11:41:54 +02:00
"golang.org/x/exp/slices"
2021-11-11 19:33:22 +01:00
"golang.org/x/image/draw"
2021-10-22 19:14:34 +02:00
"google.golang.org/protobuf/proto"
2021-02-17 00:21:30 +01:00
log "maunium.net/go/maulogger/v2"
2021-05-12 12:39:24 +02:00
2019-01-11 20:17:31 +01:00
"maunium.net/go/mautrix"
2020-05-09 13:31:06 +02:00
"maunium.net/go/mautrix/appservice"
2022-05-22 00:06:30 +02:00
"maunium.net/go/mautrix/bridge"
2022-05-30 23:27:43 +02:00
"maunium.net/go/mautrix/bridge/bridgeconfig"
2021-02-17 00:21:30 +01:00
"maunium.net/go/mautrix/crypto/attachment"
2020-05-08 21:32:22 +02:00
"maunium.net/go/mautrix/event"
2021-12-25 19:50:36 +01:00
"maunium.net/go/mautrix/format"
2020-05-08 21:32:22 +02:00
"maunium.net/go/mautrix/id"
2022-01-04 01:02:06 +01:00
"maunium.net/go/mautrix/util"
2022-08-14 18:26:42 +02:00
"maunium.net/go/mautrix/util/dbutil"
2022-01-04 01:02:06 +01:00
"maunium.net/go/mautrix/util/ffmpeg"
2022-03-18 00:12:23 +01:00
"maunium.net/go/mautrix/util/variationselector"
2019-01-11 20:17:31 +01:00
2021-12-25 19:50:36 +01:00
"go.mau.fi/whatsmeow"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
2018-08-24 18:46:14 +02:00
"maunium.net/go/mautrix-whatsapp/database"
2018-08-16 14:59:18 +02:00
)
2021-02-21 13:18:15 +01:00
const StatusBroadcastTopic = "WhatsApp status updates from your contacts"
const StatusBroadcastName = "WhatsApp Status Broadcast"
const BroadcastTopic = "WhatsApp broadcast list"
const UnnamedBroadcastName = "Unnamed broadcast list"
2021-06-01 14:28:15 +02:00
const PrivateChatTopic = "WhatsApp private chat"
2021-06-21 12:52:59 +02:00
2021-06-01 14:28:15 +02:00
var ErrStatusBroadcastDisabled = errors . New ( "status bridging is disabled" )
2021-02-21 13:18:15 +01:00
2022-05-22 00:06:30 +02:00
func ( br * WABridge ) GetPortalByMXID ( mxid id . RoomID ) * Portal {
br . portalsLock . Lock ( )
defer br . portalsLock . Unlock ( )
portal , ok := br . portalsByMXID [ mxid ]
2018-08-16 14:59:18 +02:00
if ! ok {
2022-05-22 00:06:30 +02:00
return br . loadDBPortal ( br . DB . Portal . GetByMXID ( mxid ) , nil )
2018-08-16 14:59:18 +02:00
}
return portal
}
2022-05-22 15:15:54 +02:00
func ( br * WABridge ) 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
portal . Update ( nil )
}
func ( portal * Portal ) ReceiveMatrixEvent ( user bridge . User , evt * event . Event ) {
2022-05-22 17:21:12 +02:00
if user . GetPermissionLevel ( ) >= bridgeconfig . PermissionLevelUser || portal . HasRelaybot ( ) {
2022-07-11 13:20:31 +02:00
portal . matrixMessages <- PortalMatrixMessage { user : user . ( * User ) , evt : evt , receivedAt : time . Now ( ) }
2022-05-22 15:15:54 +02:00
}
2022-05-22 00:06:30 +02:00
}
func ( br * WABridge ) GetPortalByJID ( key database . PortalKey ) * Portal {
br . portalsLock . Lock ( )
defer br . portalsLock . Unlock ( )
portal , ok := br . portalsByJID [ key ]
2018-08-16 14:59:18 +02:00
if ! ok {
2022-05-22 00:06:30 +02:00
return br . loadDBPortal ( br . DB . Portal . GetByJID ( key ) , & key )
2018-08-16 14:59:18 +02:00
}
return portal
}
2023-03-08 16:14:31 +01:00
func ( br * WABridge ) GetExistingPortalByJID ( key database . PortalKey ) * Portal {
br . portalsLock . Lock ( )
defer br . portalsLock . Unlock ( )
portal , ok := br . portalsByJID [ key ]
if ! ok {
return br . loadDBPortal ( br . DB . Portal . GetByJID ( key ) , nil )
}
return portal
}
2022-05-22 00:06:30 +02:00
func ( br * WABridge ) GetAllPortals ( ) [ ] * Portal {
return br . dbPortalsToPortals ( br . DB . Portal . GetAll ( ) )
2019-06-01 19:03:29 +02:00
}
2022-06-27 10:46:30 +02:00
func ( br * WABridge ) GetAllIPortals ( ) ( iportals [ ] bridge . Portal ) {
portals := br . GetAllPortals ( )
iportals = make ( [ ] bridge . Portal , len ( portals ) )
for i , portal := range portals {
iportals [ i ] = portal
}
return iportals
}
2022-05-22 00:06:30 +02:00
func ( br * WABridge ) GetAllPortalsByJID ( jid types . JID ) [ ] * Portal {
return br . dbPortalsToPortals ( br . DB . Portal . GetAllByJID ( jid ) )
2019-06-01 19:03:29 +02:00
}
2022-12-14 00:00:12 +01:00
func ( br * WABridge ) GetAllByParentGroup ( jid types . JID ) [ ] * Portal {
return br . dbPortalsToPortals ( br . DB . Portal . GetAllByParentGroup ( jid ) )
}
2022-05-22 00:06:30 +02:00
func ( br * WABridge ) dbPortalsToPortals ( dbPortals [ ] * database . Portal ) [ ] * Portal {
br . portalsLock . Lock ( )
defer br . portalsLock . Unlock ( )
2018-08-16 14:59:18 +02:00
output := make ( [ ] * Portal , len ( dbPortals ) )
for index , dbPortal := range dbPortals {
2019-06-13 20:30:38 +02:00
if dbPortal == nil {
continue
}
2022-05-22 00:06:30 +02:00
portal , ok := br . portalsByJID [ dbPortal . Key ]
2018-08-16 14:59:18 +02:00
if ! ok {
2022-05-22 00:06:30 +02:00
portal = br . loadDBPortal ( dbPortal , nil )
2018-08-16 14:59:18 +02:00
}
output [ index ] = portal
}
return output
}
2022-05-22 00:06:30 +02:00
func ( br * WABridge ) loadDBPortal ( dbPortal * database . Portal , key * database . PortalKey ) * Portal {
2019-05-28 20:31:25 +02:00
if dbPortal == nil {
if key == nil {
return nil
}
2022-05-22 00:06:30 +02:00
dbPortal = br . DB . Portal . New ( )
2019-05-28 20:31:25 +02:00
dbPortal . Key = * key
dbPortal . Insert ( )
}
2022-05-22 00:06:30 +02:00
portal := br . NewPortal ( dbPortal )
br . portalsByJID [ portal . Key ] = portal
2019-05-28 20:31:25 +02:00
if len ( portal . MXID ) > 0 {
2022-05-22 00:06:30 +02:00
br . portalsByMXID [ portal . MXID ] = portal
2019-05-28 20:31:25 +02:00
}
return portal
}
func ( portal * Portal ) GetUsers ( ) [ ] * User {
return nil
}
2022-05-22 00:06:30 +02:00
func ( br * WABridge ) newBlankPortal ( key database . PortalKey ) * Portal {
2020-07-05 17:57:03 +02:00
portal := & Portal {
2022-05-22 00:06:30 +02:00
bridge : br ,
log : br . Log . Sub ( fmt . Sprintf ( "Portal/%s" , key ) ) ,
2020-07-05 17:57:03 +02:00
2022-05-22 00:06:30 +02:00
messages : make ( chan PortalMessage , br . Config . Bridge . PortalMessageBuffer ) ,
matrixMessages : make ( chan PortalMatrixMessage , br . Config . Bridge . PortalMessageBuffer ) ,
mediaRetries : make ( chan PortalMediaRetry , br . Config . Bridge . PortalMessageBuffer ) ,
2022-02-10 18:18:49 +01:00
mediaErrorCache : make ( map [ types . MessageID ] * FailedMediaMeta ) ,
2020-07-05 17:57:03 +02:00
}
go portal . handleMessageLoop ( )
return portal
}
2022-05-22 00:06:30 +02:00
func ( br * WABridge ) NewManualPortal ( key database . PortalKey ) * Portal {
portal := br . newBlankPortal ( key )
portal . Portal = br . DB . Portal . New ( )
2022-02-10 18:18:49 +01:00
portal . Key = key
return portal
}
2018-08-30 23:13:08 +02:00
2022-05-22 00:06:30 +02:00
func ( br * WABridge ) NewPortal ( dbPortal * database . Portal ) * Portal {
portal := br . newBlankPortal ( dbPortal . Key )
2022-02-10 18:18:49 +01:00
portal . Portal = dbPortal
2019-05-21 22:44:14 +02:00
return portal
2018-08-16 14:59:18 +02:00
}
2018-09-01 23:01:22 +02:00
const recentlyHandledLength = 100
2021-11-02 14:46:31 +01:00
type fakeMessage struct {
2021-11-09 16:49:34 +01:00
Sender types . JID
Text string
ID string
Time time . Time
Important bool
2021-11-02 14:46:31 +01:00
}
2019-05-21 22:44:14 +02:00
type PortalMessage struct {
2021-10-27 17:31:33 +02:00
evt * events . Message
undecryptable * events . UndecryptableMessage
2022-05-02 14:35:47 +02:00
receipt * events . Receipt
2021-11-02 14:46:31 +01:00
fake * fakeMessage
2021-10-27 17:31:33 +02:00
source * User
}
2021-12-14 16:47:30 +01:00
type PortalMatrixMessage struct {
2022-07-11 13:20:31 +02:00
evt * event . Event
user * User
receivedAt time . Time
2021-12-14 16:47:30 +01:00
}
2022-02-10 18:18:49 +01:00
type PortalMediaRetry struct {
evt * events . MediaRetry
source * User
}
2021-10-27 17:31:33 +02:00
type recentlyHandledWrapper struct {
id types . MessageID
2022-02-10 18:18:49 +01:00
err database . MessageErrorType
2019-05-21 22:44:14 +02:00
}
2018-08-16 14:59:18 +02:00
type Portal struct {
* database . Portal
2022-05-22 00:06:30 +02:00
bridge * WABridge
2018-08-16 18:20:07 +02:00
log log . Logger
2018-08-23 00:12:26 +02:00
2019-05-28 13:12:35 +02:00
roomCreateLock sync . Mutex
2021-02-25 16:22:29 +01:00
encryptLock sync . Mutex
2021-10-26 16:01:10 +02:00
backfillLock sync . Mutex
2022-02-10 11:46:25 +01:00
avatarLock sync . Mutex
2018-08-30 23:13:08 +02:00
2022-05-24 12:39:29 +02:00
latestEventBackfillLock sync . Mutex
2022-12-02 14:36:19 +01:00
parentGroupUpdateLock sync . Mutex
2022-05-24 12:39:29 +02:00
2021-10-27 17:31:33 +02:00
recentlyHandled [ recentlyHandledLength ] recentlyHandledWrapper
2018-08-30 23:13:08 +02:00
recentlyHandledLock sync . Mutex
recentlyHandledIndex uint8
2018-09-01 22:38:03 +02:00
2021-12-07 15:02:51 +01:00
currentlyTyping [ ] id . UserID
currentlyTypingLock sync . Mutex
2021-12-14 16:47:30 +01:00
messages chan PortalMessage
matrixMessages chan PortalMatrixMessage
2022-02-10 18:18:49 +01:00
mediaRetries chan PortalMediaRetry
mediaErrorCache map [ types . MessageID ] * FailedMediaMeta
2019-05-21 22:44:14 +02:00
2023-01-16 16:15:04 +01:00
currentlySleepingToDelete sync . Map
2022-12-02 14:36:19 +01:00
relayUser * User
parentPortal * Portal
2018-08-30 23:13:08 +02:00
}
2022-10-05 20:18:48 +02:00
var (
_ bridge . Portal = ( * Portal ) ( nil )
_ bridge . ReadReceiptHandlingPortal = ( * Portal ) ( nil )
_ bridge . MembershipHandlingPortal = ( * Portal ) ( nil )
_ bridge . MetaHandlingPortal = ( * Portal ) ( nil )
_ bridge . TypingPortal = ( * Portal ) ( nil )
)
2021-12-14 16:47:30 +01:00
func ( portal * Portal ) handleMessageLoopItem ( msg PortalMessage ) {
if len ( portal . MXID ) == 0 {
2021-12-16 09:32:52 +01:00
if msg . fake == nil && msg . undecryptable == nil && ( msg . evt == nil || ! containsSupportedMessage ( msg . evt . Message ) ) {
2021-12-14 16:47:30 +01:00
portal . log . Debugln ( "Not creating portal room for incoming message: message is not a chat message" )
return
2020-11-16 13:28:08 +01:00
}
2021-12-14 16:47:30 +01:00
portal . log . Debugln ( "Creating Matrix room from incoming message" )
2022-03-25 07:15:52 +01:00
err := portal . CreateMatrixRoom ( msg . source , nil , false , true )
2021-12-14 16:47:30 +01:00
if err != nil {
portal . log . Errorln ( "Failed to create portal room:" , err )
return
}
}
2022-05-24 12:39:29 +02:00
portal . latestEventBackfillLock . Lock ( )
defer portal . latestEventBackfillLock . Unlock ( )
2022-05-24 13:02:06 +02:00
switch {
case msg . evt != nil :
2021-12-14 16:47:30 +01:00
portal . handleMessage ( msg . source , msg . evt )
2022-05-24 13:02:06 +02:00
case msg . receipt != nil :
2022-05-02 14:35:47 +02:00
portal . handleReceipt ( msg . receipt , msg . source )
2022-05-24 13:02:06 +02:00
case msg . undecryptable != nil :
2021-12-14 16:47:30 +01:00
portal . handleUndecryptableMessage ( msg . source , msg . undecryptable )
2022-05-24 13:02:06 +02:00
case msg . fake != nil :
2021-12-14 16:47:30 +01:00
msg . fake . ID = "FAKE::" + msg . fake . ID
portal . handleFakeMessage ( * msg . fake )
2022-05-24 13:02:06 +02:00
default :
2021-12-14 16:47:30 +01:00
portal . log . Warnln ( "Unexpected PortalMessage with no message: %+v" , msg )
}
}
func ( portal * Portal ) handleMatrixMessageLoopItem ( msg PortalMatrixMessage ) {
2023-03-02 13:14:49 +01:00
portal . latestEventBackfillLock . Lock ( )
defer portal . latestEventBackfillLock . Unlock ( )
2022-07-11 13:20:31 +02: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 ) ,
}
implicitRRStart := time . Now ( )
portal . handleMatrixReadReceipt ( msg . user , "" , evtTS , false )
timings . implicitRR = time . Since ( implicitRRStart )
2021-12-14 16:47:30 +01:00
switch msg . evt . Type {
2022-12-23 14:17:57 +01:00
case event . EventMessage , event . EventSticker , TypeMSC3381V2PollResponse , TypeMSC3381PollResponse , TypeMSC3381PollStart :
2022-07-11 13:20:31 +02:00
portal . HandleMatrixMessage ( msg . user , msg . evt , timings )
2021-12-14 16:47:30 +01:00
case event . EventRedaction :
portal . HandleMatrixRedaction ( msg . user , msg . evt )
2022-05-22 15:15:54 +02:00
case event . EventReaction :
portal . HandleMatrixReaction ( msg . user , msg . evt )
2021-12-14 16:47:30 +01:00
default :
portal . log . Warnln ( "Unsupported event type %+v in portal message channel" , msg . evt . Type )
}
}
2022-01-19 13:18:32 +01:00
func ( portal * Portal ) handleReceipt ( receipt * events . Receipt , source * User ) {
// The order of the message ID array depends on the sender's platform, so we just have to find
// the last message based on timestamp. Also, timestamps only have second precision, so if
// there are many messages at the same second just mark them all as read, because we don't
// know which one is last
markAsRead := make ( [ ] * database . Message , 0 , 1 )
var bestTimestamp time . Time
for _ , msgID := range receipt . MessageIDs {
msg := portal . bridge . DB . Message . GetByJID ( portal . Key , msgID )
if msg == nil || msg . IsFakeMXID ( ) {
continue
}
if msg . Timestamp . After ( bestTimestamp ) {
bestTimestamp = msg . Timestamp
markAsRead = append ( markAsRead [ : 0 ] , msg )
} else if msg != nil && msg . Timestamp . Equal ( bestTimestamp ) {
markAsRead = append ( markAsRead , msg )
}
}
if receipt . Sender . User == source . JID . User {
if len ( markAsRead ) > 0 {
source . SetLastReadTS ( portal . Key , markAsRead [ 0 ] . Timestamp )
} else {
source . SetLastReadTS ( portal . Key , receipt . Timestamp )
}
}
intent := portal . bridge . GetPuppetByJID ( receipt . Sender ) . IntentFor ( portal )
for _ , msg := range markAsRead {
2022-06-30 19:56:25 +02:00
err := intent . SetReadMarkers ( portal . MXID , source . makeReadMarkerContent ( msg . MXID , intent . IsCustomPuppet ) )
2022-01-19 13:18:32 +01:00
if err != nil {
portal . log . Warnfln ( "Failed to mark message %s as read by %s: %v" , msg . MXID , intent . UserID , err )
} else {
portal . log . Debugfln ( "Marked %s as read by %s" , msg . MXID , intent . UserID )
}
}
}
2021-12-14 16:47:30 +01:00
func ( portal * Portal ) handleMessageLoop ( ) {
for {
select {
case msg := <- portal . messages :
portal . handleMessageLoopItem ( msg )
case msg := <- portal . matrixMessages :
portal . handleMatrixMessageLoopItem ( msg )
2022-02-10 18:18:49 +01:00
case retry := <- portal . mediaRetries :
portal . handleMediaRetry ( retry . evt , retry . source )
2021-10-27 17:31:33 +02:00
}
2020-11-16 13:28:08 +01:00
}
}
2021-11-01 14:30:56 +01:00
func containsSupportedMessage ( waMsg * waProto . Message ) bool {
if waMsg == nil {
return false
2021-03-19 20:14:01 +01:00
}
2021-11-01 14:30:56 +01:00
return waMsg . Conversation != nil || waMsg . ExtendedTextMessage != nil || waMsg . ImageMessage != nil ||
waMsg . StickerMessage != nil || waMsg . AudioMessage != nil || waMsg . VideoMessage != nil ||
waMsg . DocumentMessage != nil || waMsg . ContactMessage != nil || waMsg . LocationMessage != nil ||
2022-06-24 20:50:58 +02:00
waMsg . LiveLocationMessage != nil || waMsg . GroupInviteMessage != nil || waMsg . ContactsArrayMessage != nil ||
2022-06-24 22:25:37 +02:00
waMsg . HighlyStructuredMessage != nil || waMsg . TemplateMessage != nil || waMsg . TemplateButtonReplyMessage != nil ||
2022-12-21 22:20:23 +01:00
waMsg . ListMessage != nil || waMsg . ListResponseMessage != nil || waMsg . PollCreationMessage != nil || waMsg . PollCreationMessageV2 != nil
2021-03-19 20:14:01 +01:00
}
2021-11-01 14:30:56 +01:00
func getMessageType ( waMsg * waProto . Message ) string {
2021-10-26 16:01:10 +02:00
switch {
case waMsg == nil :
return "ignore"
2022-01-04 14:19:25 +01:00
case waMsg . Conversation != nil , waMsg . ExtendedTextMessage != nil :
2021-10-26 16:01:10 +02:00
return "text"
case waMsg . ImageMessage != nil :
return fmt . Sprintf ( "image %s" , waMsg . GetImageMessage ( ) . GetMimetype ( ) )
case waMsg . StickerMessage != nil :
return fmt . Sprintf ( "sticker %s" , waMsg . GetStickerMessage ( ) . GetMimetype ( ) )
case waMsg . VideoMessage != nil :
return fmt . Sprintf ( "video %s" , waMsg . GetVideoMessage ( ) . GetMimetype ( ) )
case waMsg . AudioMessage != nil :
return fmt . Sprintf ( "audio %s" , waMsg . GetAudioMessage ( ) . GetMimetype ( ) )
case waMsg . DocumentMessage != nil :
return fmt . Sprintf ( "document %s" , waMsg . GetDocumentMessage ( ) . GetMimetype ( ) )
case waMsg . ContactMessage != nil :
return "contact"
2022-01-03 15:11:39 +01:00
case waMsg . ContactsArrayMessage != nil :
return "contact array"
2021-10-26 16:01:10 +02:00
case waMsg . LocationMessage != nil :
return "location"
2021-12-07 13:51:56 +01:00
case waMsg . LiveLocationMessage != nil :
return "live location start"
2021-10-31 19:42:53 +01:00
case waMsg . GroupInviteMessage != nil :
return "group invite"
2022-01-04 14:19:25 +01:00
case waMsg . ReactionMessage != nil :
return "reaction"
2022-11-26 18:36:21 +01:00
case waMsg . EncReactionMessage != nil :
return "encrypted reaction"
2022-12-21 22:20:23 +01:00
case waMsg . PollCreationMessage != nil || waMsg . PollCreationMessageV2 != nil :
2022-11-17 22:02:01 +01:00
return "poll create"
case waMsg . PollUpdateMessage != nil :
return "poll update"
2021-10-31 19:42:53 +01:00
case waMsg . ProtocolMessage != nil :
2021-10-26 16:01:10 +02:00
switch waMsg . GetProtocolMessage ( ) . GetType ( ) {
case waProto . ProtocolMessage_REVOKE :
2022-02-18 11:12:15 +01:00
if waMsg . GetProtocolMessage ( ) . GetKey ( ) == nil {
return "ignore"
}
2021-10-26 16:01:10 +02:00
return "revoke"
2022-10-08 16:46:42 +02:00
case waProto . ProtocolMessage_MESSAGE_EDIT :
return "edit"
2022-01-07 13:32:00 +01:00
case waProto . ProtocolMessage_EPHEMERAL_SETTING :
return "disappearing timer change"
2021-10-26 20:30:42 +02:00
case waProto . ProtocolMessage_APP_STATE_SYNC_KEY_SHARE , waProto . ProtocolMessage_HISTORY_SYNC_NOTIFICATION , waProto . ProtocolMessage_INITIAL_SECURITY_NOTIFICATION_SETTING_SYNC :
2021-10-26 16:01:10 +02:00
return "ignore"
default :
2022-01-04 14:19:25 +01:00
return fmt . Sprintf ( "unknown_protocol_%d" , waMsg . GetProtocolMessage ( ) . GetType ( ) )
2021-10-26 16:01:10 +02:00
}
2022-01-04 14:19:25 +01:00
case waMsg . ButtonsMessage != nil :
return "buttons"
case waMsg . ButtonsResponseMessage != nil :
return "buttons response"
case waMsg . TemplateMessage != nil :
return "template"
case waMsg . HighlyStructuredMessage != nil :
return "highly structured template"
case waMsg . TemplateButtonReplyMessage != nil :
return "template button reply"
case waMsg . InteractiveMessage != nil :
return "interactive"
case waMsg . ListMessage != nil :
return "list"
case waMsg . ProductMessage != nil :
return "product"
case waMsg . ListResponseMessage != nil :
return "list response"
case waMsg . OrderMessage != nil :
return "order"
case waMsg . InvoiceMessage != nil :
return "invoice"
case waMsg . SendPaymentMessage != nil , waMsg . RequestPaymentMessage != nil ,
waMsg . DeclinePaymentRequestMessage != nil , waMsg . CancelPaymentRequestMessage != nil ,
waMsg . PaymentInviteMessage != nil :
return "payment"
case waMsg . Call != nil :
return "call"
case waMsg . Chat != nil :
return "chat"
case waMsg . SenderKeyDistributionMessage != nil , waMsg . StickerSyncRmrMessage != nil :
2021-10-31 19:58:30 +01:00
return "ignore"
2022-01-04 14:19:25 +01:00
default :
return "unknown"
2021-10-22 19:14:34 +02:00
}
2021-10-26 16:01:10 +02:00
}
2022-01-07 13:32:00 +01:00
func pluralUnit ( val int , name string ) string {
if val == 1 {
return fmt . Sprintf ( "%d %s" , val , name )
} else if val == 0 {
return ""
}
return fmt . Sprintf ( "%d %ss" , val , name )
}
func naturalJoin ( parts [ ] string ) string {
if len ( parts ) == 0 {
return ""
} else if len ( parts ) == 1 {
return parts [ 0 ]
} else if len ( parts ) == 2 {
return fmt . Sprintf ( "%s and %s" , parts [ 0 ] , parts [ 1 ] )
} else {
return fmt . Sprintf ( "%s and %s" , strings . Join ( parts [ : len ( parts ) - 1 ] , ", " ) , parts [ len ( parts ) - 1 ] )
}
}
func formatDuration ( d time . Duration ) string {
const Day = time . Hour * 24
var days , hours , minutes , seconds int
days , d = int ( d / Day ) , d % Day
hours , d = int ( d / time . Hour ) , d % time . Hour
minutes , d = int ( d / time . Minute ) , d % time . Minute
seconds = int ( d / time . Second )
parts := make ( [ ] string , 0 , 4 )
if days > 0 {
parts = append ( parts , pluralUnit ( days , "day" ) )
}
if hours > 0 {
parts = append ( parts , pluralUnit ( hours , "hour" ) )
}
if minutes > 0 {
parts = append ( parts , pluralUnit ( seconds , "minute" ) )
}
if seconds > 0 {
parts = append ( parts , pluralUnit ( seconds , "second" ) )
}
return naturalJoin ( parts )
}
2022-05-02 14:00:57 +02:00
func ( portal * Portal ) convertMessage ( intent * appservice . IntentAPI , source * User , info * types . MessageInfo , waMsg * waProto . Message , isBackfill bool ) * ConvertedMessage {
2021-10-22 19:14:34 +02:00
switch {
case waMsg . Conversation != nil || waMsg . ExtendedTextMessage != nil :
2022-02-04 21:19:55 +01:00
return portal . convertTextMessage ( intent , source , waMsg )
2022-06-24 20:50:58 +02:00
case waMsg . TemplateMessage != nil :
return portal . convertTemplateMessage ( intent , source , info , waMsg . GetTemplateMessage ( ) )
case waMsg . HighlyStructuredMessage != nil :
return portal . convertTemplateMessage ( intent , source , info , waMsg . GetHighlyStructuredMessage ( ) . GetHydratedHsm ( ) )
2022-06-24 21:37:27 +02:00
case waMsg . TemplateButtonReplyMessage != nil :
return portal . convertTemplateButtonReplyMessage ( intent , waMsg . GetTemplateButtonReplyMessage ( ) )
2022-06-24 22:25:37 +02:00
case waMsg . ListMessage != nil :
return portal . convertListMessage ( intent , source , waMsg . GetListMessage ( ) )
case waMsg . ListResponseMessage != nil :
return portal . convertListResponseMessage ( intent , waMsg . GetListResponseMessage ( ) )
2022-11-17 22:02:01 +01:00
case waMsg . PollCreationMessage != nil :
return portal . convertPollCreationMessage ( intent , waMsg . GetPollCreationMessage ( ) )
2022-12-21 22:20:23 +01:00
case waMsg . PollCreationMessageV2 != nil :
return portal . convertPollCreationMessage ( intent , waMsg . GetPollCreationMessageV2 ( ) )
2022-11-17 22:30:42 +01:00
case waMsg . PollUpdateMessage != nil :
return portal . convertPollUpdateMessage ( intent , source , info , waMsg . GetPollUpdateMessage ( ) )
2021-10-22 19:14:34 +02:00
case waMsg . ImageMessage != nil :
2022-05-02 14:00:57 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetImageMessage ( ) , "photo" , isBackfill )
2021-10-22 19:14:34 +02:00
case waMsg . StickerMessage != nil :
2022-05-02 14:00:57 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetStickerMessage ( ) , "sticker" , isBackfill )
2021-10-22 19:14:34 +02:00
case waMsg . VideoMessage != nil :
2022-05-02 14:00:57 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetVideoMessage ( ) , "video attachment" , isBackfill )
2021-10-22 19:14:34 +02:00
case waMsg . AudioMessage != nil :
2022-05-02 14:00:57 +02:00
typeName := "audio attachment"
if waMsg . GetAudioMessage ( ) . GetPtt ( ) {
typeName = "voice message"
}
return portal . convertMediaMessage ( intent , source , info , waMsg . GetAudioMessage ( ) , typeName , isBackfill )
2021-10-22 19:14:34 +02:00
case waMsg . DocumentMessage != nil :
2022-05-02 14:00:57 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetDocumentMessage ( ) , "file attachment" , isBackfill )
2021-10-22 19:14:34 +02:00
case waMsg . ContactMessage != nil :
2021-10-26 16:01:10 +02:00
return portal . convertContactMessage ( intent , waMsg . GetContactMessage ( ) )
2022-01-03 15:11:39 +01:00
case waMsg . ContactsArrayMessage != nil :
return portal . convertContactsArrayMessage ( intent , waMsg . GetContactsArrayMessage ( ) )
2021-10-22 19:14:34 +02:00
case waMsg . LocationMessage != nil :
2021-10-26 16:01:10 +02:00
return portal . convertLocationMessage ( intent , waMsg . GetLocationMessage ( ) )
2021-12-07 13:51:56 +01:00
case waMsg . LiveLocationMessage != nil :
return portal . convertLiveLocationMessage ( intent , waMsg . GetLiveLocationMessage ( ) )
2021-10-31 19:42:53 +01:00
case waMsg . GroupInviteMessage != nil :
return portal . convertGroupInviteMessage ( intent , info , waMsg . GetGroupInviteMessage ( ) )
2022-01-07 13:32:00 +01:00
case waMsg . ProtocolMessage != nil && waMsg . ProtocolMessage . GetType ( ) == waProto . ProtocolMessage_EPHEMERAL_SETTING :
portal . ExpirationTime = waMsg . ProtocolMessage . GetEphemeralExpiration ( )
2022-05-13 01:56:40 +02:00
portal . Update ( nil )
2022-01-07 13:32:00 +01:00
return & ConvertedMessage {
Intent : intent ,
Type : event . EventMessage ,
Content : & event . MessageEventContent {
2022-01-07 14:05:09 +01:00
Body : portal . formatDisappearingMessageNotice ( ) ,
2022-01-07 13:32:00 +01:00
MsgType : event . MsgNotice ,
} ,
}
2019-05-31 21:30:57 +02:00
default :
2021-10-26 16:01:10 +02:00
return nil
}
}
2022-01-07 14:05:09 +01:00
func ( portal * Portal ) UpdateGroupDisappearingMessages ( sender * types . JID , timestamp time . Time , timer uint32 ) {
2022-05-15 13:01:23 +02:00
if portal . ExpirationTime == timer {
return
}
2022-01-07 14:05:09 +01:00
portal . ExpirationTime = timer
2022-05-13 01:56:40 +02:00
portal . Update ( nil )
2022-01-07 14:05:09 +01:00
intent := portal . MainIntent ( )
if sender != nil {
intent = portal . bridge . GetPuppetByJID ( sender . ToNonAD ( ) ) . IntentFor ( portal )
} else {
sender = & types . EmptyJID
}
_ , err := portal . sendMessage ( intent , event . EventMessage , & event . MessageEventContent {
Body : portal . formatDisappearingMessageNotice ( ) ,
MsgType : event . MsgNotice ,
} , nil , timestamp . UnixMilli ( ) )
if err != nil {
portal . log . Warnfln ( "Failed to notify portal about disappearing message timer change by %s to %d" , * sender , timer )
}
}
func ( portal * Portal ) formatDisappearingMessageNotice ( ) string {
if portal . ExpirationTime == 0 {
return "Turned off disappearing messages"
}
2023-01-16 16:15:04 +01:00
return fmt . Sprintf ( "Set the disappearing message timer to %s" , formatDuration ( time . Duration ( portal . ExpirationTime ) * time . Second ) )
2022-01-07 14:05:09 +01:00
}
2021-10-27 17:44:17 +02:00
const UndecryptableMessageNotice = "Decrypting message from WhatsApp failed, waiting for sender to re-send... " +
2021-10-27 17:31:33 +02:00
"([learn more](https://faq.whatsapp.com/general/security-and-privacy/seeing-waiting-for-this-message-this-may-take-a-while))"
2021-10-28 11:59:22 +02:00
2021-10-27 17:44:17 +02:00
var undecryptableMessageContent event . MessageEventContent
func init ( ) {
undecryptableMessageContent = format . RenderMarkdown ( UndecryptableMessageNotice , true , false )
undecryptableMessageContent . MsgType = event . MsgNotice
}
2021-10-27 17:31:33 +02:00
func ( portal * Portal ) handleUndecryptableMessage ( source * User , evt * events . UndecryptableMessage ) {
if len ( portal . MXID ) == 0 {
portal . log . Warnln ( "handleUndecryptableMessage called even though portal.MXID is empty" )
return
2022-02-10 18:18:49 +01:00
} else if portal . isRecentlyHandled ( evt . Info . ID , database . MsgErrDecryptionFailed ) {
2021-10-27 17:31:33 +02:00
portal . log . Debugfln ( "Not handling %s (undecryptable): message was recently handled" , evt . Info . ID )
return
} else if existingMsg := portal . bridge . DB . Message . GetByJID ( portal . Key , evt . Info . ID ) ; existingMsg != nil {
portal . log . Debugfln ( "Not handling %s (undecryptable): message is duplicate" , evt . Info . ID )
return
}
2022-05-17 20:47:13 +02:00
metricType := "error"
if evt . IsUnavailable {
metricType = "unavailable"
}
2022-05-16 12:46:32 +02:00
Segment . Track ( source . MXID , "WhatsApp undecryptable message" , map [ string ] interface { } {
2022-05-17 20:47:13 +02:00
"messageID" : evt . Info . ID ,
"undecryptableType" : metricType ,
2022-05-16 12:46:32 +02:00
} )
2023-01-10 15:14:03 +01:00
intent := portal . getMessageIntent ( source , & evt . Info , "undecryptable" )
2022-05-19 11:08:30 +02:00
if intent == nil {
return
2021-11-06 13:20:56 +01:00
}
2021-10-27 17:44:17 +02:00
content := undecryptableMessageContent
2021-10-31 19:42:53 +01:00
resp , err := portal . sendMessage ( intent , event . EventMessage , & content , nil , evt . Info . Timestamp . UnixMilli ( ) )
2021-10-27 17:31:33 +02:00
if err != nil {
2022-09-07 18:31:59 +02:00
portal . log . Errorfln ( "Failed to send decryption error of %s to Matrix: %v" , evt . Info . ID , err )
2022-04-07 09:48:36 +02:00
return
2021-10-27 17:31:33 +02:00
}
2022-03-05 20:22:31 +01:00
portal . finishHandling ( nil , & evt . Info , resp . EventID , database . MsgUnknown , database . MsgErrDecryptionFailed )
2021-10-27 17:31:33 +02:00
}
2021-11-02 14:46:31 +01:00
func ( portal * Portal ) handleFakeMessage ( msg fakeMessage ) {
2022-02-10 18:18:49 +01:00
if portal . isRecentlyHandled ( msg . ID , database . MsgNoError ) {
2021-11-02 14:46:31 +01:00
portal . log . Debugfln ( "Not handling %s (fake): message was recently handled" , msg . ID )
return
} else if existingMsg := portal . bridge . DB . Message . GetByJID ( portal . Key , msg . ID ) ; existingMsg != nil {
portal . log . Debugfln ( "Not handling %s (fake): message is duplicate" , msg . ID )
return
}
intent := portal . bridge . GetPuppetByJID ( msg . Sender ) . IntentFor ( portal )
2023-01-10 15:14:03 +01:00
if ! intent . IsCustomPuppet && portal . IsPrivateChat ( ) && msg . Sender . User == portal . Key . Receiver . User && portal . Key . Receiver != portal . Key . JID {
2021-11-06 13:20:56 +01:00
portal . log . Debugfln ( "Not handling %s (fake): user doesn't have double puppeting enabled" , msg . ID )
return
}
2021-11-09 16:49:34 +01:00
msgType := event . MsgNotice
if msg . Important {
msgType = event . MsgText
}
2021-11-02 14:46:31 +01:00
resp , err := portal . sendMessage ( intent , event . EventMessage , & event . MessageEventContent {
2021-11-09 16:49:34 +01:00
MsgType : msgType ,
2021-11-02 14:46:31 +01:00
Body : msg . Text ,
} , nil , msg . Time . UnixMilli ( ) )
if err != nil {
2021-11-30 14:27:15 +01:00
portal . log . Errorfln ( "Failed to send %s to Matrix: %v" , msg . ID , err )
2021-11-02 14:46:31 +01:00
} else {
portal . finishHandling ( nil , & types . MessageInfo {
ID : msg . ID ,
Timestamp : msg . Time ,
MessageSource : types . MessageSource {
Sender : msg . Sender ,
} ,
2022-03-05 20:22:31 +01:00
} , resp . EventID , database . MsgFake , database . MsgNoError )
2021-11-02 14:46:31 +01:00
}
}
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) handleMessage ( source * User , evt * events . Message ) {
if len ( portal . MXID ) == 0 {
portal . log . Warnln ( "handleMessage called even though portal.MXID is empty" )
return
2021-06-25 14:33:37 +02:00
}
2021-10-26 16:01:10 +02:00
msgID := evt . Info . ID
2021-11-01 14:30:56 +01:00
msgType := getMessageType ( evt . Message )
2021-10-26 16:01:10 +02:00
if msgType == "ignore" {
return
2022-02-10 18:18:49 +01:00
} else if portal . isRecentlyHandled ( msgID , database . MsgNoError ) {
2021-10-26 16:01:10 +02:00
portal . log . Debugfln ( "Not handling %s (%s): message was recently handled" , msgID , msgType )
return
2019-05-21 22:44:14 +02:00
}
2021-10-27 17:31:33 +02:00
existingMsg := portal . bridge . DB . Message . GetByJID ( portal . Key , msgID )
if existingMsg != nil {
2022-02-10 18:18:49 +01:00
if existingMsg . Error == database . MsgErrDecryptionFailed {
2022-05-16 12:46:32 +02:00
Segment . Track ( source . MXID , "WhatsApp undecryptable message resolved" , map [ string ] interface { } {
"messageID" : evt . Info . ID ,
} )
2021-10-27 17:31:33 +02:00
portal . log . Debugfln ( "Got decryptable version of previously undecryptable message %s (%s)" , msgID , msgType )
} else {
portal . log . Debugfln ( "Not handling %s (%s): message is duplicate" , msgID , msgType )
return
}
}
2022-10-08 16:46:42 +02:00
var editTargetMsg * database . Message
if msgType == "edit" {
editTargetID := evt . Message . GetProtocolMessage ( ) . GetKey ( ) . GetId ( )
editTargetMsg = portal . bridge . DB . Message . GetByJID ( portal . Key , editTargetID )
if editTargetMsg == nil {
portal . log . Warnfln ( "Not handling %s: couldn't find edit target %s" , msgID , editTargetID )
return
} else if editTargetMsg . Type != database . MsgNormal {
portal . log . Warnfln ( "Not handling %s: edit target %s is not a normal message (it's %s)" , msgID , editTargetID , editTargetMsg . Type )
return
} else if editTargetMsg . Sender . User != evt . Info . Sender . User {
portal . log . Warnfln ( "Not handling %s: edit target %s was sent by %s, not %s" , msgID , editTargetID , editTargetMsg . Sender . User , evt . Info . Sender . User )
return
}
evt . Message = evt . Message . GetProtocolMessage ( ) . GetEditedMessage ( )
}
2021-10-27 17:31:33 +02:00
2023-01-10 15:14:03 +01:00
intent := portal . getMessageIntent ( source , & evt . Info , msgType )
2022-05-19 11:08:30 +02:00
if intent == nil {
return
2021-11-06 13:20:56 +01:00
}
2022-05-02 14:00:57 +02:00
converted := portal . convertMessage ( intent , source , & evt . Info , evt . Message , false )
2021-10-26 16:01:10 +02:00
if converted != nil {
2021-12-25 19:50:36 +01:00
if evt . Info . IsIncomingBroadcast ( ) {
if converted . Extra == nil {
converted . Extra = map [ string ] interface { } { }
}
converted . Extra [ "fi.mau.whatsapp.source_broadcast_list" ] = evt . Info . Chat . String ( )
}
2022-06-17 10:34:24 +02:00
if portal . bridge . Config . Bridge . CaptionInMessage {
converted . MergeCaption ( )
}
2021-10-26 16:01:10 +02:00
var eventID id . EventID
2022-05-23 09:18:00 +02:00
var lastEventID id . EventID
2021-10-27 17:31:33 +02:00
if existingMsg != nil {
2023-01-16 16:15:04 +01:00
portal . MarkDisappearing ( nil , existingMsg . MXID , converted . ExpiresIn , evt . Info . Timestamp )
2021-10-27 17:31:33 +02:00
converted . Content . SetEdit ( existingMsg . MXID )
2022-08-25 16:05:40 +02:00
} else if converted . ReplyTo != nil {
portal . SetReply ( converted . Content , converted . ReplyTo , false )
2021-10-27 17:31:33 +02:00
}
2022-10-08 16:46:42 +02:00
dbMsgType := database . MsgNormal
if editTargetMsg != nil {
dbMsgType = database . MsgEdit
converted . Content . SetEdit ( editTargetMsg . MXID )
}
2021-10-31 19:42:53 +01:00
resp , err := portal . sendMessage ( converted . Intent , converted . Type , converted . Content , converted . Extra , evt . Info . Timestamp . UnixMilli ( ) )
2021-10-26 16:01:10 +02:00
if err != nil {
2021-11-30 14:27:15 +01:00
portal . log . Errorfln ( "Failed to send %s to Matrix: %v" , msgID , err )
2021-10-26 16:01:10 +02:00
} else {
2022-10-08 16:46:42 +02:00
if editTargetMsg == nil {
2023-01-16 16:15:04 +01:00
portal . MarkDisappearing ( nil , resp . EventID , converted . ExpiresIn , evt . Info . Timestamp )
2022-10-08 16:46:42 +02:00
}
2021-10-26 16:01:10 +02:00
eventID = resp . EventID
2022-05-23 09:18:00 +02:00
lastEventID = eventID
2021-10-26 16:01:10 +02:00
}
2021-10-27 17:31:33 +02:00
// TODO figure out how to handle captions with undecryptable messages turning decryptable
2022-10-08 16:46:42 +02:00
if converted . Caption != nil && existingMsg == nil && editTargetMsg == nil {
2021-10-31 19:42:53 +01:00
resp , err = portal . sendMessage ( converted . Intent , converted . Type , converted . Caption , nil , evt . Info . Timestamp . UnixMilli ( ) )
2021-10-26 16:01:10 +02:00
if err != nil {
2021-11-30 14:27:15 +01:00
portal . log . Errorfln ( "Failed to send caption of %s to Matrix: %v" , msgID , err )
2021-10-26 16:01:10 +02:00
} else {
2023-01-16 16:15:04 +01:00
portal . MarkDisappearing ( nil , resp . EventID , converted . ExpiresIn , evt . Info . Timestamp )
2022-05-23 09:18:00 +02:00
lastEventID = resp . EventID
2021-10-26 16:01:10 +02:00
}
}
2022-10-08 16:46:42 +02:00
if converted . MultiEvent != nil && existingMsg == nil && editTargetMsg == nil {
2022-01-03 15:11:39 +01:00
for index , subEvt := range converted . MultiEvent {
resp , err = portal . sendMessage ( converted . Intent , converted . Type , subEvt , nil , evt . Info . Timestamp . UnixMilli ( ) )
if err != nil {
portal . log . Errorfln ( "Failed to send sub-event %d of %s to Matrix: %v" , index + 1 , msgID , err )
2022-01-07 13:32:00 +01:00
} else {
2023-01-16 16:15:04 +01:00
portal . MarkDisappearing ( nil , resp . EventID , converted . ExpiresIn , evt . Info . Timestamp )
2022-05-23 09:18:00 +02:00
lastEventID = resp . EventID
2022-01-03 15:11:39 +01:00
}
}
}
2022-10-08 16:46:42 +02:00
if source . MXID == intent . UserID && portal . bridge . Config . Homeserver . Software != bridgeconfig . SoftwareHungry {
2022-05-23 09:18:00 +02:00
// There are some edge cases (like call notices) where previous messages aren't marked as read
// when the user sends a message from another device, so just mark the new message as read to be safe.
2022-10-08 16:46:42 +02:00
// Hungryserv does this automatically, so the bridge doesn't need to do it manually.
2022-06-30 19:56:25 +02:00
err = intent . SetReadMarkers ( portal . MXID , source . makeReadMarkerContent ( lastEventID , true ) )
2022-05-23 09:18:00 +02:00
if err != nil {
portal . log . Warnfln ( "Failed to mark own message %s as read by %s: %v" , lastEventID , source . MXID , err )
}
}
2021-10-26 16:01:10 +02:00
if len ( eventID ) != 0 {
2022-10-08 16:46:42 +02:00
portal . finishHandling ( existingMsg , & evt . Info , eventID , dbMsgType , converted . Error )
2021-10-26 16:01:10 +02:00
}
2022-11-26 18:36:21 +01:00
} else if msgType == "reaction" || msgType == "encrypted reaction" {
if evt . Message . GetEncReactionMessage ( ) != nil {
decryptedReaction , err := source . Client . DecryptReaction ( evt )
if err != nil {
portal . log . Errorfln ( "Failed to decrypt reaction from %s to %s: %v" , evt . Info . Sender , evt . Message . GetEncReactionMessage ( ) . GetTargetMessageKey ( ) . GetId ( ) , err )
} else {
portal . HandleMessageReaction ( intent , source , & evt . Info , decryptedReaction , existingMsg )
}
} else {
portal . HandleMessageReaction ( intent , source , & evt . Info , evt . Message . GetReactionMessage ( ) , existingMsg )
}
2021-10-26 16:01:10 +02:00
} else if msgType == "revoke" {
2021-10-27 20:09:36 +02:00
portal . HandleMessageRevoke ( source , & evt . Info , evt . Message . GetProtocolMessage ( ) . GetKey ( ) )
2021-10-27 17:31:33 +02:00
if existingMsg != nil {
_ , _ = portal . MainIntent ( ) . RedactEvent ( portal . MXID , existingMsg . MXID , mautrix . ReqRedact {
Reason : "The undecryptable message was actually the deletion of another message" ,
} )
2022-05-13 01:56:40 +02:00
existingMsg . UpdateMXID ( nil , "net.maunium.whatsapp.fake::" + existingMsg . MXID , database . MsgFake , database . MsgNoError )
2021-10-27 17:31:33 +02:00
}
2021-10-26 16:01:10 +02:00
} else {
2022-01-04 14:19:25 +01:00
portal . log . Warnfln ( "Unhandled message: %+v (%s)" , evt . Info , msgType )
2021-10-27 17:31:33 +02:00
if existingMsg != nil {
_ , _ = portal . MainIntent ( ) . RedactEvent ( portal . MXID , existingMsg . MXID , mautrix . ReqRedact {
Reason : "The undecryptable message contained an unsupported message type" ,
} )
2022-05-13 01:56:40 +02:00
existingMsg . UpdateMXID ( nil , "net.maunium.whatsapp.fake::" + existingMsg . MXID , database . MsgFake , database . MsgNoError )
2021-10-27 17:31:33 +02:00
}
2021-10-26 16:01:10 +02:00
return
}
portal . bridge . Metrics . TrackWhatsAppMessage ( evt . Info . Timestamp , strings . Split ( msgType , " " ) [ 0 ] )
2019-05-21 22:44:14 +02:00
}
2022-02-10 18:18:49 +01:00
func ( portal * Portal ) isRecentlyHandled ( id types . MessageID , error database . MessageErrorType ) bool {
2018-08-30 23:13:08 +02:00
start := portal . recentlyHandledIndex
2022-02-10 18:18:49 +01:00
lookingForMsg := recentlyHandledWrapper { id , error }
2018-09-01 23:01:22 +02:00
for i := start ; i != start ; i = ( i - 1 ) % recentlyHandledLength {
2021-10-27 17:31:33 +02:00
if portal . recentlyHandled [ i ] == lookingForMsg {
2018-08-30 23:13:08 +02:00
return true
}
}
return false
}
2022-08-14 18:26:42 +02:00
func ( portal * Portal ) markHandled ( txn dbutil . Transaction , msg * database . Message , info * types . MessageInfo , mxid id . EventID , isSent , recent bool , msgType database . MessageType , errType database . MessageErrorType ) * database . Message {
2021-10-27 17:31:33 +02:00
if msg == nil {
msg = portal . bridge . DB . Message . New ( )
msg . Chat = portal . Key
msg . JID = info . ID
msg . MXID = mxid
msg . Timestamp = info . Timestamp
msg . Sender = info . Sender
msg . Sent = isSent
2022-03-05 20:22:31 +01:00
msg . Type = msgType
2022-05-13 01:56:40 +02:00
msg . Error = errType
2021-12-25 19:50:36 +01:00
if info . IsIncomingBroadcast ( ) {
msg . BroadcastListJID = info . Chat
}
2022-05-13 01:56:40 +02:00
msg . Insert ( txn )
2021-10-27 17:31:33 +02:00
} else {
2022-05-13 01:56:40 +02:00
msg . UpdateMXID ( txn , mxid , msgType , errType )
2021-10-27 17:31:33 +02:00
}
2018-08-30 23:13:08 +02:00
2021-10-26 16:01:10 +02:00
if recent {
portal . recentlyHandledLock . Lock ( )
index := portal . recentlyHandledIndex
portal . recentlyHandledIndex = ( portal . recentlyHandledIndex + 1 ) % recentlyHandledLength
portal . recentlyHandledLock . Unlock ( )
2022-05-13 01:56:40 +02:00
portal . recentlyHandled [ index ] = recentlyHandledWrapper { msg . JID , errType }
2021-10-26 16:01:10 +02:00
}
2021-02-17 00:22:06 +01:00
return msg
2018-08-30 23:13:08 +02:00
}
2022-06-28 13:37:49 +02:00
func ( portal * Portal ) getMessagePuppet ( user * User , info * types . MessageInfo ) ( puppet * Puppet ) {
2021-10-26 16:01:10 +02:00
if info . IsFromMe {
return portal . bridge . GetPuppetByJID ( user . JID )
} else if portal . IsPrivateChat ( ) {
2022-06-28 13:37:49 +02:00
puppet = portal . bridge . GetPuppetByJID ( portal . Key . JID )
2022-09-28 14:07:09 +02:00
} else if ! info . Sender . IsEmpty ( ) {
2022-06-28 13:37:49 +02:00
puppet = portal . bridge . GetPuppetByJID ( info . Sender )
}
if puppet == nil {
portal . log . Warnfln ( "Message %+v doesn't seem to have a valid sender (%s): puppet is nil" , * info , info . Sender )
return nil
2021-10-26 16:01:10 +02:00
}
2022-06-28 15:22:10 +02:00
user . EnqueuePortalResync ( portal )
2022-06-28 13:37:49 +02:00
puppet . SyncContact ( user , true , true , "handling message" )
return puppet
2021-10-26 16:01:10 +02:00
}
2023-01-10 15:14:03 +01:00
func ( portal * Portal ) getMessageIntent ( user * User , info * types . MessageInfo , msgType string ) * appservice . IntentAPI {
2022-05-19 11:08:30 +02:00
puppet := portal . getMessagePuppet ( user , info )
if puppet == nil {
return nil
}
2023-01-10 15:14:03 +01:00
intent := puppet . IntentFor ( portal )
if ! intent . IsCustomPuppet && portal . IsPrivateChat ( ) && info . Sender . User == portal . Key . Receiver . User && portal . Key . Receiver != portal . Key . JID {
portal . log . Debugfln ( "Not handling %s (%s): user doesn't have double puppeting enabled" , info . ID , msgType )
return nil
}
return intent
2020-06-10 13:58:57 +02:00
}
2022-05-13 01:56:40 +02:00
func ( portal * Portal ) finishHandling ( existing * database . Message , message * types . MessageInfo , mxid id . EventID , msgType database . MessageType , errType database . MessageErrorType ) {
portal . markHandled ( nil , existing , message , mxid , true , true , msgType , errType )
2020-06-05 16:54:09 +02:00
portal . sendDeliveryReceipt ( mxid )
2022-02-10 18:18:49 +01:00
var suffix string
2022-05-13 01:56:40 +02:00
if errType == database . MsgErrDecryptionFailed {
2022-02-10 18:18:49 +01:00
suffix = "(undecryptable message error notice)"
2022-05-13 01:56:40 +02:00
} else if errType == database . MsgErrMediaNotFound {
2022-02-10 18:18:49 +01:00
suffix = "(media not found notice)"
2021-10-27 17:31:33 +02:00
}
2022-03-05 20:22:31 +01:00
portal . log . Debugfln ( "Handled message %s (%s) -> %s %s" , message . ID , msgType , mxid , suffix )
2018-08-16 14:59:18 +02:00
}
2018-08-18 21:57:08 +02:00
2021-10-22 19:14:34 +02:00
func ( portal * Portal ) kickExtraUsers ( participantMap map [ types . JID ] bool ) {
2021-02-21 13:45:33 +01:00
members , err := portal . MainIntent ( ) . JoinedMembers ( portal . MXID )
if err != nil {
portal . log . Warnln ( "Failed to get member list:" , err )
2021-10-26 16:01:10 +02:00
return
}
for member := range members . Joined {
jid , ok := portal . bridge . ParsePuppetMXID ( member )
if ok {
_ , shouldBePresent := participantMap [ jid ]
if ! shouldBePresent {
_ , err = portal . MainIntent ( ) . KickUser ( portal . MXID , & mautrix . ReqKickUser {
UserID : member ,
Reason : "User had left this WhatsApp chat" ,
} )
if err != nil {
portal . log . Warnfln ( "Failed to kick user %s who had left: %v" , member , err )
2021-02-21 13:45:33 +01:00
}
}
}
}
}
2021-10-22 19:14:34 +02:00
//func (portal *Portal) SyncBroadcastRecipients(source *User, metadata *whatsapp.BroadcastListInfo) {
// participantMap := make(map[whatsapp.JID]bool)
// for _, recipient := range metadata.Recipients {
// participantMap[recipient.JID] = true
//
// puppet := portal.bridge.GetPuppetByJID(recipient.JID)
// puppet.SyncContactIfNecessary(source)
// err := puppet.DefaultIntent().EnsureJoined(portal.MXID)
// if err != nil {
// portal.log.Warnfln("Failed to make puppet of %s join %s: %v", recipient.JID, portal.MXID, err)
// }
// }
// portal.kickExtraUsers(participantMap)
//}
2022-09-29 13:39:58 +02:00
func ( portal * Portal ) syncParticipant ( source * User , participant types . GroupParticipant , puppet * Puppet , user * User , wg * sync . WaitGroup ) {
defer func ( ) {
wg . Done ( )
if err := recover ( ) ; err != nil {
portal . log . Errorfln ( "Syncing participant %s panicked: %v\n%s" , participant . JID , err , debug . Stack ( ) )
}
} ( )
puppet . SyncContact ( source , true , false , "group participant" )
2022-10-10 16:07:37 +02:00
if portal . MXID != "" {
if user != nil && user != source {
portal . ensureUserInvited ( user )
}
if user == nil || ! puppet . IntentFor ( portal ) . IsCustomPuppet {
err := puppet . IntentFor ( portal ) . EnsureJoined ( portal . MXID )
if err != nil {
portal . log . Warnfln ( "Failed to make puppet of %s join %s: %v" , participant . JID , portal . MXID , err )
}
2022-09-29 13:39:58 +02:00
}
}
}
2022-10-10 16:07:37 +02:00
func ( portal * Portal ) SyncParticipants ( source * User , metadata * types . GroupInfo ) ( [ ] id . UserID , * event . PowerLevelsEventContent ) {
2018-08-26 15:11:48 +02:00
changed := false
2022-10-10 16:07:37 +02:00
var levels * event . PowerLevelsEventContent
var err error
if portal . MXID != "" {
levels , err = portal . MainIntent ( ) . PowerLevels ( portal . MXID )
}
if levels == nil || err != nil {
2018-08-26 15:11:48 +02:00
levels = portal . GetBasePowerLevels ( )
changed = true
}
2022-05-12 09:45:23 +02:00
changed = portal . applyPowerLevelFixes ( levels ) || changed
2022-09-29 13:39:58 +02:00
var wg sync . WaitGroup
wg . Add ( len ( metadata . Participants ) )
2021-10-22 19:14:34 +02:00
participantMap := make ( map [ types . JID ] bool )
2022-10-10 16:07:37 +02:00
userIDs := make ( [ ] id . UserID , 0 , len ( metadata . Participants ) )
2018-08-23 00:12:26 +02:00
for _ , participant := range metadata . Participants {
2022-09-29 13:39:58 +02:00
portal . log . Debugfln ( "Syncing participant %s (admin: %t)" , participant . JID , participant . IsAdmin )
2020-07-05 22:16:59 +02:00
participantMap [ participant . JID ] = true
2019-05-24 01:33:26 +02:00
puppet := portal . bridge . GetPuppetByJID ( participant . JID )
2021-10-28 12:57:15 +02:00
user := portal . bridge . GetUserByJID ( participant . JID )
2022-09-29 13:39:58 +02:00
if portal . bridge . Config . Bridge . ParallelMemberSync {
go portal . syncParticipant ( source , participant , puppet , user , & wg )
} else {
portal . syncParticipant ( source , participant , puppet , user , & wg )
2019-05-24 01:33:26 +02:00
}
2018-08-26 15:11:48 +02:00
expectedLevel := 0
2021-11-01 12:03:09 +01:00
if participant . IsSuperAdmin {
2018-08-26 15:11:48 +02:00
expectedLevel = 95
} else if participant . IsAdmin {
expectedLevel = 50
}
2018-08-26 15:19:50 +02:00
changed = levels . EnsureUserLevel ( puppet . MXID , expectedLevel ) || changed
2018-08-28 23:40:54 +02:00
if user != nil {
2022-10-10 16:07:37 +02:00
userIDs = append ( userIDs , user . MXID )
2018-08-28 23:40:54 +02:00
changed = levels . EnsureUserLevel ( user . MXID , expectedLevel ) || changed
2018-08-26 15:11:48 +02:00
}
2022-10-10 16:07:37 +02:00
if user == nil || puppet . CustomMXID != user . MXID {
userIDs = append ( userIDs , puppet . MXID )
}
2018-08-26 15:11:48 +02:00
}
2022-10-10 16:07:37 +02:00
if portal . MXID != "" {
if changed {
_ , err = portal . MainIntent ( ) . SetPowerLevels ( portal . MXID , levels )
if err != nil {
portal . log . Errorln ( "Failed to change power levels:" , err )
}
2018-08-30 00:10:26 +02:00
}
2022-10-10 16:07:37 +02:00
portal . kickExtraUsers ( participantMap )
2018-08-23 00:12:26 +02:00
}
2022-09-29 13:39:58 +02:00
wg . Wait ( )
portal . log . Debugln ( "Participant sync completed" )
2022-10-10 16:07:37 +02:00
return userIDs , levels
2018-08-23 00:12:26 +02:00
}
2018-08-19 17:21:38 +02:00
2022-09-28 13:45:36 +02:00
func reuploadAvatar ( intent * appservice . IntentAPI , url string ) ( id . ContentURI , error ) {
getResp , err := http . DefaultClient . Get ( url )
if err != nil {
return id . ContentURI { } , fmt . Errorf ( "failed to download avatar: %w" , err )
}
data , err := io . ReadAll ( getResp . Body )
_ = getResp . Body . Close ( )
if err != nil {
return id . ContentURI { } , fmt . Errorf ( "failed to read avatar bytes: %w" , err )
}
resp , err := intent . UploadBytes ( data , http . DetectContentType ( data ) )
if err != nil {
return id . ContentURI { } , fmt . Errorf ( "failed to upload avatar to Matrix: %w" , err )
}
return resp . ContentURI , nil
}
2022-12-14 00:00:31 +01:00
func ( user * User ) updateAvatar ( jid types . JID , isCommunity bool , avatarID * string , avatarURL * id . ContentURI , avatarSet * bool , log log . Logger , intent * appservice . IntentAPI ) bool {
2022-06-28 15:22:10 +02:00
currentID := ""
if * avatarSet && * avatarID != "remove" && * avatarID != "unauthorized" {
currentID = * avatarID
}
2022-12-14 00:00:31 +01:00
avatar , err := user . Client . GetProfilePictureInfo ( jid , & whatsmeow . GetProfilePictureParams {
Preview : false ,
ExistingID : currentID ,
IsCommunity : isCommunity ,
} )
2022-06-28 15:22:10 +02:00
if errors . Is ( err , whatsmeow . ErrProfilePictureUnauthorized ) {
if * avatarID == "" {
* avatarID = "unauthorized"
* avatarSet = false
return true
2018-08-26 00:55:21 +02:00
}
2018-08-23 00:12:26 +02:00
return false
2022-06-28 15:22:10 +02:00
} else if errors . Is ( err , whatsmeow . ErrProfilePictureNotSet ) {
2021-10-22 19:14:34 +02:00
avatar = & types . ProfilePictureInfo { ID : "remove" }
2022-07-06 12:54:32 +02:00
if avatar . ID == * avatarID && * avatarSet {
return false
}
* avatarID = avatar . ID
* avatarURL = id . ContentURI { }
return true
2022-06-28 15:22:10 +02:00
} else if err != nil {
log . Warnln ( "Failed to get avatar URL:" , err )
return false
} else if avatar == nil {
// Avatar hasn't changed
return false
}
if avatar . ID == * avatarID && * avatarSet {
2021-10-22 19:14:34 +02:00
return false
} else if len ( avatar . URL ) == 0 {
2022-06-28 15:22:10 +02:00
log . Warnln ( "Didn't get URL in response to avatar query" )
2021-10-22 19:14:34 +02:00
return false
2022-06-28 15:22:10 +02:00
} else if avatar . ID != * avatarID || avatarURL . IsEmpty ( ) {
url , err := reuploadAvatar ( intent , avatar . URL )
2021-02-09 22:41:14 +01:00
if err != nil {
2022-06-28 15:22:10 +02:00
log . Warnln ( "Failed to reupload avatar:" , err )
2021-02-09 22:41:14 +01:00
return false
}
2022-06-28 15:22:10 +02:00
* avatarURL = url
}
2022-06-29 19:05:55 +02:00
log . Debugfln ( "Updated avatar %s -> %s" , * avatarID , avatar . ID )
2022-06-28 15:22:10 +02:00
* avatarID = avatar . ID
* avatarSet = false
return true
}
func ( portal * Portal ) UpdateAvatar ( user * User , setBy types . JID , updateInfo bool ) bool {
portal . avatarLock . Lock ( )
defer portal . avatarLock . Unlock ( )
2022-12-14 00:00:31 +01:00
changed := user . updateAvatar ( portal . Key . JID , portal . IsParent , & portal . Avatar , & portal . AvatarURL , & portal . AvatarSet , portal . log , portal . MainIntent ( ) )
2022-06-28 15:22:10 +02:00
if ! changed || portal . Avatar == "unauthorized" {
if changed || updateInfo {
portal . Update ( nil )
}
return changed
2018-08-23 00:12:26 +02:00
}
2019-05-22 22:27:58 +02:00
if len ( portal . MXID ) > 0 {
2021-10-28 11:59:22 +02:00
intent := portal . MainIntent ( )
if ! setBy . IsEmpty ( ) {
intent = portal . bridge . GetPuppetByJID ( setBy ) . IntentFor ( portal )
}
2022-06-28 15:22:10 +02:00
_ , err := intent . SetRoomAvatar ( portal . MXID , portal . AvatarURL )
2021-10-28 11:59:22 +02:00
if errors . Is ( err , mautrix . MForbidden ) && intent != portal . MainIntent ( ) {
_ , err = portal . MainIntent ( ) . SetRoomAvatar ( portal . MXID , portal . AvatarURL )
}
2019-05-22 22:27:58 +02:00
if err != nil {
2022-01-19 13:13:06 +01:00
portal . log . Warnln ( "Failed to set room avatar:" , err )
2022-06-28 15:22:10 +02:00
return true
2022-06-28 13:37:49 +02:00
} else {
portal . AvatarSet = true
2019-05-22 22:27:58 +02:00
}
2018-08-23 00:12:26 +02:00
}
2020-06-15 13:56:52 +02:00
if updateInfo {
portal . UpdateBridgeInfo ( )
2022-06-28 13:37:49 +02:00
portal . Update ( nil )
2022-12-14 00:00:12 +01:00
portal . updateChildRooms ( )
2020-06-15 13:56:52 +02:00
}
2018-08-23 00:12:26 +02:00
return true
}
2021-10-28 11:59:22 +02:00
func ( portal * Portal ) UpdateName ( name string , setBy types . JID , updateInfo bool ) bool {
2021-02-21 13:45:33 +01:00
if name == "" && portal . IsBroadcastList ( ) {
2021-02-21 13:18:15 +01:00
name = UnnamedBroadcastName
}
2023-04-14 12:40:34 +02:00
if portal . Name != name || ( ! portal . NameSet && len ( portal . MXID ) > 0 && portal . shouldSetDMRoomMetadata ( ) ) {
2022-06-28 13:37:49 +02:00
portal . log . Debugfln ( "Updating name %q -> %q" , portal . Name , name )
2021-02-09 22:41:14 +01:00
portal . Name = name
2022-06-28 13:37:49 +02:00
portal . NameSet = false
2022-07-05 10:05:22 +02:00
if updateInfo {
defer portal . Update ( nil )
2019-03-13 23:38:11 +01:00
}
2022-07-05 10:05:22 +02:00
2023-04-14 12:40:34 +02:00
if len ( portal . MXID ) > 0 && ! portal . shouldSetDMRoomMetadata ( ) {
portal . UpdateBridgeInfo ( )
} else if len ( portal . MXID ) > 0 {
2022-07-05 10:05:22 +02:00
intent := portal . MainIntent ( )
if ! setBy . IsEmpty ( ) {
intent = portal . bridge . GetPuppetByJID ( setBy ) . IntentFor ( portal )
}
_ , err := intent . SetRoomName ( portal . MXID , name )
if errors . Is ( err , mautrix . MForbidden ) && intent != portal . MainIntent ( ) {
_ , err = portal . MainIntent ( ) . SetRoomName ( portal . MXID , name )
}
if err == nil {
portal . NameSet = true
if updateInfo {
portal . UpdateBridgeInfo ( )
2022-12-14 00:00:12 +01:00
portal . updateChildRooms ( )
2022-07-05 10:05:22 +02:00
}
return true
} else {
portal . log . Warnln ( "Failed to set room name:" , err )
2020-06-15 13:56:52 +02:00
}
2018-08-23 00:12:26 +02:00
}
}
return false
}
2021-10-28 11:59:22 +02:00
func ( portal * Portal ) UpdateTopic ( topic string , setBy types . JID , updateInfo bool ) bool {
2022-06-28 13:37:49 +02:00
if portal . Topic != topic || ! portal . TopicSet {
portal . log . Debugfln ( "Updating topic %q -> %q" , portal . Topic , topic )
2021-02-09 22:41:14 +01:00
portal . Topic = topic
2022-06-28 13:37:49 +02:00
portal . TopicSet = false
2021-10-28 11:59:22 +02:00
intent := portal . MainIntent ( )
if ! setBy . IsEmpty ( ) {
intent = portal . bridge . GetPuppetByJID ( setBy ) . IntentFor ( portal )
2019-03-13 23:38:11 +01:00
}
2018-08-26 00:55:21 +02:00
_ , err := intent . SetRoomTopic ( portal . MXID , topic )
2021-10-28 11:59:22 +02:00
if errors . Is ( err , mautrix . MForbidden ) && intent != portal . MainIntent ( ) {
_ , err = portal . MainIntent ( ) . SetRoomTopic ( portal . MXID , topic )
}
2018-08-23 00:12:26 +02:00
if err == nil {
2022-06-28 13:37:49 +02:00
portal . TopicSet = true
2020-06-15 13:56:52 +02:00
if updateInfo {
portal . UpdateBridgeInfo ( )
2022-06-28 13:37:49 +02:00
portal . Update ( nil )
2020-06-15 13:56:52 +02:00
}
2018-08-23 00:12:26 +02:00
return true
2021-02-09 22:41:14 +01:00
} else {
portal . log . Warnln ( "Failed to set room topic:" , err )
2018-08-23 00:12:26 +02:00
}
}
return false
}
2022-12-02 15:08:35 +01:00
func ( portal * Portal ) UpdateParentGroup ( source * User , parent types . JID , updateInfo bool ) bool {
2022-12-02 14:36:19 +01:00
portal . parentGroupUpdateLock . Lock ( )
defer portal . parentGroupUpdateLock . Unlock ( )
if portal . ParentGroup != parent {
portal . log . Debugfln ( "Updating parent group %v -> %v" , portal . ParentGroup , parent )
2022-12-02 15:08:35 +01:00
portal . updateCommunitySpace ( source , false , false )
2022-12-02 14:36:19 +01:00
portal . ParentGroup = parent
portal . parentPortal = nil
portal . InSpace = false
2022-12-02 15:08:35 +01:00
portal . updateCommunitySpace ( source , true , false )
2022-12-02 14:36:19 +01:00
if updateInfo {
portal . UpdateBridgeInfo ( )
portal . Update ( nil )
}
return true
} else if ! portal . ParentGroup . IsEmpty ( ) && ! portal . InSpace {
2022-12-02 15:08:35 +01:00
return portal . updateCommunitySpace ( source , true , updateInfo )
2022-12-02 14:36:19 +01:00
}
return false
}
2021-11-03 13:43:53 +01:00
func ( portal * Portal ) UpdateMetadata ( user * User , groupInfo * types . GroupInfo ) bool {
2019-03-13 23:38:11 +01:00
if portal . IsPrivateChat ( ) {
return false
2021-02-21 13:45:33 +01:00
} else if portal . IsStatusBroadcastList ( ) {
2019-03-13 23:38:11 +01:00
update := false
2021-10-28 11:59:22 +02:00
update = portal . UpdateName ( StatusBroadcastName , types . EmptyJID , false ) || update
update = portal . UpdateTopic ( StatusBroadcastTopic , types . EmptyJID , false ) || update
2021-02-21 13:18:15 +01:00
return update
2021-02-21 13:45:33 +01:00
} else if portal . IsBroadcastList ( ) {
2021-02-21 13:18:15 +01:00
update := false
2021-10-22 19:14:34 +02:00
//broadcastMetadata, err := user.Conn.GetBroadcastMetadata(portal.Key.JID)
//if err == nil && broadcastMetadata.Status == 200 {
// portal.SyncBroadcastRecipients(user, broadcastMetadata)
// update = portal.UpdateName(broadcastMetadata.Name, "", nil, false) || update
//} else {
// user.Conn.Store.ContactsLock.RLock()
// contact, _ := user.Conn.Store.Contacts[portal.Key.JID]
// user.Conn.Store.ContactsLock.RUnlock()
// update = portal.UpdateName(contact.Name, "", nil, false) || update
//}
//update = portal.UpdateTopic(BroadcastTopic, "", nil, false) || update
2019-03-13 23:38:11 +01:00
return update
}
2021-11-03 13:43:53 +01:00
if groupInfo == nil {
var err error
groupInfo , err = user . Client . GetGroupInfo ( portal . Key . JID )
if err != nil {
portal . log . Errorln ( "Failed to get group info:" , err )
return false
}
2018-12-05 09:20:39 +01:00
}
2021-11-03 13:43:53 +01:00
portal . SyncParticipants ( user , groupInfo )
2018-08-23 00:12:26 +02:00
update := false
2021-11-03 13:43:53 +01:00
update = portal . UpdateName ( groupInfo . Name , groupInfo . NameSetBy , false ) || update
update = portal . UpdateTopic ( groupInfo . Topic , groupInfo . TopicSetBy , false ) || update
2022-12-02 15:08:35 +01:00
update = portal . UpdateParentGroup ( user , groupInfo . LinkedParentJID , false ) || update
2022-01-07 14:05:09 +01:00
if portal . ExpirationTime != groupInfo . DisappearingTimer {
update = true
portal . ExpirationTime = groupInfo . DisappearingTimer
}
2022-12-02 14:36:19 +01:00
if portal . IsParent != groupInfo . IsParent {
if portal . MXID != "" {
portal . log . Warnfln ( "Existing group changed is_parent from %t to %t" , portal . IsParent , groupInfo . IsParent )
}
2022-12-03 15:48:38 +01:00
portal . IsParent = groupInfo . IsParent
2022-12-02 14:36:19 +01:00
update = true
}
2020-10-12 12:59:14 +02:00
2021-11-03 13:43:53 +01:00
portal . RestrictMessageSending ( groupInfo . IsAnnounce )
portal . RestrictMetadataChanges ( groupInfo . IsLocked )
2020-10-12 12:59:14 +02:00
2018-08-23 00:12:26 +02:00
return update
}
2021-12-29 20:40:08 +01:00
func ( portal * Portal ) ensureUserInvited ( user * User ) bool {
return user . ensureInvited ( portal . MainIntent ( ) , portal . MXID , portal . IsPrivateChat ( ) )
2019-05-30 16:22:03 +02:00
}
2021-11-01 10:28:52 +01:00
func ( portal * Portal ) UpdateMatrixRoom ( user * User , groupInfo * types . GroupInfo ) bool {
2018-08-19 17:21:38 +02:00
if len ( portal . MXID ) == 0 {
2021-11-03 13:43:53 +01:00
return false
2018-08-19 17:21:38 +02:00
}
2021-11-03 13:43:53 +01:00
portal . log . Infoln ( "Syncing portal for" , user . MXID )
portal . ensureUserInvited ( user )
2022-12-02 14:36:19 +01:00
go portal . addToPersonalSpace ( user )
2018-08-19 17:21:38 +02:00
2018-08-23 00:12:26 +02:00
update := false
2021-11-03 13:43:53 +01:00
update = portal . UpdateMetadata ( user , groupInfo ) || update
2022-06-28 15:22:10 +02:00
if ! portal . IsPrivateChat ( ) && ! portal . IsBroadcastList ( ) {
2021-10-28 11:59:22 +02:00
update = portal . UpdateAvatar ( user , types . EmptyJID , false ) || update
2019-03-13 23:38:11 +01:00
}
2022-06-28 15:22:10 +02:00
if update || portal . LastSync . Add ( 24 * time . Hour ) . Before ( time . Now ( ) ) {
portal . LastSync = time . Now ( )
2022-05-13 01:56:40 +02:00
portal . Update ( nil )
2020-06-15 13:56:52 +02:00
portal . UpdateBridgeInfo ( )
2022-12-14 00:00:12 +01:00
portal . updateChildRooms ( )
2018-08-19 17:21:38 +02:00
}
2021-06-01 14:28:15 +02:00
return true
2018-08-19 17:21:38 +02:00
}
2020-05-08 21:32:22 +02:00
func ( portal * Portal ) GetBasePowerLevels ( ) * event . PowerLevelsEventContent {
2018-08-26 15:11:48 +02:00
anyone := 0
nope := 99
2020-06-25 23:05:51 +02:00
invite := 50
2019-07-16 11:16:17 +02:00
if portal . bridge . Config . Bridge . AllowUserInvite {
invite = 0
}
2020-05-08 21:32:22 +02:00
return & event . PowerLevelsEventContent {
2018-08-26 15:11:48 +02:00
UsersDefault : anyone ,
EventsDefault : anyone ,
RedactPtr : & anyone ,
StateDefaultPtr : & nope ,
BanPtr : & nope ,
2019-07-16 11:16:17 +02:00
InvitePtr : & invite ,
2020-05-08 21:32:22 +02:00
Users : map [ id . UserID ] int {
2018-08-26 15:11:48 +02:00
portal . MainIntent ( ) . UserID : 100 ,
} ,
2018-08-30 00:10:26 +02:00
Events : map [ string ] int {
2023-01-16 16:17:36 +01:00
event . StateRoomName . Type : anyone ,
event . StateRoomAvatar . Type : anyone ,
event . StateTopic . Type : anyone ,
event . EventReaction . Type : anyone ,
event . EventRedaction . Type : anyone ,
TypeMSC3381PollResponse . Type : anyone ,
2018-08-26 15:11:48 +02:00
} ,
}
}
2022-05-12 09:45:23 +02:00
func ( portal * Portal ) applyPowerLevelFixes ( levels * event . PowerLevelsEventContent ) bool {
changed := false
changed = levels . EnsureEventLevel ( event . EventReaction , 0 ) || changed
changed = levels . EnsureEventLevel ( event . EventRedaction , 0 ) || changed
2023-01-16 16:17:36 +01:00
changed = levels . EnsureEventLevel ( TypeMSC3381PollResponse , 0 ) || changed
2022-05-12 09:45:23 +02:00
return changed
}
2021-10-28 11:59:22 +02:00
func ( portal * Portal ) ChangeAdminStatus ( jids [ ] types . JID , setAdmin bool ) id . EventID {
levels , err := portal . MainIntent ( ) . PowerLevels ( portal . MXID )
if err != nil {
levels = portal . GetBasePowerLevels ( )
}
newLevel := 0
if setAdmin {
newLevel = 50
}
2022-05-12 09:45:23 +02:00
changed := portal . applyPowerLevelFixes ( levels )
2021-10-28 11:59:22 +02:00
for _ , jid := range jids {
puppet := portal . bridge . GetPuppetByJID ( jid )
changed = levels . EnsureUserLevel ( puppet . MXID , newLevel ) || changed
user := portal . bridge . GetUserByJID ( jid )
if user != nil {
changed = levels . EnsureUserLevel ( user . MXID , newLevel ) || changed
}
}
if changed {
resp , err := portal . MainIntent ( ) . SetPowerLevels ( portal . MXID , levels )
if err != nil {
portal . log . Errorln ( "Failed to change power levels:" , err )
} else {
return resp . EventID
}
}
return ""
}
2018-08-26 15:11:48 +02:00
2021-02-09 22:41:14 +01:00
func ( portal * Portal ) RestrictMessageSending ( restrict bool ) id . EventID {
2018-08-26 15:11:48 +02:00
levels , err := portal . MainIntent ( ) . PowerLevels ( portal . MXID )
if err != nil {
levels = portal . GetBasePowerLevels ( )
}
2020-10-12 12:59:14 +02:00
newLevel := 0
2018-08-26 15:11:48 +02:00
if restrict {
2020-10-12 12:59:14 +02:00
newLevel = 50
2018-08-26 15:11:48 +02:00
}
2020-10-12 12:59:14 +02:00
2022-05-12 09:45:23 +02:00
changed := portal . applyPowerLevelFixes ( levels )
if levels . EventsDefault == newLevel && ! changed {
2021-02-09 22:41:14 +01:00
return ""
2020-10-12 12:59:14 +02:00
}
levels . EventsDefault = newLevel
2021-02-09 22:41:14 +01:00
resp , err := portal . MainIntent ( ) . SetPowerLevels ( portal . MXID , levels )
2018-08-30 00:10:26 +02:00
if err != nil {
portal . log . Errorln ( "Failed to change power levels:" , err )
2021-02-09 22:41:14 +01:00
return ""
} else {
return resp . EventID
2018-08-30 00:10:26 +02:00
}
2018-08-26 15:11:48 +02:00
}
2021-02-09 22:41:14 +01:00
func ( portal * Portal ) RestrictMetadataChanges ( restrict bool ) id . EventID {
2018-08-26 15:11:48 +02:00
levels , err := portal . MainIntent ( ) . PowerLevels ( portal . MXID )
if err != nil {
levels = portal . GetBasePowerLevels ( )
}
newLevel := 0
if restrict {
newLevel = 50
}
2022-05-12 09:45:23 +02:00
changed := portal . applyPowerLevelFixes ( levels )
2020-05-08 21:32:22 +02:00
changed = levels . EnsureEventLevel ( event . StateRoomName , newLevel ) || changed
changed = levels . EnsureEventLevel ( event . StateRoomAvatar , newLevel ) || changed
changed = levels . EnsureEventLevel ( event . StateTopic , newLevel ) || changed
2018-08-26 15:11:48 +02:00
if changed {
2021-02-09 22:41:14 +01:00
resp , err := portal . MainIntent ( ) . SetPowerLevels ( portal . MXID , levels )
2018-08-30 00:10:26 +02:00
if err != nil {
portal . log . Errorln ( "Failed to change power levels:" , err )
2021-02-09 22:41:14 +01:00
} else {
return resp . EventID
2018-08-30 00:10:26 +02:00
}
2018-08-26 15:11:48 +02:00
}
2021-02-09 22:41:14 +01:00
return ""
2018-08-26 15:11:48 +02:00
}
2022-05-31 16:28:58 +02:00
func ( portal * Portal ) getBridgeInfoStateKey ( ) string {
return fmt . Sprintf ( "net.maunium.whatsapp://whatsapp/%s" , portal . Key . JID )
}
2021-11-03 19:41:34 +01:00
func ( portal * Portal ) getBridgeInfo ( ) ( string , event . BridgeEventContent ) {
bridgeInfo := event . BridgeEventContent {
2020-06-15 19:38:41 +02:00
BridgeBot : portal . bridge . Bot . UserID ,
Creator : portal . MainIntent ( ) . UserID ,
2021-11-03 19:41:34 +01:00
Protocol : event . BridgeInfoSection {
2020-06-15 19:38:41 +02:00
ID : "whatsapp" ,
DisplayName : "WhatsApp" ,
2021-12-29 20:40:08 +01:00
AvatarURL : portal . bridge . Config . AppService . Bot . ParsedAvatar . CUString ( ) ,
2020-06-15 19:38:41 +02:00
ExternalURL : "https://www.whatsapp.com/" ,
} ,
2021-11-03 19:41:34 +01:00
Channel : event . BridgeInfoSection {
2021-10-22 19:14:34 +02:00
ID : portal . Key . JID . String ( ) ,
2020-06-15 19:38:41 +02:00
DisplayName : portal . Name ,
AvatarURL : portal . AvatarURL . CUString ( ) ,
2020-06-15 13:56:52 +02:00
} ,
}
2022-12-02 14:36:19 +01:00
if parent := portal . GetParentPortal ( ) ; parent != nil {
bridgeInfo . Network = & event . BridgeInfoSection {
ID : parent . Key . JID . String ( ) ,
DisplayName : parent . Name ,
AvatarURL : parent . AvatarURL . CUString ( ) ,
}
}
2022-05-31 16:28:58 +02:00
return portal . getBridgeInfoStateKey ( ) , bridgeInfo
2020-06-15 13:56:52 +02:00
}
func ( portal * Portal ) UpdateBridgeInfo ( ) {
2020-06-15 19:28:04 +02:00
if len ( portal . MXID ) == 0 {
portal . log . Debugln ( "Not updating bridge info: no Matrix room created" )
return
}
portal . log . Debugln ( "Updating bridge info..." )
2020-06-15 13:56:52 +02:00
stateKey , content := portal . getBridgeInfo ( )
2021-11-03 19:41:34 +01:00
_ , err := portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateBridge , stateKey , content )
2020-06-15 13:56:52 +02:00
if err != nil {
portal . log . Warnln ( "Failed to update m.bridge:" , err )
}
2021-11-03 19:41:34 +01:00
// TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
_ , err = portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateHalfShotBridge , stateKey , content )
2020-06-15 13:56:52 +02:00
if err != nil {
portal . log . Warnln ( "Failed to update uk.half-shot.bridge:" , err )
}
}
2022-12-14 00:00:12 +01:00
func ( portal * Portal ) updateChildRooms ( ) {
if ! portal . IsParent {
return
}
children := portal . bridge . GetAllByParentGroup ( portal . Key . JID )
for _ , child := range children {
changed := child . updateCommunitySpace ( nil , true , false )
child . UpdateBridgeInfo ( )
if changed {
portal . Update ( nil )
}
}
}
2023-04-14 12:40:34 +02:00
func ( portal * Portal ) shouldSetDMRoomMetadata ( ) bool {
return ! portal . IsPrivateChat ( ) ||
portal . bridge . Config . Bridge . PrivateChatPortalMeta == "always" ||
( portal . IsEncrypted ( ) && portal . bridge . Config . Bridge . PrivateChatPortalMeta != "never" )
}
2022-06-21 19:57:08 +02: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
}
2022-03-25 07:15:52 +01:00
func ( portal * Portal ) CreateMatrixRoom ( user * User , groupInfo * types . GroupInfo , isFullInfo , backfill bool ) error {
2018-08-23 00:12:26 +02:00
portal . roomCreateLock . Lock ( )
defer portal . roomCreateLock . Unlock ( )
2018-08-18 21:57:08 +02:00
if len ( portal . MXID ) > 0 {
return nil
}
2018-12-05 10:45:14 +01:00
intent := portal . MainIntent ( )
if err := intent . EnsureRegistered ( ) ; err != nil {
return err
}
2019-05-22 15:46:18 +02:00
portal . log . Infoln ( "Creating Matrix room. Info source:" , user . MXID )
2021-10-22 19:14:34 +02:00
//var broadcastMetadata *types.BroadcastListInfo
2018-08-24 23:45:50 +02:00
if portal . IsPrivateChat ( ) {
2019-06-01 19:03:29 +02:00
puppet := portal . bridge . GetPuppetByJID ( portal . Key . JID )
2022-05-13 10:34:45 +02:00
puppet . SyncContact ( user , true , false , "creating private chat portal" )
2023-04-14 12:40:34 +02:00
portal . Name = puppet . Displayname
portal . AvatarURL = puppet . AvatarURL
portal . Avatar = puppet . Avatar
2021-06-01 14:28:15 +02:00
portal . Topic = PrivateChatTopic
2021-02-21 13:45:33 +01:00
} else if portal . IsStatusBroadcastList ( ) {
2021-06-01 14:28:15 +02:00
if ! portal . bridge . Config . Bridge . EnableStatusBroadcast {
portal . log . Debugln ( "Status bridging is disabled in config, not creating room after all" )
return ErrStatusBroadcastDisabled
}
portal . Name = StatusBroadcastName
portal . Topic = StatusBroadcastTopic
2021-02-21 13:45:33 +01:00
} else if portal . IsBroadcastList ( ) {
2021-10-22 19:14:34 +02:00
//var err error
//broadcastMetadata, err = user.Conn.GetBroadcastMetadata(portal.Key.JID)
//if err == nil && broadcastMetadata.Status == 200 {
// portal.Name = broadcastMetadata.Name
//} else {
// user.Conn.Store.ContactsLock.RLock()
// contact, _ := user.Conn.Store.Contacts[portal.Key.JID]
// user.Conn.Store.ContactsLock.RUnlock()
// portal.Name = contact.Name
//}
//if len(portal.Name) == 0 {
// portal.Name = UnnamedBroadcastName
//}
//portal.Topic = BroadcastTopic
portal . log . Debugln ( "Broadcast list is not yet supported, not creating room after all" )
return fmt . Errorf ( "broadcast list bridging is currently not supported" )
2019-03-13 23:38:11 +01:00
} else {
2021-11-03 13:43:53 +01:00
if groupInfo == nil || ! isFullInfo {
foundInfo , err := user . Client . GetGroupInfo ( portal . Key . JID )
2022-04-26 03:46:05 +02:00
// Ensure that the user is actually a participant in the conversation
// before creating the matrix room
if errors . Is ( err , whatsmeow . ErrNotInGroup ) {
user . log . Debugfln ( "Skipping creating matrix room for %s because the user is not a participant" , portal . Key . JID )
2022-05-10 22:28:30 +02:00
user . bridge . DB . Backfill . DeleteAllForPortal ( user . MXID , portal . Key )
user . bridge . DB . HistorySync . DeleteAllMessagesForPortal ( user . MXID , portal . Key )
2022-04-26 03:46:05 +02:00
return err
} else if err != nil {
2021-10-31 18:59:23 +01:00
portal . log . Warnfln ( "Failed to get group info through %s: %v" , user . JID , err )
2021-11-03 13:43:53 +01:00
} else {
groupInfo = foundInfo
2021-11-03 20:34:06 +01:00
isFullInfo = true
2021-10-31 18:59:23 +01:00
}
}
if groupInfo != nil {
portal . Name = groupInfo . Name
portal . Topic = groupInfo . Topic
2022-12-02 14:36:19 +01:00
portal . IsParent = groupInfo . IsParent
portal . ParentGroup = groupInfo . LinkedParentJID
2022-12-02 14:53:59 +01:00
if groupInfo . IsEphemeral {
portal . ExpirationTime = groupInfo . DisappearingTimer
}
2019-03-13 23:38:11 +01:00
}
2021-10-28 11:59:22 +02:00
portal . UpdateAvatar ( user , types . EmptyJID , false )
2018-08-18 21:57:08 +02:00
}
2018-08-26 15:11:48 +02:00
2022-12-02 14:53:59 +01:00
powerLevels := portal . GetBasePowerLevels ( )
if groupInfo != nil {
if groupInfo . IsAnnounce {
powerLevels . EventsDefault = 50
}
if groupInfo . IsLocked {
powerLevels . EnsureEventLevel ( event . StateRoomName , 50 )
powerLevels . EnsureEventLevel ( event . StateRoomAvatar , 50 )
powerLevels . EnsureEventLevel ( event . StateTopic , 50 )
}
}
2020-06-15 13:56:52 +02:00
bridgeInfoStateKey , bridgeInfo := portal . getBridgeInfo ( )
2020-10-12 12:59:14 +02:00
2020-05-08 21:32:22 +02:00
initialState := [ ] * event . Event { {
Type : event . StatePowerLevels ,
Content : event . Content {
2022-12-02 14:53:59 +01:00
Parsed : powerLevels ,
2019-05-22 22:27:58 +02:00
} ,
2020-06-01 14:09:58 +02:00
} , {
2021-11-03 19:41:34 +01:00
Type : event . StateBridge ,
2020-06-15 19:38:41 +02:00
Content : event . Content { Parsed : bridgeInfo } ,
2020-06-11 13:41:45 +02:00
StateKey : & bridgeInfoStateKey ,
2020-06-01 14:09:58 +02:00
} , {
// TODO remove this once https://github.com/matrix-org/matrix-doc/pull/2346 is in spec
2021-11-03 19:41:34 +01:00
Type : event . StateHalfShotBridge ,
2020-06-15 19:38:41 +02:00
Content : event . Content { Parsed : bridgeInfo } ,
2020-06-11 13:41:45 +02:00
StateKey : & bridgeInfoStateKey ,
2019-05-22 22:27:58 +02:00
} }
2021-08-18 15:24:13 +02:00
var invite [ ] id . UserID
2020-05-12 21:25:55 +02:00
if portal . bridge . Config . Bridge . Encryption . Default {
initialState = append ( initialState , & event . Event {
Type : event . StateEncryption ,
Content : event . Content {
2022-06-21 19:57:08 +02:00
Parsed : portal . GetEncryptionEventContent ( ) ,
2020-05-12 21:25:55 +02:00
} ,
} )
portal . Encrypted = true
if portal . IsPrivateChat ( ) {
invite = append ( invite , portal . bridge . Bot . UserID )
}
}
2023-04-14 12:40:34 +02:00
if ! portal . AvatarURL . IsEmpty ( ) && portal . shouldSetDMRoomMetadata ( ) {
initialState = append ( initialState , & event . Event {
Type : event . StateRoomAvatar ,
Content : event . Content {
Parsed : event . RoomAvatarEventContent { URL : portal . AvatarURL } ,
} ,
} )
portal . AvatarSet = true
} else {
portal . AvatarSet = false
}
2020-05-12 21:25:55 +02:00
2021-11-01 10:17:44 +01:00
creationContent := make ( map [ string ] interface { } )
if ! portal . bridge . Config . Bridge . FederateRooms {
creationContent [ "m.federate" ] = false
}
2022-12-02 14:36:19 +01:00
if portal . IsParent {
creationContent [ "type" ] = event . RoomTypeSpace
2022-12-02 15:08:35 +01:00
} else if parent := portal . GetParentPortal ( ) ; parent != nil && parent . MXID != "" {
2022-12-02 14:36:19 +01:00
initialState = append ( initialState , & event . Event {
2022-12-02 15:08:35 +01:00
Type : event . StateSpaceParent ,
StateKey : proto . String ( parent . MXID . String ( ) ) ,
2022-12-02 14:36:19 +01:00
Content : event . Content {
Parsed : & event . SpaceParentEventContent {
Via : [ ] string { portal . bridge . Config . Homeserver . Domain } ,
Canonical : true ,
} ,
} ,
} )
}
2022-10-10 16:07:37 +02:00
autoJoinInvites := portal . bridge . Config . Homeserver . Software == bridgeconfig . SoftwareHungry
if autoJoinInvites {
portal . log . Debugfln ( "Hungryserv mode: adding all group members in create request" )
if groupInfo != nil {
// TODO non-hungryserv could also include all members in invites, and then send joins manually?
participants , powerLevels := portal . SyncParticipants ( user , groupInfo )
invite = append ( invite , participants ... )
if initialState [ 0 ] . Type != event . StatePowerLevels {
panic ( fmt . Errorf ( "unexpected type %s in first initial state event" , initialState [ 0 ] . Type . Type ) )
}
initialState [ 0 ] . Content . Parsed = powerLevels
} else {
invite = append ( invite , user . MXID )
}
}
2023-04-14 12:40:34 +02:00
req := & mautrix . ReqCreateRoom {
2021-11-01 10:17:44 +01:00
Visibility : "private" ,
Name : portal . Name ,
Topic : portal . Topic ,
Invite : invite ,
Preset : "private_chat" ,
IsDirect : portal . IsPrivateChat ( ) ,
InitialState : initialState ,
CreationContent : creationContent ,
2022-10-10 16:07:37 +02:00
BeeperAutoJoinInvites : autoJoinInvites ,
2023-04-14 12:40:34 +02:00
}
if ! portal . shouldSetDMRoomMetadata ( ) {
req . Name = ""
}
resp , err := intent . CreateRoom ( req )
2018-08-18 21:57:08 +02:00
if err != nil {
return err
}
2022-12-14 00:00:12 +01:00
portal . log . Infoln ( "Matrix room created:" , resp . RoomID )
2022-12-02 14:36:19 +01:00
portal . InSpace = false
2023-04-14 12:40:34 +02:00
portal . NameSet = len ( req . Name ) > 0
portal . TopicSet = len ( req . Topic ) > 0
2018-08-18 21:57:08 +02:00
portal . MXID = resp . RoomID
2020-05-28 19:35:43 +02:00
portal . bridge . portalsLock . Lock ( )
portal . bridge . portalsByMXID [ portal . MXID ] = portal
portal . bridge . portalsLock . Unlock ( )
2022-05-29 18:22:07 +02:00
portal . Update ( nil )
2020-05-12 21:25:55 +02:00
// We set the memberships beforehand to make sure the encryption key exchange in initial backfill knows the users are here.
2022-10-10 16:07:37 +02:00
inviteMembership := event . MembershipInvite
if autoJoinInvites {
inviteMembership = event . MembershipJoin
}
2021-08-18 15:24:13 +02:00
for _ , userID := range invite {
2022-10-10 16:07:37 +02:00
portal . bridge . StateStore . SetMembership ( portal . MXID , userID , inviteMembership )
2020-05-12 21:25:55 +02:00
}
2022-10-10 16:07:37 +02:00
if ! autoJoinInvites {
portal . ensureUserInvited ( user )
}
2021-10-28 11:59:22 +02:00
user . syncChatDoublePuppetDetails ( portal , true )
2021-08-18 15:24:13 +02:00
2022-12-02 15:08:35 +01:00
go portal . updateCommunitySpace ( user , true , true )
2022-12-02 14:36:19 +01:00
go portal . addToPersonalSpace ( user )
2021-12-28 21:14:09 +01:00
2022-12-02 14:53:59 +01:00
if groupInfo != nil && ! autoJoinInvites {
portal . SyncParticipants ( user , groupInfo )
2019-05-22 22:27:58 +02:00
}
2021-10-22 19:14:34 +02:00
//if broadcastMetadata != nil {
// portal.SyncBroadcastRecipients(user, broadcastMetadata)
//}
2021-10-28 12:57:15 +02:00
if portal . IsPrivateChat ( ) {
2020-01-07 20:25:41 +01:00
puppet := user . bridge . GetPuppetByJID ( portal . Key . JID )
2020-05-12 21:25:55 +02:00
if portal . bridge . Config . Bridge . Encryption . Default {
err = portal . bridge . Bot . EnsureJoined ( portal . MXID )
if err != nil {
portal . log . Errorln ( "Failed to join created portal with bridge bot for e2be:" , err )
}
}
2020-08-22 12:07:55 +02:00
user . UpdateDirectChats ( map [ id . UserID ] [ ] id . RoomID { puppet . MXID : { portal . MXID } } )
2022-12-14 00:00:12 +01:00
} else if portal . IsParent {
portal . updateChildRooms ( )
2020-01-07 20:25:41 +01:00
}
2021-02-26 15:09:24 +01:00
2021-10-26 16:01:10 +02:00
firstEventResp , err := portal . MainIntent ( ) . SendMessageEvent ( portal . MXID , PortalCreationDummyEvent , struct { } { } )
if err != nil {
portal . log . Errorln ( "Failed to send dummy event to mark portal creation:" , err )
} else {
portal . FirstEventID = firstEventResp . EventID
2022-05-13 01:56:40 +02:00
portal . Update ( nil )
2021-10-26 16:01:10 +02:00
}
2022-03-25 07:15:52 +01:00
if user . bridge . Config . Bridge . HistorySync . Backfill && backfill {
2022-04-24 05:50:41 +02:00
portals := [ ] * Portal { portal }
2022-11-10 18:07:08 +01:00
user . EnqueueImmediateBackfills ( portals )
2022-04-24 05:50:41 +02:00
user . EnqueueDeferredBackfills ( portals )
2022-05-13 21:18:52 +02:00
user . BackfillQueue . ReCheck ( )
2022-03-25 07:15:52 +01:00
}
2018-08-18 21:57:08 +02:00
return nil
}
2022-12-02 14:36:19 +01:00
func ( portal * Portal ) addToPersonalSpace ( user * User ) {
2021-12-29 20:40:08 +01:00
spaceID := user . GetSpaceRoom ( )
if len ( spaceID ) == 0 || user . IsInSpace ( portal . Key ) {
return
}
_ , err := portal . bridge . Bot . SendStateEvent ( spaceID , event . StateSpaceChild , portal . MXID . String ( ) , & event . SpaceChildEventContent {
Via : [ ] string { portal . bridge . Config . Homeserver . Domain } ,
} )
if err != nil {
portal . log . Errorfln ( "Failed to add room to %s's personal filtering space (%s): %v" , user . MXID , spaceID , err )
} else {
portal . log . Debugfln ( "Added room to %s's personal filtering space (%s)" , user . MXID , spaceID )
user . MarkInSpace ( portal . Key )
}
2021-12-28 21:14:09 +01:00
}
2022-12-03 15:48:38 +01:00
func ( portal * Portal ) removeSpaceParentEvent ( space id . RoomID ) {
_ , err := portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateSpaceParent , space . String ( ) , & event . SpaceParentEventContent { } )
if err != nil {
portal . log . Warnfln ( "Failed to send m.space.parent event to remove portal from %s: %v" , space , err )
}
}
2022-12-05 17:04:21 +01:00
func ( portal * Portal ) updateCommunitySpace ( user * User , add , updateInfo bool ) bool {
2022-12-02 14:36:19 +01:00
if add == portal . InSpace {
return false
}
space := portal . GetParentPortal ( )
2022-12-02 15:08:35 +01:00
if space == nil {
2022-12-02 14:36:19 +01:00
return false
2022-12-02 15:08:35 +01:00
} else if space . MXID == "" {
2022-12-14 00:00:12 +01:00
if ! add || user == nil {
2022-12-02 15:08:35 +01:00
return false
}
portal . log . Debugfln ( "Creating portal for parent group %v" , space . Key . JID )
err := space . CreateMatrixRoom ( user , nil , false , false )
if err != nil {
portal . log . Debugfln ( "Failed to create portal for parent group: %v" , err )
return false
}
2022-12-02 14:36:19 +01:00
}
var action string
var parentContent event . SpaceParentEventContent
var childContent event . SpaceChildEventContent
if add {
parentContent . Canonical = true
parentContent . Via = [ ] string { portal . bridge . Config . Homeserver . Domain }
childContent . Via = [ ] string { portal . bridge . Config . Homeserver . Domain }
action = "add portal to"
portal . log . Debugfln ( "Adding %s to space %s (%s)" , portal . MXID , space . MXID , space . Key . JID )
} else {
action = "remove portal from"
portal . log . Debugfln ( "Removing %s from space %s (%s)" , portal . MXID , space . MXID , space . Key . JID )
}
_ , err := space . MainIntent ( ) . SendStateEvent ( space . MXID , event . StateSpaceChild , portal . MXID . String ( ) , & childContent )
if err != nil {
portal . log . Errorfln ( "Failed to send m.space.child event to %s %s: %v" , action , space . MXID , err )
return false
}
_ , err = portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateSpaceParent , space . MXID . String ( ) , & parentContent )
if err != nil {
portal . log . Warnfln ( "Failed to send m.space.parent event to %s %s: %v" , action , space . MXID , err )
}
portal . InSpace = add
2022-12-05 17:04:21 +01:00
if updateInfo {
2022-12-02 15:08:35 +01:00
portal . Update ( nil )
2022-12-05 17:04:21 +01:00
portal . UpdateBridgeInfo ( )
2022-12-02 15:08:35 +01:00
}
2022-12-02 14:36:19 +01:00
return true
}
2018-08-18 21:57:08 +02:00
func ( portal * Portal ) IsPrivateChat ( ) bool {
2021-10-22 19:14:34 +02:00
return portal . Key . JID . Server == types . DefaultUserServer
2018-08-18 21:57:08 +02:00
}
2022-01-07 13:32:00 +01:00
func ( portal * Portal ) IsGroupChat ( ) bool {
return portal . Key . JID . Server == types . GroupServer
}
2021-02-21 13:45:33 +01:00
func ( portal * Portal ) IsBroadcastList ( ) bool {
2021-10-22 19:14:34 +02:00
return portal . Key . JID . Server == types . BroadcastServer
2021-02-21 13:18:15 +01:00
}
2021-02-21 13:45:33 +01:00
func ( portal * Portal ) IsStatusBroadcastList ( ) bool {
2021-10-22 19:14:34 +02:00
return portal . Key . JID == types . StatusBroadcastJID
2021-02-21 13:18:15 +01:00
}
2019-11-10 20:22:11 +01:00
func ( portal * Portal ) HasRelaybot ( ) bool {
2021-10-28 12:57:15 +02:00
return portal . bridge . Config . Bridge . Relay . Enabled && len ( portal . RelayUserID ) > 0
}
func ( portal * Portal ) GetRelayUser ( ) * User {
if ! portal . HasRelaybot ( ) {
return nil
} else if portal . relayUser == nil {
portal . relayUser = portal . bridge . GetUserByMXID ( portal . RelayUserID )
2019-11-10 20:22:11 +01:00
}
2021-10-28 12:57:15 +02:00
return portal . relayUser
2019-11-10 20:22:11 +01:00
}
2022-12-02 14:36:19 +01:00
func ( portal * Portal ) GetParentPortal ( ) * Portal {
if portal . ParentGroup . IsEmpty ( ) {
return nil
} else if portal . parentPortal == nil {
portal . parentPortal = portal . bridge . GetPortalByJID ( database . NewPortalKey ( portal . ParentGroup , portal . ParentGroup ) )
}
return portal . parentPortal
}
2018-08-18 21:57:08 +02:00
func ( portal * Portal ) MainIntent ( ) * appservice . IntentAPI {
if portal . IsPrivateChat ( ) {
2019-05-24 01:33:26 +02:00
return portal . bridge . GetPuppetByJID ( portal . Key . JID ) . DefaultIntent ( )
2018-08-18 21:57:08 +02:00
}
2018-08-30 00:10:26 +02:00
return portal . bridge . Bot
2018-08-18 21:57:08 +02:00
}
2023-05-24 11:41:54 +02:00
func ( portal * Portal ) addReplyMention ( content * event . MessageEventContent , sender types . JID ) {
if content . Mentions == nil {
return
}
var mxid id . UserID
if user := portal . bridge . GetUserByJID ( sender ) ; user != nil {
mxid = user . MXID
} else {
puppet := portal . bridge . GetPuppetByJID ( sender )
mxid = puppet . MXID
}
if slices . Contains ( content . Mentions . UserIDs , mxid ) {
content . Mentions . UserIDs = append ( content . Mentions . UserIDs , mxid )
}
}
2022-08-25 16:05:40 +02:00
func ( portal * Portal ) SetReply ( content * event . MessageEventContent , replyTo * ReplyInfo , isBackfill bool ) bool {
if replyTo == nil {
2021-11-05 10:47:51 +01:00
return false
2018-08-23 23:52:06 +02:00
}
2023-03-08 16:14:31 +01:00
key := portal . Key
targetPortal := portal
defer func ( ) {
if content . RelatesTo != nil && content . RelatesTo . InReplyTo != nil && targetPortal != portal {
content . RelatesTo . InReplyTo . UnstableRoomID = targetPortal . MXID
}
} ( )
if portal . bridge . Config . Bridge . CrossRoomReplies && ! replyTo . Chat . IsEmpty ( ) && replyTo . Chat != key . JID {
if replyTo . Chat . Server == types . GroupServer {
key = database . NewPortalKey ( replyTo . Chat , types . EmptyJID )
2023-04-26 14:16:36 +02:00
} else if replyTo . Chat == types . StatusBroadcastJID {
2023-03-08 16:14:31 +01:00
key = database . NewPortalKey ( replyTo . Chat , key . Receiver )
}
if key != portal . Key {
targetPortal = portal . bridge . GetExistingPortalByJID ( key )
if targetPortal == nil {
return false
}
}
}
message := portal . bridge . DB . Message . GetByJID ( key , replyTo . MessageID )
2021-11-05 10:47:51 +01:00
if message == nil || message . IsFakeMXID ( ) {
2022-08-25 16:05:40 +02:00
if isBackfill && portal . bridge . Config . Homeserver . Software == bridgeconfig . SoftwareHungry {
2023-03-08 16:14:31 +01:00
content . RelatesTo = ( & event . RelatesTo { } ) . SetReplyTo ( targetPortal . deterministicEventID ( replyTo . Sender , replyTo . MessageID , "" ) )
2023-05-24 11:41:54 +02:00
portal . addReplyMention ( content , replyTo . Sender )
2022-08-25 16:05:40 +02:00
return true
}
2021-11-05 10:47:51 +01:00
return false
}
2023-05-24 11:41:54 +02:00
// TODO store sender mxid in db message
portal . addReplyMention ( content , message . Sender )
2023-04-19 16:26:21 +02:00
content . RelatesTo = ( & event . RelatesTo { } ) . SetReplyTo ( message . MXID )
if portal . bridge . Config . Bridge . DisableReplyFallbacks {
return true
}
2023-03-08 16:14:31 +01:00
evt , err := targetPortal . MainIntent ( ) . GetEvent ( targetPortal . MXID , message . MXID )
2021-11-05 10:47:51 +01:00
if err != nil {
portal . log . Warnln ( "Failed to get reply target:" , err )
return true
}
_ = evt . Content . ParseRaw ( evt . Type )
if evt . Type == event . EventEncrypted {
decryptedEvt , err := portal . bridge . Crypto . Decrypt ( evt )
if err != nil {
portal . log . Warnln ( "Failed to decrypt reply target:" , err )
} else {
evt = decryptedEvt
2020-06-30 15:26:13 +02:00
}
2018-08-23 23:52:06 +02:00
}
2021-11-05 10:47:51 +01:00
content . SetReply ( evt )
return true
2018-08-24 21:05:38 +02:00
}
2022-03-05 20:22:31 +01:00
func ( portal * Portal ) HandleMessageReaction ( intent * appservice . IntentAPI , user * User , info * types . MessageInfo , reaction * waProto . ReactionMessage , existingMsg * database . Message ) {
if existingMsg != nil {
2022-03-12 18:47:47 +01:00
_ , _ = portal . MainIntent ( ) . RedactEvent ( portal . MXID , existingMsg . MXID , mautrix . ReqRedact {
2022-03-05 20:22:31 +01:00
Reason : "The undecryptable message was actually a reaction" ,
} )
}
2022-03-12 18:47:47 +01:00
targetJID := reaction . GetKey ( ) . GetId ( )
if reaction . GetText ( ) == "" {
existing := portal . bridge . DB . Reaction . GetByTargetJID ( portal . Key , targetJID , info . Sender )
if existing == nil {
portal . log . Debugfln ( "Dropping removal %s of unknown reaction to %s from %s" , info . ID , targetJID , info . Sender )
return
}
2022-03-05 20:22:31 +01:00
2022-06-30 19:56:25 +02:00
resp , err := intent . RedactEvent ( portal . MXID , existing . MXID )
2022-03-12 18:47:47 +01:00
if err != nil {
portal . log . Errorfln ( "Failed to redact reaction %s/%s from %s to %s: %v" , existing . MXID , existing . JID , info . Sender , targetJID , err )
}
portal . finishHandling ( existingMsg , info , resp . EventID , database . MsgReaction , database . MsgNoError )
existing . Delete ( )
} else {
target := portal . bridge . DB . Message . GetByJID ( portal . Key , targetJID )
if target == nil {
portal . log . Debugfln ( "Dropping reaction %s from %s to unknown message %s" , info . ID , info . Sender , targetJID )
return
}
2022-03-05 20:22:31 +01:00
2022-06-30 19:56:25 +02:00
var content event . ReactionEventContent
2022-03-12 18:47:47 +01:00
content . RelatesTo = event . RelatesTo {
Type : event . RelAnnotation ,
EventID : target . MXID ,
2022-03-18 00:12:23 +01:00
Key : variationselector . Add ( reaction . GetText ( ) ) ,
2022-03-12 18:47:47 +01:00
}
resp , err := intent . SendMassagedMessageEvent ( portal . MXID , event . EventReaction , & content , info . Timestamp . UnixMilli ( ) )
if err != nil {
portal . log . Errorfln ( "Failed to bridge reaction %s from %s to %s: %v" , info . ID , info . Sender , target . JID , err )
return
}
portal . finishHandling ( existingMsg , info , resp . EventID , database . MsgReaction , database . MsgNoError )
2022-09-28 14:54:02 +02:00
portal . upsertReaction ( nil , intent , target . JID , info . Sender , resp . EventID , info . ID )
2022-03-12 18:47:47 +01:00
}
2022-03-05 20:22:31 +01:00
}
2021-10-27 20:09:36 +02:00
func ( portal * Portal ) HandleMessageRevoke ( user * User , info * types . MessageInfo , key * waProto . MessageKey ) bool {
2021-10-22 19:14:34 +02:00
msg := portal . bridge . DB . Message . GetByJID ( portal . Key , key . GetId ( ) )
2021-02-09 22:41:14 +01:00
if msg == nil || msg . IsFakeMXID ( ) {
2021-06-25 14:33:37 +02:00
return false
2019-05-16 00:59:36 +02:00
}
2021-10-27 20:09:36 +02:00
intent := portal . bridge . GetPuppetByJID ( info . Sender ) . IntentFor ( portal )
2022-06-30 19:56:25 +02:00
_ , err := intent . RedactEvent ( portal . MXID , msg . MXID )
2019-05-16 00:59:36 +02:00
if err != nil {
2021-10-27 20:09:36 +02:00
if errors . Is ( err , mautrix . MForbidden ) {
2022-06-30 19:56:25 +02:00
_ , err = portal . MainIntent ( ) . RedactEvent ( portal . MXID , msg . MXID )
2021-10-27 20:09:36 +02:00
if err != nil {
portal . log . Errorln ( "Failed to redact %s: %v" , msg . JID , err )
}
}
2021-06-25 14:33:37 +02:00
} else {
msg . Delete ( )
2019-05-16 00:59:36 +02:00
}
2021-06-25 14:33:37 +02:00
return true
2019-05-16 00:59:36 +02:00
}
2022-07-18 15:08:52 +02:00
func ( portal * Portal ) deleteForMe ( user * User , content * events . DeleteForMe ) bool {
matrixUsers , err := portal . GetMatrixUsers ( )
if err != nil {
portal . log . Errorln ( "Failed to get Matrix users in portal to see if DeleteForMe should be handled:" , err )
return false
}
if len ( matrixUsers ) == 1 && matrixUsers [ 0 ] == user . MXID {
msg := portal . bridge . DB . Message . GetByJID ( portal . Key , content . MessageID )
if msg == nil || msg . IsFakeMXID ( ) {
return false
}
_ , err := portal . MainIntent ( ) . RedactEvent ( portal . MXID , msg . MXID )
if err != nil {
portal . log . Errorln ( "Failed to redact %s: %v" , msg . JID , err )
} else {
msg . Delete ( )
}
return true
}
return false
}
2021-10-31 19:42:53 +01:00
func ( portal * Portal ) sendMainIntentMessage ( content * event . MessageEventContent ) ( * mautrix . RespSendEvent , error ) {
return portal . sendMessage ( portal . MainIntent ( ) , event . EventMessage , content , nil , 0 )
2020-05-09 01:03:59 +02:00
}
2022-06-30 19:56:25 +02:00
func ( portal * Portal ) encrypt ( intent * appservice . IntentAPI , content * event . Content , eventType event . Type ) ( event . Type , error ) {
if ! portal . Encrypted || portal . bridge . Crypto == nil {
return eventType , nil
2021-10-26 16:01:10 +02:00
}
2022-06-30 19:56:25 +02:00
intent . AddDoublePuppetValue ( content )
// TODO maybe the locking should be inside mautrix-go?
portal . encryptLock . Lock ( )
defer portal . encryptLock . Unlock ( )
err := portal . bridge . Crypto . Encrypt ( portal . MXID , eventType , content )
if err != nil {
return eventType , fmt . Errorf ( "failed to encrypt event: %w" , err )
}
return event . EventEncrypted , nil
2021-10-26 16:01:10 +02:00
}
2021-10-31 19:42:53 +01:00
func ( portal * Portal ) sendMessage ( intent * appservice . IntentAPI , eventType event . Type , content * event . MessageEventContent , extraContent map [ string ] interface { } , timestamp int64 ) ( * mautrix . RespSendEvent , error ) {
wrappedContent := event . Content { Parsed : content , Raw : extraContent }
2021-10-26 16:01:10 +02:00
var err error
2022-06-30 19:56:25 +02:00
eventType , err = portal . encrypt ( intent , & wrappedContent , eventType )
2021-10-26 16:01:10 +02:00
if err != nil {
return nil , err
2020-05-09 01:03:59 +02:00
}
2022-01-04 01:02:06 +01:00
2021-07-05 18:26:33 +02:00
_ , _ = intent . UserTyping ( portal . MXID , false , 0 )
2020-05-09 01:03:59 +02:00
if timestamp == 0 {
return intent . SendMessageEvent ( portal . MXID , eventType , & wrappedContent )
} else {
return intent . SendMassagedMessageEvent ( portal . MXID , eventType , & wrappedContent , timestamp )
}
}
2022-08-25 16:05:40 +02:00
type ReplyInfo struct {
MessageID types . MessageID
2023-03-08 16:14:31 +01:00
Chat types . JID
2022-08-25 16:05:40 +02:00
Sender types . JID
}
type Replyable interface {
GetStanzaId ( ) string
GetParticipant ( ) string
2023-03-08 16:14:31 +01:00
GetRemoteJid ( ) string
2022-08-25 16:05:40 +02:00
}
func GetReply ( replyable Replyable ) * ReplyInfo {
if replyable . GetStanzaId ( ) == "" {
return nil
}
sender , err := types . ParseJID ( replyable . GetParticipant ( ) )
if err != nil {
return nil
}
2023-03-08 16:14:31 +01:00
chat , _ := types . ParseJID ( replyable . GetRemoteJid ( ) )
2022-08-25 16:05:40 +02:00
return & ReplyInfo {
MessageID : types . MessageID ( replyable . GetStanzaId ( ) ) ,
2023-03-08 16:14:31 +01:00
Chat : chat ,
2022-08-25 16:05:40 +02:00
Sender : sender ,
}
}
2021-10-26 16:01:10 +02:00
type ConvertedMessage struct {
Intent * appservice . IntentAPI
Type event . Type
Content * event . MessageEventContent
2021-10-31 19:42:53 +01:00
Extra map [ string ] interface { }
2021-10-26 16:01:10 +02:00
Caption * event . MessageEventContent
2021-11-05 10:47:51 +01:00
2022-01-03 15:11:39 +01:00
MultiEvent [ ] * event . MessageEventContent
2022-08-25 16:05:40 +02:00
ReplyTo * ReplyInfo
2023-01-16 16:15:04 +01:00
ExpiresIn time . Duration
2022-02-10 18:18:49 +01:00
Error database . MessageErrorType
2022-05-02 14:00:57 +02:00
MediaKey [ ] byte
2021-10-26 16:01:10 +02:00
}
2018-08-23 00:12:26 +02:00
2022-06-17 10:34:24 +02:00
func ( cm * ConvertedMessage ) MergeCaption ( ) {
if cm . Caption == nil {
return
}
2022-06-28 11:09:12 +02:00
cm . Content . FileName = cm . Content . Body
2022-06-17 10:34:24 +02:00
extensibleCaption := map [ string ] interface { } {
"org.matrix.msc1767.text" : cm . Caption . Body ,
}
cm . Extra [ "org.matrix.msc1767.caption" ] = extensibleCaption
cm . Content . Body = cm . Caption . Body
if cm . Caption . Format == event . FormatHTML {
cm . Content . Format = event . FormatHTML
cm . Content . FormattedBody = cm . Caption . FormattedBody
extensibleCaption [ "org.matrix.msc1767.html" ] = cm . Caption . FormattedBody
}
cm . Caption = nil
}
2022-02-04 21:19:55 +01:00
func ( portal * Portal ) convertTextMessage ( intent * appservice . IntentAPI , source * User , msg * waProto . Message ) * ConvertedMessage {
2020-05-08 21:32:22 +02:00
content := & event . MessageEventContent {
2021-10-26 16:01:10 +02:00
Body : msg . GetConversation ( ) ,
2020-05-08 21:32:22 +02:00
MsgType : event . MsgText ,
2018-08-24 18:46:14 +02:00
}
2022-02-15 12:15:41 +01:00
if len ( msg . GetExtendedTextMessage ( ) . GetText ( ) ) > 0 {
2021-10-26 16:01:10 +02:00
content . Body = msg . GetExtendedTextMessage ( ) . GetText ( )
2018-08-23 00:12:26 +02:00
}
2018-08-19 17:21:38 +02:00
2022-02-15 12:15:41 +01:00
contextInfo := msg . GetExtendedTextMessage ( ) . GetContextInfo ( )
2022-06-24 22:25:37 +02:00
portal . bridge . Formatter . ParseWhatsApp ( portal . MXID , content , contextInfo . GetMentionedJid ( ) , false , false )
2023-01-16 16:15:04 +01:00
expiresIn := time . Duration ( contextInfo . GetExpiration ( ) ) * time . Second
2022-02-15 12:15:41 +01:00
extraAttrs := map [ string ] interface { } { }
extraAttrs [ "com.beeper.linkpreviews" ] = portal . convertURLPreviewToBeeper ( intent , source , msg . GetExtendedTextMessage ( ) )
2022-01-07 13:32:00 +01:00
return & ConvertedMessage {
Intent : intent ,
Type : event . EventMessage ,
Content : content ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( contextInfo ) ,
2022-01-07 13:32:00 +01:00
ExpiresIn : expiresIn ,
2022-02-04 01:33:21 +01:00
Extra : extraAttrs ,
2022-01-07 13:32:00 +01:00
}
2021-02-09 22:41:14 +01:00
}
2022-06-24 20:50:58 +02:00
func ( portal * Portal ) convertTemplateMessage ( intent * appservice . IntentAPI , source * User , info * types . MessageInfo , tplMsg * waProto . TemplateMessage ) * ConvertedMessage {
converted := & ConvertedMessage {
Intent : intent ,
Type : event . EventMessage ,
Content : & event . MessageEventContent {
Body : "Unsupported business message" ,
MsgType : event . MsgText ,
} ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( tplMsg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( tplMsg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2022-06-24 20:50:58 +02:00
}
tpl := tplMsg . GetHydratedTemplate ( )
if tpl == nil {
return converted
}
content := tpl . GetHydratedContentText ( )
if buttons := tpl . GetHydratedButtons ( ) ; len ( buttons ) > 0 {
addButtonText := false
descriptions := make ( [ ] string , len ( buttons ) )
for i , rawButton := range buttons {
switch button := rawButton . GetHydratedButton ( ) . ( type ) {
case * waProto . HydratedTemplateButton_QuickReplyButton :
descriptions [ i ] = fmt . Sprintf ( "<%s>" , button . QuickReplyButton . GetDisplayText ( ) )
addButtonText = true
case * waProto . HydratedTemplateButton_UrlButton :
descriptions [ i ] = fmt . Sprintf ( "[%s](%s)" , button . UrlButton . GetDisplayText ( ) , button . UrlButton . GetUrl ( ) )
case * waProto . HydratedTemplateButton_CallButton :
descriptions [ i ] = fmt . Sprintf ( "[%s](tel:%s)" , button . CallButton . GetDisplayText ( ) , button . CallButton . GetPhoneNumber ( ) )
}
}
description := strings . Join ( descriptions , " - " )
if addButtonText {
description += "\nUse the WhatsApp app to click buttons"
}
content = fmt . Sprintf ( "%s\n\n%s" , content , description )
}
if footer := tpl . GetHydratedFooterText ( ) ; footer != "" {
content = fmt . Sprintf ( "%s\n\n%s" , content , footer )
}
var convertedTitle * ConvertedMessage
switch title := tpl . GetTitle ( ) . ( type ) {
2022-07-30 10:30:44 +02:00
case * waProto . TemplateMessage_HydratedFourRowTemplate_DocumentMessage :
2022-06-24 20:50:58 +02:00
convertedTitle = portal . convertMediaMessage ( intent , source , info , title . DocumentMessage , "file attachment" , false )
2022-07-30 10:30:44 +02:00
case * waProto . TemplateMessage_HydratedFourRowTemplate_ImageMessage :
2022-06-24 20:50:58 +02:00
convertedTitle = portal . convertMediaMessage ( intent , source , info , title . ImageMessage , "photo" , false )
2022-07-30 10:30:44 +02:00
case * waProto . TemplateMessage_HydratedFourRowTemplate_VideoMessage :
2022-06-24 20:50:58 +02:00
convertedTitle = portal . convertMediaMessage ( intent , source , info , title . VideoMessage , "video attachment" , false )
2022-07-30 10:30:44 +02:00
case * waProto . TemplateMessage_HydratedFourRowTemplate_LocationMessage :
2022-06-24 20:50:58 +02:00
content = fmt . Sprintf ( "Unsupported location message\n\n%s" , content )
2022-07-30 10:30:44 +02:00
case * waProto . TemplateMessage_HydratedFourRowTemplate_HydratedTitleText :
2022-06-24 20:50:58 +02:00
content = fmt . Sprintf ( "%s\n\n%s" , title . HydratedTitleText , content )
}
converted . Content . Body = content
2022-06-24 22:25:37 +02:00
portal . bridge . Formatter . ParseWhatsApp ( portal . MXID , converted . Content , nil , true , false )
2022-06-24 20:50:58 +02:00
if convertedTitle != nil {
converted . MediaKey = convertedTitle . MediaKey
converted . Extra = convertedTitle . Extra
converted . Caption = converted . Content
converted . Content = convertedTitle . Content
converted . Error = convertedTitle . Error
}
if converted . Extra == nil {
converted . Extra = make ( map [ string ] interface { } )
}
converted . Extra [ "fi.mau.whatsapp.hydrated_template_id" ] = tpl . GetTemplateId ( )
return converted
}
2022-06-24 22:25:37 +02:00
func ( portal * Portal ) convertTemplateButtonReplyMessage ( intent * appservice . IntentAPI , msg * waProto . TemplateButtonReplyMessage ) * ConvertedMessage {
return & ConvertedMessage {
Intent : intent ,
Type : event . EventMessage ,
Content : & event . MessageEventContent {
Body : msg . GetSelectedDisplayText ( ) ,
MsgType : event . MsgText ,
} ,
Extra : map [ string ] interface { } {
"fi.mau.whatsapp.template_button_reply" : map [ string ] interface { } {
"id" : msg . GetSelectedId ( ) ,
"index" : msg . GetSelectedIndex ( ) ,
} ,
} ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2022-06-24 22:25:37 +02:00
}
}
func ( portal * Portal ) convertListMessage ( intent * appservice . IntentAPI , source * User , msg * waProto . ListMessage ) * ConvertedMessage {
converted := & ConvertedMessage {
Intent : intent ,
Type : event . EventMessage ,
Content : & event . MessageEventContent {
Body : "Unsupported business message" ,
MsgType : event . MsgText ,
} ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2022-06-24 22:25:37 +02:00
}
body := msg . GetDescription ( )
if msg . GetTitle ( ) != "" {
if body == "" {
body = msg . GetTitle ( )
} else {
body = fmt . Sprintf ( "%s\n\n%s" , msg . GetTitle ( ) , body )
}
}
2022-08-04 19:42:03 +02:00
randomID := util . RandomString ( 64 )
2022-06-24 22:25:37 +02:00
body = fmt . Sprintf ( "%s\n%s" , body , randomID )
if msg . GetFooterText ( ) != "" {
body = fmt . Sprintf ( "%s\n\n%s" , body , msg . GetFooterText ( ) )
}
converted . Content . Body = body
portal . bridge . Formatter . ParseWhatsApp ( portal . MXID , converted . Content , nil , false , true )
var optionsMarkdown strings . Builder
_ , _ = fmt . Fprintf ( & optionsMarkdown , "#### %s\n" , msg . GetButtonText ( ) )
for _ , section := range msg . GetSections ( ) {
nesting := ""
if section . GetTitle ( ) != "" {
_ , _ = fmt . Fprintf ( & optionsMarkdown , "* %s\n" , section . GetTitle ( ) )
nesting = " "
}
for _ , row := range section . GetRows ( ) {
if row . GetDescription ( ) != "" {
_ , _ = fmt . Fprintf ( & optionsMarkdown , "%s* %s: %s\n" , nesting , row . GetTitle ( ) , row . GetDescription ( ) )
} else {
_ , _ = fmt . Fprintf ( & optionsMarkdown , "%s* %s\n" , nesting , row . GetTitle ( ) )
}
}
}
optionsMarkdown . WriteString ( "\nUse the WhatsApp app to respond" )
rendered := format . RenderMarkdown ( optionsMarkdown . String ( ) , true , false )
converted . Content . Body = strings . Replace ( converted . Content . Body , randomID , rendered . Body , 1 )
converted . Content . FormattedBody = strings . Replace ( converted . Content . FormattedBody , randomID , rendered . FormattedBody , 1 )
return converted
}
func ( portal * Portal ) convertListResponseMessage ( intent * appservice . IntentAPI , msg * waProto . ListResponseMessage ) * ConvertedMessage {
var body string
if msg . GetTitle ( ) != "" {
if msg . GetDescription ( ) != "" {
body = fmt . Sprintf ( "%s\n\n%s" , msg . GetTitle ( ) , msg . GetDescription ( ) )
} else {
body = msg . GetTitle ( )
}
} else if msg . GetDescription ( ) != "" {
body = msg . GetDescription ( )
} else {
body = "Unsupported list reply message"
}
return & ConvertedMessage {
Intent : intent ,
Type : event . EventMessage ,
Content : & event . MessageEventContent {
Body : body ,
MsgType : event . MsgText ,
} ,
Extra : map [ string ] interface { } {
"fi.mau.whatsapp.list_reply" : map [ string ] interface { } {
"row_id" : msg . GetSingleSelectReply ( ) . GetSelectedRowId ( ) ,
} ,
} ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2022-06-24 22:25:37 +02:00
}
}
2022-11-17 22:30:42 +01:00
func ( portal * Portal ) convertPollUpdateMessage ( intent * appservice . IntentAPI , source * User , info * types . MessageInfo , msg * waProto . PollUpdateMessage ) * ConvertedMessage {
pollMessage := portal . bridge . DB . Message . GetByJID ( portal . Key , msg . GetPollCreationMessageKey ( ) . GetId ( ) )
if pollMessage == nil {
portal . log . Warnfln ( "Failed to convert vote message %s: poll message %s not found" , info . ID , msg . GetPollCreationMessageKey ( ) . GetId ( ) )
return nil
}
vote , err := source . Client . DecryptPollVote ( & events . Message {
Info : * info ,
Message : & waProto . Message { PollUpdateMessage : msg } ,
} )
if err != nil {
portal . log . Errorfln ( "Failed to decrypt vote message %s: %v" , info . ID , err )
return nil
}
selectedHashes := make ( [ ] string , len ( vote . GetSelectedOptions ( ) ) )
2022-12-23 14:17:57 +01:00
if pollMessage . Type == database . MsgMatrixPoll {
mappedAnswers := pollMessage . GetPollOptionIDs ( vote . GetSelectedOptions ( ) )
for i , opt := range vote . GetSelectedOptions ( ) {
if len ( opt ) != 32 {
portal . log . Warnfln ( "Unexpected option hash length %d in %s's vote to %s" , len ( opt ) , info . Sender , pollMessage . MXID )
continue
}
var ok bool
selectedHashes [ i ] , ok = mappedAnswers [ * ( * [ 32 ] byte ) ( opt ) ]
if ! ok {
portal . log . Warnfln ( "Didn't find ID for option %X in %s's vote to %s" , opt , info . Sender , pollMessage . MXID )
}
}
} else {
for i , opt := range vote . GetSelectedOptions ( ) {
selectedHashes [ i ] = hex . EncodeToString ( opt )
}
2022-11-17 22:30:42 +01:00
}
2022-12-23 14:17:57 +01:00
evtType := TypeMSC3381PollResponse
2022-11-26 17:53:08 +01:00
//if portal.bridge.Config.Bridge.ExtEvPolls == 2 {
2022-12-23 14:17:57 +01:00
// evtType = TypeMSC3381V2PollResponse
2022-11-26 17:53:08 +01:00
//}
2022-11-17 22:30:42 +01:00
return & ConvertedMessage {
Intent : intent ,
Type : evtType ,
Content : & event . MessageEventContent {
RelatesTo : & event . RelatesTo {
Type : event . RelReference ,
EventID : pollMessage . MXID ,
} ,
} ,
Extra : map [ string ] any {
"org.matrix.msc3381.poll.response" : map [ string ] any {
"answers" : selectedHashes ,
} ,
"org.matrix.msc3381.v2.selections" : selectedHashes ,
} ,
}
}
2022-11-17 22:02:01 +01:00
func ( portal * Portal ) convertPollCreationMessage ( intent * appservice . IntentAPI , msg * waProto . PollCreationMessage ) * ConvertedMessage {
optionNames := make ( [ ] string , len ( msg . GetOptions ( ) ) )
2022-11-17 22:30:42 +01:00
optionsListText := make ( [ ] string , len ( optionNames ) )
optionsListHTML := make ( [ ] string , len ( optionNames ) )
msc3381Answers := make ( [ ] map [ string ] any , len ( optionNames ) )
msc3381V2Answers := make ( [ ] map [ string ] any , len ( optionNames ) )
2022-11-17 22:02:01 +01:00
for i , opt := range msg . GetOptions ( ) {
optionNames [ i ] = opt . GetOptionName ( )
optionsListText [ i ] = fmt . Sprintf ( "%d. %s\n" , i + 1 , optionNames [ i ] )
optionsListHTML [ i ] = fmt . Sprintf ( "<li>%s</li>" , event . TextToHTML ( optionNames [ i ] ) )
2022-11-17 22:30:42 +01:00
optionHash := sha256 . Sum256 ( [ ] byte ( opt . GetOptionName ( ) ) )
optionHashStr := hex . EncodeToString ( optionHash [ : ] )
msc3381Answers [ i ] = map [ string ] any {
"id" : optionHashStr ,
"org.matrix.msc1767.text" : opt . GetOptionName ( ) ,
}
msc3381V2Answers [ i ] = map [ string ] any {
"org.matrix.msc3381.v2.id" : optionHashStr ,
"org.matrix.msc1767.markup" : [ ] map [ string ] any {
{ "mimetype" : "text/plain" , "body" : opt . GetOptionName ( ) } ,
} ,
}
2022-11-17 22:02:01 +01:00
}
body := fmt . Sprintf ( "%s\n\n%s" , msg . GetName ( ) , strings . Join ( optionsListText , "\n" ) )
formattedBody := fmt . Sprintf ( "<p>%s</p><ol>%s</ol>" , event . TextToHTML ( msg . GetName ( ) ) , strings . Join ( optionsListHTML , "" ) )
2022-11-17 22:30:42 +01:00
maxChoices := int ( msg . GetSelectableOptionsCount ( ) )
if maxChoices <= 0 {
maxChoices = len ( optionNames )
}
evtType := event . EventMessage
2022-11-26 17:53:08 +01:00
if portal . bridge . Config . Bridge . ExtEvPolls {
2022-12-23 14:17:57 +01:00
evtType = TypeMSC3381PollStart
2022-11-17 22:30:42 +01:00
}
2022-11-26 17:53:08 +01:00
//else if portal.bridge.Config.Bridge.ExtEvPolls == 2 {
// evtType.Type = "org.matrix.msc3381.v2.poll.start"
//}
2022-11-17 22:02:01 +01:00
return & ConvertedMessage {
Intent : intent ,
2022-11-17 22:30:42 +01:00
Type : evtType ,
2022-11-17 22:02:01 +01:00
Content : & event . MessageEventContent {
Body : body ,
MsgType : event . MsgText ,
Format : event . FormatHTML ,
FormattedBody : formattedBody ,
} ,
Extra : map [ string ] any {
2022-11-17 22:30:42 +01:00
// Custom metadata
2022-11-17 22:02:01 +01:00
"fi.mau.whatsapp.poll" : map [ string ] any {
2022-11-17 22:30:42 +01:00
"option_names" : optionNames ,
"selectable_options_count" : msg . GetSelectableOptionsCount ( ) ,
} ,
// Current extensible events (as of November 2022)
"org.matrix.msc1767.markup" : [ ] map [ string ] any {
{ "mimetype" : "text/html" , "body" : formattedBody } ,
{ "mimetype" : "text/plain" , "body" : body } ,
} ,
"org.matrix.msc3381.v2.poll" : map [ string ] any {
"kind" : "org.matrix.msc3381.v2.disclosed" ,
"max_selections" : maxChoices ,
"question" : map [ string ] any {
2022-11-17 23:28:04 +01:00
"org.matrix.msc1767.markup" : [ ] map [ string ] any {
2022-11-17 22:30:42 +01:00
{ "mimetype" : "text/plain" , "body" : msg . GetName ( ) } ,
} ,
} ,
"answers" : msc3381V2Answers ,
} ,
// Legacy extensible events
"org.matrix.msc1767.message" : [ ] map [ string ] any {
{ "mimetype" : "text/html" , "body" : formattedBody } ,
{ "mimetype" : "text/plain" , "body" : body } ,
} ,
"org.matrix.msc3381.poll.start" : map [ string ] any {
"kind" : "org.matrix.msc3381.poll.disclosed" ,
"max_selections" : maxChoices ,
"question" : map [ string ] any {
2022-11-17 23:28:04 +01:00
"org.matrix.msc1767.text" : msg . GetName ( ) ,
2022-11-17 22:30:42 +01:00
} ,
"answers" : msc3381Answers ,
2022-11-17 22:02:01 +01:00
} ,
} ,
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2022-11-17 22:02:01 +01:00
}
}
2021-12-07 13:51:56 +01:00
func ( portal * Portal ) convertLiveLocationMessage ( intent * appservice . IntentAPI , msg * waProto . LiveLocationMessage ) * ConvertedMessage {
content := & event . MessageEventContent {
Body : "Started sharing live location" ,
MsgType : event . MsgNotice ,
}
if len ( msg . GetCaption ( ) ) > 0 {
content . Body += ": " + msg . GetCaption ( )
}
return & ConvertedMessage {
2022-01-07 13:32:00 +01:00
Intent : intent ,
Type : event . EventMessage ,
Content : content ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2021-12-07 13:51:56 +01:00
}
}
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) convertLocationMessage ( intent * appservice . IntentAPI , msg * waProto . LocationMessage ) * ConvertedMessage {
2021-10-22 19:14:34 +02:00
url := msg . GetUrl ( )
2020-06-10 13:58:57 +02:00
if len ( url ) == 0 {
2021-10-22 19:14:34 +02:00
url = fmt . Sprintf ( "https://maps.google.com/?q=%.5f,%.5f" , msg . GetDegreesLatitude ( ) , msg . GetDegreesLongitude ( ) )
2020-06-10 13:58:57 +02:00
}
2021-10-22 19:14:34 +02:00
name := msg . GetName ( )
2020-06-10 13:58:57 +02:00
if len ( name ) == 0 {
latChar := 'N'
2021-10-22 19:14:34 +02:00
if msg . GetDegreesLatitude ( ) < 0 {
2020-06-10 13:58:57 +02:00
latChar = 'S'
}
longChar := 'E'
2021-10-22 19:14:34 +02:00
if msg . GetDegreesLongitude ( ) < 0 {
2020-06-10 13:58:57 +02:00
longChar = 'W'
}
2021-10-22 19:14:34 +02:00
name = fmt . Sprintf ( "%.4f° %c %.4f° %c" , math . Abs ( msg . GetDegreesLatitude ( ) ) , latChar , math . Abs ( msg . GetDegreesLongitude ( ) ) , longChar )
2020-06-10 13:58:57 +02:00
}
content := & event . MessageEventContent {
MsgType : event . MsgLocation ,
2021-10-22 19:14:34 +02:00
Body : fmt . Sprintf ( "Location: %s\n%s\n%s" , name , msg . GetAddress ( ) , url ) ,
2020-06-10 13:58:57 +02:00
Format : event . FormatHTML ,
2021-10-22 19:14:34 +02:00
FormattedBody : fmt . Sprintf ( "Location: <a href='%s'>%s</a><br>%s" , url , name , msg . GetAddress ( ) ) ,
GeoURI : fmt . Sprintf ( "geo:%.5f,%.5f" , msg . GetDegreesLatitude ( ) , msg . GetDegreesLongitude ( ) ) ,
2020-06-10 13:58:57 +02:00
}
2021-10-22 19:14:34 +02:00
if len ( msg . GetJpegThumbnail ( ) ) > 0 {
thumbnailMime := http . DetectContentType ( msg . GetJpegThumbnail ( ) )
uploadedThumbnail , _ := intent . UploadBytes ( msg . GetJpegThumbnail ( ) , thumbnailMime )
2020-06-10 13:58:57 +02:00
if uploadedThumbnail != nil {
2021-10-22 19:14:34 +02:00
cfg , _ , _ := image . DecodeConfig ( bytes . NewReader ( msg . GetJpegThumbnail ( ) ) )
2020-06-10 13:58:57 +02:00
content . Info = & event . FileInfo {
ThumbnailInfo : & event . FileInfo {
2021-10-22 19:14:34 +02:00
Size : len ( msg . GetJpegThumbnail ( ) ) ,
2020-06-10 13:58:57 +02:00
Width : cfg . Width ,
Height : cfg . Height ,
MimeType : thumbnailMime ,
} ,
ThumbnailURL : uploadedThumbnail . ContentURI . CUString ( ) ,
}
}
}
2021-11-05 10:47:51 +01:00
return & ConvertedMessage {
2022-01-07 13:32:00 +01:00
Intent : intent ,
Type : event . EventMessage ,
Content : content ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2021-11-05 10:47:51 +01:00
}
2020-06-10 13:58:57 +02:00
}
2020-06-10 13:06:36 +02:00
2021-10-31 19:47:30 +01:00
const inviteMsg = ` %s<hr/>This invitation to join "%s" expires at %s. Reply to this message with <code>!wa accept</code> to accept the invite. `
2021-10-31 19:42:53 +01:00
const inviteMetaField = "fi.mau.whatsapp.invite"
2022-03-14 12:15:52 +01:00
const escapedInviteMetaField = ` fi\.mau\.whatsapp\.invite `
type InviteMeta struct {
JID types . JID ` json:"jid" `
Code string ` json:"code" `
Expiration int64 ` json:"expiration,string" `
Inviter types . JID ` json:"inviter" `
}
2021-10-31 19:42:53 +01:00
func ( portal * Portal ) convertGroupInviteMessage ( intent * appservice . IntentAPI , info * types . MessageInfo , msg * waProto . GroupInviteMessage ) * ConvertedMessage {
expiry := time . Unix ( msg . GetInviteExpiration ( ) , 0 )
2022-11-13 15:06:56 +01:00
htmlMessage := fmt . Sprintf ( inviteMsg , event . TextToHTML ( msg . GetCaption ( ) ) , msg . GetGroupName ( ) , expiry )
2021-10-31 19:42:53 +01:00
content := & event . MessageEventContent {
MsgType : event . MsgText ,
Body : format . HTMLToText ( htmlMessage ) ,
Format : event . FormatHTML ,
FormattedBody : htmlMessage ,
}
2022-03-14 12:15:52 +01:00
groupJID , err := types . ParseJID ( msg . GetGroupJid ( ) )
if err != nil {
portal . log . Errorfln ( "Failed to parse invite group JID: %v" , err )
}
2021-10-31 19:42:53 +01:00
extraAttrs := map [ string ] interface { } {
2022-03-14 12:15:52 +01:00
inviteMetaField : InviteMeta {
JID : groupJID ,
Code : msg . GetInviteCode ( ) ,
Expiration : msg . GetInviteExpiration ( ) ,
Inviter : info . Sender . ToNonAD ( ) ,
2021-10-31 19:42:53 +01:00
} ,
}
2021-11-05 10:47:51 +01:00
return & ConvertedMessage {
2022-01-07 13:32:00 +01:00
Intent : intent ,
Type : event . EventMessage ,
Content : content ,
Extra : extraAttrs ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2021-11-05 10:47:51 +01:00
}
2021-10-31 19:42:53 +01:00
}
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) convertContactMessage ( intent * appservice . IntentAPI , msg * waProto . ContactMessage ) * ConvertedMessage {
2021-10-22 19:14:34 +02:00
fileName := fmt . Sprintf ( "%s.vcf" , msg . GetDisplayName ( ) )
data := [ ] byte ( msg . GetVcard ( ) )
2020-06-10 14:26:14 +02:00
mimeType := "text/vcard"
2022-04-27 13:31:57 +02:00
uploadMimeType , file := portal . encryptFileInPlace ( data , mimeType )
2020-06-10 13:06:36 +02:00
2020-06-10 14:26:14 +02:00
uploadResp , err := intent . UploadBytesWithName ( data , uploadMimeType , fileName )
2020-06-10 13:06:36 +02:00
if err != nil {
2021-10-22 19:14:34 +02:00
portal . log . Errorfln ( "Failed to upload vcard of %s: %v" , msg . GetDisplayName ( ) , err )
2021-10-26 16:01:10 +02:00
return nil
2020-06-10 13:06:36 +02:00
}
content := & event . MessageEventContent {
Body : fileName ,
MsgType : event . MsgFile ,
2020-06-10 14:26:14 +02:00
File : file ,
2020-06-10 13:06:36 +02:00
Info : & event . FileInfo {
2020-06-10 14:26:14 +02:00
MimeType : mimeType ,
2021-10-22 19:14:34 +02:00
Size : len ( msg . GetVcard ( ) ) ,
2020-06-10 13:06:36 +02:00
} ,
}
2020-06-10 14:26:14 +02:00
if content . File != nil {
content . File . URL = uploadResp . ContentURI . CUString ( )
} else {
content . URL = uploadResp . ContentURI . CUString ( )
}
2020-06-10 13:06:36 +02:00
2021-11-05 10:47:51 +01:00
return & ConvertedMessage {
2022-01-07 13:32:00 +01:00
Intent : intent ,
Type : event . EventMessage ,
Content : content ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2021-11-05 10:47:51 +01:00
}
2020-06-20 21:24:27 +02:00
}
2020-06-10 14:26:14 +02:00
2022-01-03 15:11:39 +01:00
func ( portal * Portal ) convertContactsArrayMessage ( intent * appservice . IntentAPI , msg * waProto . ContactsArrayMessage ) * ConvertedMessage {
name := msg . GetDisplayName ( )
if len ( name ) == 0 {
name = fmt . Sprintf ( "%d contacts" , len ( msg . GetContacts ( ) ) )
}
contacts := make ( [ ] * event . MessageEventContent , 0 , len ( msg . GetContacts ( ) ) )
for _ , contact := range msg . GetContacts ( ) {
converted := portal . convertContactMessage ( intent , contact )
if converted != nil {
contacts = append ( contacts , converted . Content )
}
}
return & ConvertedMessage {
Intent : intent ,
Type : event . EventMessage ,
Content : & event . MessageEventContent {
MsgType : event . MsgNotice ,
Body : fmt . Sprintf ( "Sent %s" , name ) ,
} ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2022-01-03 15:11:39 +01:00
MultiEvent : contacts ,
}
}
2021-10-28 11:59:22 +02:00
func ( portal * Portal ) tryKickUser ( userID id . UserID , intent * appservice . IntentAPI ) error {
_ , err := intent . KickUser ( portal . MXID , & mautrix . ReqKickUser { UserID : userID } )
if err != nil {
httpErr , ok := err . ( mautrix . HTTPError )
if ok && httpErr . RespError != nil && httpErr . RespError . ErrCode == "M_FORBIDDEN" {
_ , err = portal . MainIntent ( ) . KickUser ( portal . MXID , & mautrix . ReqKickUser { UserID : userID } )
}
}
return err
}
func ( portal * Portal ) removeUser ( isSameUser bool , kicker * appservice . IntentAPI , target id . UserID , targetIntent * appservice . IntentAPI ) {
if ! isSameUser || targetIntent == nil {
err := portal . tryKickUser ( target , kicker )
if err != nil {
portal . log . Warnfln ( "Failed to kick %s from %s: %v" , target , portal . MXID , err )
if targetIntent != nil {
2022-06-30 19:56:25 +02:00
_ , _ = targetIntent . LeaveRoom ( portal . MXID )
2021-10-28 11:59:22 +02:00
}
}
} else {
2022-06-30 19:56:25 +02:00
_ , err := targetIntent . LeaveRoom ( portal . MXID )
2021-10-28 11:59:22 +02:00
if err != nil {
portal . log . Warnfln ( "Failed to leave portal as %s: %v" , target , err )
_ , _ = portal . MainIntent ( ) . KickUser ( portal . MXID , & mautrix . ReqKickUser { UserID : target } )
}
}
2022-07-18 15:16:17 +02:00
portal . CleanupIfEmpty ( )
2021-10-28 11:59:22 +02:00
}
func ( portal * Portal ) HandleWhatsAppKick ( source * User , senderJID types . JID , jids [ ] types . JID ) {
sender := portal . bridge . GetPuppetByJID ( senderJID )
senderIntent := sender . IntentFor ( portal )
for _ , jid := range jids {
2021-11-05 11:17:56 +01:00
//if source != nil && source.JID.User == jid.User {
// portal.log.Debugln("Ignoring self-kick by", source.MXID)
// continue
//}
2021-10-28 11:59:22 +02:00
puppet := portal . bridge . GetPuppetByJID ( jid )
portal . removeUser ( puppet . JID == sender . JID , senderIntent , puppet . MXID , puppet . DefaultIntent ( ) )
if ! portal . IsBroadcastList ( ) {
user := portal . bridge . GetUserByJID ( jid )
if user != nil {
var customIntent * appservice . IntentAPI
if puppet . CustomMXID == user . MXID {
customIntent = puppet . CustomIntent ( )
}
portal . removeUser ( puppet . JID == sender . JID , senderIntent , user . MXID , customIntent )
}
}
}
}
func ( portal * Portal ) HandleWhatsAppInvite ( source * User , senderJID * types . JID , jids [ ] types . JID ) ( evtID id . EventID ) {
intent := portal . MainIntent ( )
if senderJID != nil && ! senderJID . IsEmpty ( ) {
sender := portal . bridge . GetPuppetByJID ( * senderJID )
intent = sender . IntentFor ( portal )
}
for _ , jid := range jids {
puppet := portal . bridge . GetPuppetByJID ( jid )
2022-05-13 10:34:45 +02:00
puppet . SyncContact ( source , true , false , "handling whatsapp invite" )
2022-06-30 19:56:25 +02:00
resp , err := intent . SendStateEvent ( portal . MXID , event . StateMember , puppet . MXID . String ( ) , & event . MemberEventContent {
Membership : event . MembershipInvite ,
Displayname : puppet . Displayname ,
AvatarURL : puppet . AvatarURL . CUString ( ) ,
} )
2021-10-28 11:59:22 +02:00
if err != nil {
portal . log . Warnfln ( "Failed to invite %s as %s: %v" , puppet . MXID , intent . UserID , err )
_ = portal . MainIntent ( ) . EnsureInvited ( portal . MXID , puppet . MXID )
} else {
evtID = resp . EventID
}
err = puppet . DefaultIntent ( ) . EnsureJoined ( portal . MXID )
if err != nil {
portal . log . Errorfln ( "Failed to ensure %s is joined: %v" , puppet . MXID , err )
}
}
return
}
2021-10-22 19:14:34 +02:00
2022-07-18 15:16:17 +02:00
func ( portal * Portal ) HandleWhatsAppDeleteChat ( user * User ) {
2022-12-02 15:19:37 +01:00
if portal . MXID == "" {
return
}
2022-07-18 15:16:17 +02:00
matrixUsers , err := portal . GetMatrixUsers ( )
if err != nil {
portal . log . Errorln ( "Failed to get Matrix users to see if DeleteChat should be handled:" , err )
return
}
if len ( matrixUsers ) > 1 {
portal . log . Infoln ( "Portal contains more than one Matrix user, so deleteChat will not be handled." )
return
} else if ( len ( matrixUsers ) == 1 && matrixUsers [ 0 ] == user . MXID ) || len ( matrixUsers ) < 1 {
portal . log . Debugln ( "User deleted chat and there are no other Matrix users using it, deleting portal..." )
portal . Delete ( )
portal . Cleanup ( false )
}
}
2022-02-10 18:18:49 +01:00
const failedMediaField = "fi.mau.whatsapp.failed_media"
type FailedMediaKeys struct {
Key [ ] byte ` json:"key" `
Length int ` json:"length" `
Type whatsmeow . MediaType ` json:"type" `
SHA256 [ ] byte ` json:"sha256" `
EncSHA256 [ ] byte ` json:"enc_sha256" `
}
type FailedMediaMeta struct {
Type event . Type ` json:"type" `
Content * event . MessageEventContent ` json:"content" `
ExtraContent map [ string ] interface { } ` json:"extra_content,omitempty" `
Media FailedMediaKeys ` json:"whatsapp_media" `
}
func shallowCopyMap ( data map [ string ] interface { } ) map [ string ] interface { } {
newMap := make ( map [ string ] interface { } , len ( data ) )
for key , value := range data {
newMap [ key ] = value
}
return newMap
}
2022-04-05 23:35:02 +02:00
func ( portal * Portal ) makeMediaBridgeFailureMessage ( info * types . MessageInfo , bridgeErr error , converted * ConvertedMessage , keys * FailedMediaKeys , userFriendlyError string ) * ConvertedMessage {
2023-04-10 11:18:30 +02:00
if errors . Is ( bridgeErr , whatsmeow . ErrMediaDownloadFailedWith403 ) || errors . Is ( bridgeErr , whatsmeow . ErrMediaDownloadFailedWith404 ) || errors . Is ( bridgeErr , whatsmeow . ErrMediaDownloadFailedWith410 ) {
2022-05-14 13:28:55 +02:00
portal . log . Debugfln ( "Failed to bridge media for %s: %v" , info . ID , bridgeErr )
} else {
portal . log . Errorfln ( "Failed to bridge media for %s: %v" , info . ID , bridgeErr )
}
2022-02-10 18:18:49 +01:00
if keys != nil {
2022-06-17 10:34:24 +02:00
if portal . bridge . Config . Bridge . CaptionInMessage {
converted . MergeCaption ( )
}
2022-02-10 18:18:49 +01:00
meta := & FailedMediaMeta {
Type : converted . Type ,
Content : converted . Content ,
ExtraContent : shallowCopyMap ( converted . Extra ) ,
Media : * keys ,
}
converted . Extra [ failedMediaField ] = meta
portal . mediaErrorCache [ info . ID ] = meta
}
converted . Type = event . EventMessage
2022-04-05 23:35:02 +02:00
body := userFriendlyError
if body == "" {
body = fmt . Sprintf ( "Failed to bridge media: %v" , bridgeErr )
}
2022-02-10 18:18:49 +01:00
converted . Content = & event . MessageEventContent {
2021-10-26 16:01:10 +02:00
MsgType : event . MsgNotice ,
2022-04-05 23:35:02 +02:00
Body : body ,
2022-02-10 18:18:49 +01:00
}
return converted
2021-10-26 16:01:10 +02:00
}
2022-04-27 13:31:57 +02:00
func ( portal * Portal ) encryptFileInPlace ( data [ ] byte , mimeType string ) ( string , * event . EncryptedFileInfo ) {
2021-10-26 16:01:10 +02:00
if ! portal . Encrypted {
2022-04-27 13:31:57 +02:00
return mimeType , nil
2021-10-26 16:01:10 +02:00
}
file := & event . EncryptedFileInfo {
EncryptedFile : * attachment . NewEncryptedFile ( ) ,
URL : "" ,
}
2022-04-27 18:04:34 +02:00
file . EncryptInPlace ( data )
2022-04-27 13:31:57 +02:00
return "application/octet-stream" , file
2021-10-26 16:01:10 +02:00
}
2021-10-22 19:14:34 +02:00
type MediaMessage interface {
whatsmeow . DownloadableMessage
GetContextInfo ( ) * waProto . ContextInfo
2022-02-10 18:18:49 +01:00
GetFileLength ( ) uint64
2021-10-22 19:14:34 +02:00
GetMimetype ( ) string
}
2021-10-26 16:01:10 +02:00
type MediaMessageWithThumbnail interface {
2021-10-22 19:14:34 +02:00
MediaMessage
GetJpegThumbnail ( ) [ ] byte
}
2021-10-26 16:01:10 +02:00
type MediaMessageWithCaption interface {
MediaMessage
GetCaption ( ) string
}
2021-12-09 18:20:52 +01:00
type MediaMessageWithDimensions interface {
MediaMessage
GetHeight ( ) uint32
GetWidth ( ) uint32
}
2021-10-22 19:14:34 +02:00
type MediaMessageWithFileName interface {
MediaMessage
GetFileName ( ) string
}
type MediaMessageWithDuration interface {
MediaMessage
GetSeconds ( ) uint32
}
2022-07-05 11:52:32 +02:00
const WhatsAppStickerSize = 190
2022-02-10 18:18:49 +01:00
func ( portal * Portal ) convertMediaMessageContent ( intent * appservice . IntentAPI , msg MediaMessage ) * ConvertedMessage {
2021-10-22 19:14:34 +02:00
content := & event . MessageEventContent {
Info : & event . FileInfo {
MimeType : msg . GetMimetype ( ) ,
2022-02-10 18:18:49 +01:00
Size : int ( msg . GetFileLength ( ) ) ,
2021-10-22 19:14:34 +02:00
} ,
}
2022-02-10 18:26:16 +01:00
extraContent := map [ string ] interface { } { }
2021-10-22 19:14:34 +02:00
2022-02-10 18:18:49 +01:00
messageWithDimensions , ok := msg . ( MediaMessageWithDimensions )
if ok {
content . Info . Width = int ( messageWithDimensions . GetWidth ( ) )
content . Info . Height = int ( messageWithDimensions . GetHeight ( ) )
}
2021-10-22 19:14:34 +02:00
msgWithName , ok := msg . ( MediaMessageWithFileName )
if ok && len ( msgWithName . GetFileName ( ) ) > 0 {
content . Body = msgWithName . GetFileName ( )
} else {
mimeClass := strings . Split ( msg . GetMimetype ( ) , "/" ) [ 0 ]
2020-11-02 16:18:18 +01:00
switch mimeClass {
case "application" :
2021-10-22 19:14:34 +02:00
content . Body = "file"
2020-11-02 16:18:18 +01:00
default :
2021-10-22 19:14:34 +02:00
content . Body = mimeClass
2020-11-02 16:18:18 +01:00
}
2020-06-20 17:26:45 +02:00
2022-01-04 01:02:06 +01:00
content . Body += util . ExtensionFromMimetype ( msg . GetMimetype ( ) )
2018-08-23 23:52:06 +02:00
}
2021-10-22 19:14:34 +02:00
msgWithDuration , ok := msg . ( MediaMessageWithDuration )
if ok {
content . Info . Duration = int ( msgWithDuration . GetSeconds ( ) ) * 1000
2018-08-23 23:52:06 +02:00
}
2021-10-22 19:14:34 +02:00
2022-02-10 18:26:16 +01:00
videoMessage , ok := msg . ( * waProto . VideoMessage )
var isGIF bool
if ok && videoMessage . GetGifPlayback ( ) {
isGIF = true
extraContent [ "info" ] = map [ string ] interface { } {
"fi.mau.loop" : true ,
"fi.mau.autoplay" : true ,
"fi.mau.hide_controls" : true ,
"fi.mau.no_audio" : true ,
2023-03-28 11:26:30 +02:00
"fi.mau.gif" : true ,
2022-02-10 18:26:16 +01:00
}
}
2021-10-26 16:01:10 +02:00
messageWithThumbnail , ok := msg . ( MediaMessageWithThumbnail )
2022-02-10 18:26:16 +01:00
if ok && messageWithThumbnail . GetJpegThumbnail ( ) != nil && ( portal . bridge . Config . Bridge . WhatsappThumbnail || isGIF ) {
2021-10-26 16:01:10 +02:00
thumbnailData := messageWithThumbnail . GetJpegThumbnail ( )
2021-10-22 19:14:34 +02:00
thumbnailMime := http . DetectContentType ( thumbnailData )
thumbnailCfg , _ , _ := image . DecodeConfig ( bytes . NewReader ( thumbnailData ) )
thumbnailSize := len ( thumbnailData )
2022-04-27 13:31:57 +02:00
thumbnailUploadMime , thumbnailFile := portal . encryptFileInPlace ( thumbnailData , thumbnailMime )
uploadedThumbnail , err := intent . UploadBytes ( thumbnailData , thumbnailUploadMime )
2020-06-10 14:26:14 +02:00
if err != nil {
2022-02-10 18:18:49 +01:00
portal . log . Warnfln ( "Failed to upload thumbnail: %v" , err )
2020-06-10 14:26:14 +02:00
} else if uploadedThumbnail != nil {
if thumbnailFile != nil {
thumbnailFile . URL = uploadedThumbnail . ContentURI . CUString ( )
content . Info . ThumbnailFile = thumbnailFile
} else {
content . Info . ThumbnailURL = uploadedThumbnail . ContentURI . CUString ( )
}
2020-05-08 21:32:22 +02:00
content . Info . ThumbnailInfo = & event . FileInfo {
2020-06-10 14:26:14 +02:00
Size : thumbnailSize ,
Width : thumbnailCfg . Width ,
Height : thumbnailCfg . Height ,
2018-08-23 23:52:06 +02:00
MimeType : thumbnailMime ,
}
}
}
2022-07-05 11:46:34 +02:00
eventType := event . EventMessage
switch msg . ( type ) {
case * waProto . ImageMessage :
content . MsgType = event . MsgImage
case * waProto . StickerMessage :
eventType = event . EventSticker
2022-07-05 11:52:32 +02:00
if content . Info . Width > content . Info . Height {
content . Info . Height /= content . Info . Width / WhatsAppStickerSize
content . Info . Width = WhatsAppStickerSize
} else if content . Info . Width < content . Info . Height {
content . Info . Width /= content . Info . Height / WhatsAppStickerSize
content . Info . Height = WhatsAppStickerSize
} else {
content . Info . Width = WhatsAppStickerSize
content . Info . Height = WhatsAppStickerSize
}
2022-07-05 11:46:34 +02:00
case * waProto . VideoMessage :
2020-05-08 21:32:22 +02:00
content . MsgType = event . MsgVideo
2022-07-05 11:46:34 +02:00
case * waProto . AudioMessage :
2020-05-08 21:32:22 +02:00
content . MsgType = event . MsgAudio
2022-07-05 11:46:34 +02:00
case * waProto . DocumentMessage :
content . MsgType = event . MsgFile
2018-08-23 23:52:06 +02:00
default :
2022-07-05 11:46:34 +02:00
portal . log . Warnfln ( "Unexpected media type %T in convertMediaMessageContent" , msg )
2020-05-08 21:32:22 +02:00
content . MsgType = event . MsgFile
2018-08-23 23:52:06 +02:00
}
2022-01-04 01:02:06 +01:00
audioMessage , ok := msg . ( * waProto . AudioMessage )
if ok {
2022-02-10 11:50:04 +01:00
var waveform [ ] int
if audioMessage . Waveform != nil {
waveform = make ( [ ] int , len ( audioMessage . Waveform ) )
2022-02-21 13:34:20 +01:00
max := 0
2022-02-10 11:50:04 +01:00
for i , part := range audioMessage . Waveform {
2022-02-21 13:34:20 +01:00
waveform [ i ] = int ( part )
if waveform [ i ] > max {
max = waveform [ i ]
}
}
2022-02-28 20:41:44 +01:00
multiplier := 0
if max > 0 {
multiplier = 1024 / max
}
2022-02-21 13:34:20 +01:00
if multiplier > 32 {
multiplier = 32
}
for i := range waveform {
waveform [ i ] *= multiplier
2022-02-10 11:50:04 +01:00
}
}
2022-01-04 01:02:06 +01:00
extraContent [ "org.matrix.msc1767.audio" ] = map [ string ] interface { } {
"duration" : int ( audioMessage . GetSeconds ( ) ) * 1000 ,
2022-02-10 11:50:04 +01:00
"waveform" : waveform ,
2022-01-04 01:02:06 +01:00
}
if audioMessage . GetPtt ( ) {
extraContent [ "org.matrix.msc3245.voice" ] = map [ string ] interface { } { }
}
}
2022-02-10 18:18:49 +01:00
messageWithCaption , ok := msg . ( MediaMessageWithCaption )
var captionContent * event . MessageEventContent
if ok && len ( messageWithCaption . GetCaption ( ) ) > 0 {
captionContent = & event . MessageEventContent {
Body : messageWithCaption . GetCaption ( ) ,
MsgType : event . MsgNotice ,
}
2022-06-24 22:25:37 +02:00
portal . bridge . Formatter . ParseWhatsApp ( portal . MXID , captionContent , msg . GetContextInfo ( ) . GetMentionedJid ( ) , false , false )
2022-02-10 18:18:49 +01:00
}
2021-10-26 16:01:10 +02:00
return & ConvertedMessage {
2022-01-07 13:32:00 +01:00
Intent : intent ,
Type : eventType ,
Content : content ,
Caption : captionContent ,
2022-08-25 16:05:40 +02:00
ReplyTo : GetReply ( msg . GetContextInfo ( ) ) ,
2023-01-16 16:15:04 +01:00
ExpiresIn : time . Duration ( msg . GetContextInfo ( ) . GetExpiration ( ) ) * time . Second ,
2022-01-07 13:32:00 +01:00
Extra : extraContent ,
2018-08-27 22:15:05 +02:00
}
2018-08-19 17:21:38 +02:00
}
2022-02-10 18:18:49 +01:00
func ( portal * Portal ) uploadMedia ( intent * appservice . IntentAPI , data [ ] byte , content * event . MessageEventContent ) error {
2022-04-27 13:31:57 +02:00
uploadMimeType , file := portal . encryptFileInPlace ( data , content . Info . MimeType )
2022-02-10 18:18:49 +01:00
2022-03-21 20:08:48 +01:00
req := mautrix . ReqUploadMedia {
ContentBytes : data ,
ContentType : uploadMimeType ,
}
var mxc id . ContentURI
if portal . bridge . Config . Homeserver . AsyncMedia {
uploaded , err := intent . UnstableUploadAsync ( req )
if err != nil {
return err
}
mxc = uploaded . ContentURI
} else {
uploaded , err := intent . UploadMedia ( req )
if err != nil {
return err
}
mxc = uploaded . ContentURI
2022-02-10 18:18:49 +01:00
}
if file != nil {
2022-03-21 20:08:48 +01:00
file . URL = mxc . CUString ( )
2022-02-10 18:18:49 +01:00
content . File = file
} else {
2022-03-21 20:08:48 +01:00
content . URL = mxc . CUString ( )
2022-02-10 18:18:49 +01:00
}
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
}
2023-02-05 00:01:41 +01:00
// 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
}
}
2022-02-10 18:18:49 +01:00
return nil
}
2022-05-02 14:00:57 +02:00
func ( portal * Portal ) convertMediaMessage ( intent * appservice . IntentAPI , source * User , info * types . MessageInfo , msg MediaMessage , typeName string , isBackfill bool ) * ConvertedMessage {
2022-02-10 18:18:49 +01:00
converted := portal . convertMediaMessageContent ( intent , msg )
2022-10-03 09:09:07 +02:00
if msg . GetFileLength ( ) > uint64 ( portal . bridge . MediaConfig . UploadSize ) {
return portal . makeMediaBridgeFailureMessage ( info , errors . New ( "file is too large" ) , converted , nil , fmt . Sprintf ( "Large %s not bridged - please use WhatsApp app to view" , typeName ) )
}
2022-02-10 18:18:49 +01:00
data , err := source . Client . Download ( msg )
2023-04-10 11:18:30 +02:00
if errors . Is ( err , whatsmeow . ErrMediaDownloadFailedWith403 ) || errors . Is ( err , whatsmeow . ErrMediaDownloadFailedWith404 ) || errors . Is ( err , whatsmeow . ErrMediaDownloadFailedWith410 ) {
2022-02-10 18:18:49 +01:00
converted . Error = database . MsgErrMediaNotFound
2022-05-02 14:00:57 +02:00
converted . MediaKey = msg . GetMediaKey ( )
2022-04-21 00:59:59 +02:00
2022-05-02 14:00:57 +02:00
errorText := fmt . Sprintf ( "Old %s." , typeName )
2022-05-10 20:18:01 +02:00
if portal . bridge . Config . Bridge . HistorySync . MediaRequests . AutoRequestMedia && isBackfill {
2022-05-02 14:00:57 +02:00
errorText += " Media will be automatically requested from your phone later."
2022-04-21 00:59:59 +02:00
} else {
2022-08-22 14:00:01 +02:00
errorText += " React with the \u267b (recycle) emoji to request this media from your phone."
2022-04-21 00:59:59 +02:00
}
2022-02-10 18:18:49 +01:00
return portal . makeMediaBridgeFailureMessage ( info , err , converted , & FailedMediaKeys {
Key : msg . GetMediaKey ( ) ,
Length : int ( msg . GetFileLength ( ) ) ,
Type : whatsmeow . GetMediaType ( msg ) ,
SHA256 : msg . GetFileSha256 ( ) ,
EncSHA256 : msg . GetFileEncSha256 ( ) ,
2022-04-21 00:59:59 +02:00
} , errorText )
2022-02-10 18:18:49 +01:00
} else if errors . Is ( err , whatsmeow . ErrNoURLPresent ) {
portal . log . Debugfln ( "No URL present error for media message %s, ignoring..." , info . ID )
return nil
} else if errors . Is ( err , whatsmeow . ErrFileLengthMismatch ) || errors . Is ( err , whatsmeow . ErrInvalidMediaSHA256 ) {
portal . log . Warnfln ( "Mismatching media checksums in %s: %v. Ignoring because WhatsApp seems to ignore them too" , info . ID , err )
} else if err != nil {
2022-04-05 23:35:02 +02:00
return portal . makeMediaBridgeFailureMessage ( info , err , converted , nil , "" )
2022-02-10 18:18:49 +01:00
}
err = portal . uploadMedia ( intent , data , converted . Content )
if err != nil {
if errors . Is ( err , mautrix . MTooLarge ) {
2022-04-05 23:35:02 +02:00
return portal . makeMediaBridgeFailureMessage ( info , errors . New ( "homeserver rejected too large file" ) , converted , nil , "" )
2022-02-10 18:18:49 +01:00
} else if httpErr , ok := err . ( mautrix . HTTPError ) ; ok && httpErr . IsStatus ( 413 ) {
2022-04-05 23:35:02 +02:00
return portal . makeMediaBridgeFailureMessage ( info , errors . New ( "proxy rejected too large file" ) , converted , nil , "" )
2022-02-10 18:18:49 +01:00
} else {
2022-04-05 23:35:02 +02:00
return portal . makeMediaBridgeFailureMessage ( info , fmt . Errorf ( "failed to upload media: %w" , err ) , converted , nil , "" )
2022-02-10 18:18:49 +01:00
}
}
return converted
}
func ( portal * Portal ) fetchMediaRetryEvent ( msg * database . Message ) ( * FailedMediaMeta , error ) {
errorMeta , ok := portal . mediaErrorCache [ msg . JID ]
if ok {
return errorMeta , nil
}
evt , err := portal . MainIntent ( ) . GetEvent ( portal . MXID , msg . MXID )
if err != nil {
return nil , fmt . Errorf ( "failed to fetch event %s: %w" , msg . MXID , err )
}
if evt . Type == event . EventEncrypted {
err = evt . Content . ParseRaw ( evt . Type )
if err != nil {
return nil , fmt . Errorf ( "failed to parse encrypted content in %s: %w" , msg . MXID , err )
}
evt , err = portal . bridge . Crypto . Decrypt ( evt )
if err != nil {
return nil , fmt . Errorf ( "failed to decrypt event %s: %w" , msg . MXID , err )
}
}
errorMetaResult := gjson . GetBytes ( evt . Content . VeryRaw , strings . ReplaceAll ( failedMediaField , "." , "\\." ) )
if ! errorMetaResult . Exists ( ) || ! errorMetaResult . IsObject ( ) {
return nil , fmt . Errorf ( "didn't find failed media metadata in %s" , msg . MXID )
}
var errorMetaBytes [ ] byte
if errorMetaResult . Index > 0 {
errorMetaBytes = evt . Content . VeryRaw [ errorMetaResult . Index : errorMetaResult . Index + len ( errorMetaResult . Raw ) ]
} else {
errorMetaBytes = [ ] byte ( errorMetaResult . Raw )
}
err = json . Unmarshal ( errorMetaBytes , & errorMeta )
if err != nil {
return nil , fmt . Errorf ( "failed to unmarshal failed media metadata in %s: %w" , msg . MXID , err )
}
return errorMeta , nil
}
2022-05-02 14:29:02 +02:00
func ( portal * Portal ) sendMediaRetryFailureEdit ( intent * appservice . IntentAPI , msg * database . Message , err error ) {
content := event . MessageEventContent {
MsgType : event . MsgNotice ,
Body : fmt . Sprintf ( "Failed to bridge media after re-requesting it from your phone: %v" , err ) ,
}
contentCopy := content
content . NewContent = & contentCopy
content . RelatesTo = & event . RelatesTo {
EventID : msg . MXID ,
Type : event . RelReplace ,
}
resp , sendErr := portal . sendMessage ( intent , event . EventMessage , & content , nil , time . Now ( ) . UnixMilli ( ) )
if sendErr != nil {
portal . log . Warnfln ( "Failed to edit %s after retry failure for %s: %v" , msg . MXID , msg . JID , sendErr )
} else {
portal . log . Debugfln ( "Successfully edited %s -> %s after retry failure for %s" , msg . MXID , resp . EventID , msg . JID )
}
}
2022-02-10 18:18:49 +01:00
func ( portal * Portal ) handleMediaRetry ( retry * events . MediaRetry , source * User ) {
msg := portal . bridge . DB . Message . GetByJID ( portal . Key , retry . MessageID )
if msg == nil {
portal . log . Warnfln ( "Dropping media retry notification for unknown message %s" , retry . MessageID )
return
} else if msg . Error != database . MsgErrMediaNotFound {
portal . log . Warnfln ( "Dropping media retry notification for non-errored message %s / %s" , retry . MessageID , msg . MXID )
return
}
meta , err := portal . fetchMediaRetryEvent ( msg )
if err != nil {
portal . log . Warnfln ( "Can't handle media retry notification for %s: %v" , retry . MessageID , err )
return
}
var puppet * Puppet
if retry . FromMe {
puppet = portal . bridge . GetPuppetByJID ( source . JID )
} else if retry . ChatID . Server == types . DefaultUserServer {
puppet = portal . bridge . GetPuppetByJID ( retry . ChatID )
} else {
puppet = portal . bridge . GetPuppetByJID ( retry . SenderID )
}
intent := puppet . IntentFor ( portal )
2022-05-02 14:29:02 +02:00
retryData , err := whatsmeow . DecryptMediaRetryNotification ( retry , meta . Media . Key )
if err != nil {
portal . log . Warnfln ( "Failed to handle media retry notification for %s: %v" , retry . MessageID , err )
portal . sendMediaRetryFailureEdit ( intent , msg , err )
return
} else if retryData . GetResult ( ) != waProto . MediaRetryNotification_SUCCESS {
2022-07-30 10:30:44 +02:00
errorName := waProto . MediaRetryNotification_ResultType_name [ int32 ( retryData . GetResult ( ) ) ]
2022-05-19 14:14:22 +02:00
if retryData . GetDirectPath ( ) == "" {
portal . log . Warnfln ( "Got error response in media retry notification for %s: %s" , retry . MessageID , errorName )
2022-05-19 14:18:15 +02:00
portal . log . Debugfln ( "Error response contents: %+v" , retryData )
2022-05-19 14:14:22 +02:00
if retryData . GetResult ( ) == waProto . MediaRetryNotification_NOT_FOUND {
portal . sendMediaRetryFailureEdit ( intent , msg , whatsmeow . ErrMediaNotAvailableOnPhone )
} else {
portal . sendMediaRetryFailureEdit ( intent , msg , fmt . Errorf ( "phone sent error response: %s" , errorName ) )
}
return
2022-05-02 15:50:13 +02:00
} else {
2022-05-19 14:14:22 +02:00
portal . log . Debugfln ( "Got error response %s in media retry notification for %s, but response also contains a new download URL - trying to download" , retry . MessageID , errorName )
2022-05-02 15:50:13 +02:00
}
2022-05-02 14:29:02 +02:00
}
2022-02-10 18:18:49 +01:00
data , err := source . Client . DownloadMediaWithPath ( retryData . GetDirectPath ( ) , meta . Media . EncSHA256 , meta . Media . SHA256 , meta . Media . Key , meta . Media . Length , meta . Media . Type , "" )
if err != nil {
portal . log . Warnfln ( "Failed to download media in %s after retry notification: %v" , retry . MessageID , err )
2022-05-02 14:29:02 +02:00
portal . sendMediaRetryFailureEdit ( intent , msg , err )
2022-02-10 18:18:49 +01:00
return
}
err = portal . uploadMedia ( intent , data , meta . Content )
if err != nil {
portal . log . Warnfln ( "Failed to re-upload media for %s after retry notification: %v" , retry . MessageID , err )
2022-05-02 14:29:02 +02:00
portal . sendMediaRetryFailureEdit ( intent , msg , fmt . Errorf ( "re-uploading media failed: %v" , err ) )
2022-02-10 18:18:49 +01:00
return
}
replaceContent := & event . MessageEventContent {
MsgType : meta . Content . MsgType ,
Body : "* " + meta . Content . Body ,
NewContent : meta . Content ,
RelatesTo : & event . RelatesTo {
EventID : msg . MXID ,
Type : event . RelReplace ,
} ,
}
2022-02-10 18:56:30 +01:00
// Move the extra content into m.new_content too
meta . ExtraContent = map [ string ] interface { } {
"m.new_content" : shallowCopyMap ( meta . ExtraContent ) ,
}
2022-02-10 18:18:49 +01:00
resp , err := portal . sendMessage ( intent , meta . Type , replaceContent , meta . ExtraContent , time . Now ( ) . UnixMilli ( ) )
if err != nil {
portal . log . Warnfln ( "Failed to edit %s after retry notification for %s: %v" , msg . MXID , retry . MessageID , err )
return
}
portal . log . Debugfln ( "Successfully edited %s -> %s after retry notification for %s" , msg . MXID , resp . EventID , retry . MessageID )
2022-05-13 01:56:40 +02:00
msg . UpdateMXID ( nil , resp . EventID , database . MsgNormal , database . MsgNoError )
2022-02-10 18:18:49 +01:00
}
2022-05-12 18:32:14 +02:00
func ( portal * Portal ) requestMediaRetry ( user * User , eventID id . EventID , mediaKey [ ] byte ) ( bool , error ) {
2022-02-10 18:18:49 +01:00
msg := portal . bridge . DB . Message . GetByMXID ( eventID )
if msg == nil {
2022-05-11 23:37:30 +02:00
err := errors . New ( fmt . Sprintf ( "%s requested a media retry for unknown event %s" , user . MXID , eventID ) )
portal . log . Debugfln ( err . Error ( ) )
return false , err
2022-02-10 18:18:49 +01:00
} else if msg . Error != database . MsgErrMediaNotFound {
2022-05-11 23:37:30 +02:00
err := errors . New ( fmt . Sprintf ( "%s requested a media retry for non-errored event %s" , user . MXID , eventID ) )
portal . log . Debugfln ( err . Error ( ) )
return false , err
2022-02-10 18:18:49 +01:00
}
2022-05-12 18:32:14 +02:00
// If the media key is not provided, grab it from the event in Matrix
if mediaKey == nil {
evt , err := portal . fetchMediaRetryEvent ( msg )
if err != nil {
portal . log . Warnfln ( "Can't send media retry request for %s: %v" , msg . JID , err )
return true , nil
}
mediaKey = evt . Media . Key
2022-02-10 18:18:49 +01:00
}
2022-05-12 18:32:14 +02:00
err := user . Client . SendMediaRetryReceipt ( & types . MessageInfo {
2022-02-10 18:18:49 +01:00
ID : msg . JID ,
MessageSource : types . MessageSource {
IsFromMe : msg . Sender . User == user . JID . User ,
IsGroup : ! portal . IsPrivateChat ( ) ,
Sender : msg . Sender ,
Chat : portal . Key . JID ,
} ,
2022-05-12 18:32:14 +02:00
} , mediaKey )
2022-02-10 18:18:49 +01:00
if err != nil {
portal . log . Warnfln ( "Failed to send media retry request for %s: %v" , msg . JID , err )
} else {
portal . log . Debugfln ( "Sent media retry request for %s" , msg . JID )
}
2022-05-11 23:37:30 +02:00
return true , err
2022-02-10 18:18:49 +01:00
}
2021-11-11 19:33:22 +01:00
const thumbnailMaxSize = 72
const thumbnailMinSize = 24
2022-08-22 14:00:01 +02:00
func createThumbnailAndGetSize ( source [ ] byte , pngThumbnail bool ) ( [ ] byte , int , int , error ) {
2021-11-11 19:33:22 +01:00
src , _ , err := image . Decode ( bytes . NewReader ( source ) )
2020-05-08 21:32:22 +02:00
if err != nil {
2022-02-04 21:19:55 +01:00
return nil , 0 , 0 , fmt . Errorf ( "failed to decode thumbnail: %w" , err )
2018-08-25 23:26:24 +02:00
}
2021-11-11 19:33:22 +01:00
imageBounds := src . Bounds ( )
width , height := imageBounds . Max . X , imageBounds . Max . Y
2018-08-25 23:26:24 +02:00
var img image . Image
2021-11-11 19:33:22 +01:00
if width <= thumbnailMaxSize && height <= thumbnailMaxSize {
// No need to resize
img = src
} else {
if width == height {
width = thumbnailMaxSize
height = thumbnailMaxSize
} else if width < height {
width /= height / thumbnailMaxSize
height = thumbnailMaxSize
} else {
height /= width / thumbnailMaxSize
width = thumbnailMaxSize
}
if width < thumbnailMinSize {
width = thumbnailMinSize
}
if height < thumbnailMinSize {
height = thumbnailMinSize
}
dst := image . NewRGBA ( image . Rect ( 0 , 0 , width , height ) )
draw . NearestNeighbor . Scale ( dst , dst . Rect , src , src . Bounds ( ) , draw . Over , nil )
img = dst
2018-08-25 23:26:24 +02:00
}
2021-11-11 19:33:22 +01:00
2018-08-25 23:26:24 +02:00
var buf bytes . Buffer
2022-08-22 14:00:01 +02:00
if pngThumbnail {
err = png . Encode ( & buf , img )
} else {
err = jpeg . Encode ( & buf , img , & jpeg . Options { Quality : jpeg . DefaultQuality } )
}
2018-08-25 23:26:24 +02:00
if err != nil {
2022-02-04 21:19:55 +01:00
return nil , width , height , fmt . Errorf ( "failed to re-encode thumbnail: %w" , err )
2021-11-11 19:33:22 +01:00
}
2022-02-04 21:19:55 +01:00
return buf . Bytes ( ) , width , height , nil
}
2022-08-22 14:00:01 +02:00
func createThumbnail ( source [ ] byte , png bool ) ( [ ] byte , error ) {
data , _ , _ , err := createThumbnailAndGetSize ( source , png )
2022-02-04 21:19:55 +01:00
return data , err
2021-11-11 19:33:22 +01:00
}
2022-08-22 14:00:01 +02:00
func ( portal * Portal ) downloadThumbnail ( ctx context . Context , original [ ] byte , thumbnailURL id . ContentURIString , eventID id . EventID , png bool ) ( [ ] byte , error ) {
2021-11-11 19:33:22 +01:00
if len ( thumbnailURL ) == 0 {
// just fall back to making thumbnail of original
} else if mxc , err := thumbnailURL . Parse ( ) ; err != nil {
portal . log . Warnfln ( "Malformed thumbnail URL in %s: %v (falling back to generating thumbnail from source)" , eventID , err )
2022-06-29 19:05:55 +02:00
} else if thumbnail , err := portal . MainIntent ( ) . DownloadBytesContext ( ctx , mxc ) ; err != nil {
2021-11-11 19:33:22 +01:00
portal . log . Warnfln ( "Failed to download thumbnail in %s: %v (falling back to generating thumbnail from source)" , eventID , err )
} else {
2022-08-22 14:00:01 +02:00
return createThumbnail ( thumbnail , png )
2018-08-25 23:26:24 +02:00
}
2022-08-22 14:00:01 +02:00
return createThumbnail ( original , png )
2018-08-25 23:26:24 +02:00
}
2021-10-06 20:11:37 +02:00
func ( portal * Portal ) convertWebPtoPNG ( webpImage [ ] byte ) ( [ ] byte , error ) {
webpDecoded , err := webp . Decode ( bytes . NewReader ( webpImage ) )
if err != nil {
return nil , fmt . Errorf ( "failed to decode webp image: %w" , err )
}
var pngBuffer bytes . Buffer
2022-08-22 14:00:01 +02:00
if err = png . Encode ( & pngBuffer , webpDecoded ) ; err != nil {
return nil , fmt . Errorf ( "failed to encode png image: %w" , err )
}
return pngBuffer . Bytes ( ) , nil
}
2022-08-24 20:06:27 +02:00
type PaddedImage struct {
image . Image
Size int
OffsetX int
OffsetY int
}
func ( img * PaddedImage ) Bounds ( ) image . Rectangle {
return image . Rect ( 0 , 0 , img . Size , img . Size )
}
func ( img * PaddedImage ) At ( x , y int ) color . Color {
return img . Image . At ( x + img . OffsetX , y + img . OffsetY )
}
2022-08-22 14:00:01 +02:00
func ( portal * Portal ) convertToWebP ( img [ ] byte ) ( [ ] byte , error ) {
2022-08-24 20:08:47 +02:00
decodedImg , _ , err := image . Decode ( bytes . NewReader ( img ) )
2022-08-22 14:00:01 +02:00
if err != nil {
2022-09-06 21:55:46 +02:00
return img , fmt . Errorf ( "failed to decode image: %w" , err )
2022-08-22 14:00:01 +02:00
}
2022-08-24 20:08:47 +02:00
bounds := decodedImg . Bounds ( )
2022-08-24 20:06:27 +02:00
width , height := bounds . Dx ( ) , bounds . Dy ( )
if width != height {
paddedImg := & PaddedImage {
2022-08-24 20:08:47 +02:00
Image : decodedImg ,
2022-08-24 20:06:27 +02:00
OffsetX : bounds . Min . Y ,
OffsetY : bounds . Min . X ,
}
if width > height {
paddedImg . Size = width
paddedImg . OffsetY -= ( paddedImg . Size - height ) / 2
} else {
paddedImg . Size = height
paddedImg . OffsetX -= ( paddedImg . Size - width ) / 2
}
2022-08-24 20:08:47 +02:00
decodedImg = paddedImg
2022-08-24 20:06:27 +02:00
}
2022-08-24 20:08:47 +02:00
var webpBuffer bytes . Buffer
if err = webp . Encode ( & webpBuffer , decodedImg , nil ) ; err != nil {
2022-09-06 21:55:46 +02:00
return img , fmt . Errorf ( "failed to encode webp image: %w" , err )
2021-10-06 20:11:37 +02:00
}
2022-08-24 20:08:47 +02:00
return webpBuffer . Bytes ( ) , nil
2021-10-06 20:11:37 +02:00
}
2022-06-29 19:05:55 +02:00
func ( portal * Portal ) preprocessMatrixMedia ( ctx context . Context , sender * User , relaybotFormatted bool , content * event . MessageEventContent , eventID id . EventID , mediaType whatsmeow . MediaType ) ( * MediaUpload , error ) {
2022-07-11 13:38:47 +02:00
fileName := content . Body
2019-11-10 20:22:11 +01:00
var caption string
2021-10-22 19:14:34 +02:00
var mentionedJIDs [ ] string
2022-07-11 13:38:47 +02:00
var hasHTMLCaption bool
2022-08-22 14:00:01 +02:00
isSticker := string ( content . MsgType ) == event . EventSticker . Type
2022-07-11 13:38:47 +02:00
if content . FileName != "" && content . Body != content . FileName {
fileName = content . FileName
caption = content . Body
hasHTMLCaption = content . Format == event . FormatHTML
}
if relaybotFormatted || hasHTMLCaption {
2023-05-24 11:41:54 +02:00
caption , mentionedJIDs = portal . bridge . Formatter . ParseMatrix ( content . FormattedBody , content . Mentions )
2018-08-24 21:31:18 +02:00
}
2019-11-10 20:22:11 +01:00
2020-05-20 15:43:55 +02:00
var file * event . EncryptedFileInfo
rawMXC := content . URL
if content . File != nil {
file = content . File
rawMXC = file . URL
}
mxc , err := rawMXC . Parse ( )
2018-08-24 21:31:18 +02:00
if err != nil {
2022-05-31 16:28:58 +02:00
return nil , err
2020-05-08 21:32:22 +02:00
}
2022-06-29 19:05:55 +02:00
data , err := portal . MainIntent ( ) . DownloadBytesContext ( ctx , mxc )
2020-05-08 21:32:22 +02:00
if err != nil {
2022-06-21 20:56:08 +02:00
return nil , util . NewDualError ( errMediaDownloadFailed , err )
2018-08-24 21:31:18 +02:00
}
2020-05-20 15:43:55 +02:00
if file != nil {
2022-04-27 18:04:34 +02:00
err = file . DecryptInPlace ( data )
2020-05-20 15:43:55 +02:00
if err != nil {
2022-06-21 20:56:08 +02:00
return nil , util . NewDualError ( errMediaDecryptFailed , err )
2020-05-20 15:43:55 +02:00
}
}
2022-08-22 14:00:01 +02:00
mimeType := content . GetInfo ( ) . MimeType
var convertErr error
// Allowed mime types from https://developers.facebook.com/docs/whatsapp/on-premises/reference/media
switch {
case isSticker :
2022-08-25 10:43:16 +02:00
if mimeType != "image/webp" || content . Info . Width != content . Info . Height {
2022-08-22 14:00:01 +02:00
data , convertErr = portal . convertToWebP ( data )
content . Info . MimeType = "image/webp"
}
case mediaType == whatsmeow . MediaVideo :
switch mimeType {
case "video/mp4" , "video/3gpp" :
// Allowed
case "image/gif" :
data , convertErr = ffmpeg . ConvertBytes ( ctx , data , ".mp4" , [ ] string { "-f" , "gif" } , [ ] string {
"-pix_fmt" , "yuv420p" , "-c:v" , "libx264" , "-movflags" , "+faststart" ,
"-filter:v" , "crop='floor(in_w/2)*2:floor(in_h/2)*2'" ,
} , mimeType )
content . Info . MimeType = "video/mp4"
case "video/webm" :
data , convertErr = ffmpeg . ConvertBytes ( ctx , data , ".mp4" , [ ] string { "-f" , "webm" } , [ ] string {
"-pix_fmt" , "yuv420p" , "-c:v" , "libx264" ,
} , mimeType )
content . Info . MimeType = "video/mp4"
default :
return nil , fmt . Errorf ( "%w %q in video message" , errMediaUnsupportedType , mimeType )
}
case mediaType == whatsmeow . MediaImage :
switch mimeType {
case "image/jpeg" , "image/png" :
// Allowed
case "image/webp" :
data , convertErr = portal . convertWebPtoPNG ( data )
content . Info . MimeType = "image/png"
default :
return nil , fmt . Errorf ( "%w %q in image message" , errMediaUnsupportedType , mimeType )
}
case mediaType == whatsmeow . MediaAudio :
switch mimeType {
case "audio/aac" , "audio/mp4" , "audio/amr" , "audio/mpeg" , "audio/ogg; codecs=opus" :
// Allowed
case "audio/ogg" :
// Hopefully it's opus already
content . Info . MimeType = "audio/ogg; codecs=opus"
default :
return nil , fmt . Errorf ( "%w %q in audio message" , errMediaUnsupportedType , mimeType )
2020-06-23 15:36:05 +02:00
}
2022-08-22 14:00:01 +02:00
case mediaType == whatsmeow . MediaDocument :
// Everything is allowed
2020-06-23 15:36:05 +02:00
}
2022-08-22 14:00:01 +02:00
if convertErr != nil {
2022-09-06 21:55:46 +02:00
if content . Info . MimeType != mimeType || data == nil {
return nil , util . NewDualError ( fmt . Errorf ( "%w (%s to %s)" , errMediaConvertFailed , mimeType , content . Info . MimeType ) , convertErr )
} else {
// If the mime type didn't change and the errored conversion function returned the original data, just log a warning and continue
portal . log . Warnfln ( "Failed to re-encode %s media: %v, continuing with original file" , mimeType , convertErr )
}
2021-10-06 20:11:37 +02:00
}
2022-06-29 19:05:55 +02:00
uploadResp , err := sender . Client . Upload ( ctx , data , mediaType )
2018-08-25 23:26:24 +02:00
if err != nil {
2022-06-21 20:56:08 +02:00
return nil , util . NewDualError ( errMediaWhatsAppUploadFailed , err )
2018-08-25 23:26:24 +02:00
}
2021-11-11 19:33:22 +01:00
// Audio doesn't have thumbnails
var thumbnail [ ] byte
if mediaType != whatsmeow . MediaAudio {
2022-08-22 14:00:01 +02:00
thumbnail , err = portal . downloadThumbnail ( ctx , data , content . GetInfo ( ) . ThumbnailURL , eventID , isSticker )
2021-11-11 19:33:22 +01:00
// Ignore format errors for non-image files, we don't care about those thumbnails
if err != nil && ( ! errors . Is ( err , image . ErrFormat ) || mediaType == whatsmeow . MediaImage ) {
2022-05-31 16:28:58 +02:00
portal . log . Warnfln ( "Failed to generate thumbnail for %s: %v" , eventID , err )
2021-11-11 19:33:22 +01:00
}
}
2018-08-25 23:26:24 +02:00
return & MediaUpload {
2021-10-22 19:14:34 +02:00
UploadResponse : uploadResp ,
2022-07-11 13:38:47 +02:00
FileName : fileName ,
2021-10-22 19:14:34 +02:00
Caption : caption ,
MentionedJIDs : mentionedJIDs ,
2021-11-11 19:33:22 +01:00
Thumbnail : thumbnail ,
2021-10-22 19:14:34 +02:00
FileLength : len ( data ) ,
2022-05-31 16:28:58 +02:00
} , nil
2018-08-25 23:26:24 +02:00
}
type MediaUpload struct {
2021-10-22 19:14:34 +02:00
whatsmeow . UploadResponse
2018-08-25 23:26:24 +02:00
Caption string
2022-07-11 13:38:47 +02:00
FileName string
2021-10-22 19:14:34 +02:00
MentionedJIDs [ ] string
2018-08-25 23:26:24 +02:00
Thumbnail [ ] byte
2021-10-22 19:14:34 +02:00
FileLength int
2018-08-25 23:26:24 +02:00
}
2023-05-02 18:59:10 +02:00
func ( portal * Portal ) addRelaybotFormat ( userID id . UserID , content * event . MessageEventContent ) bool {
member := portal . MainIntent ( ) . Member ( portal . MXID , userID )
2021-11-05 19:08:49 +01:00
if member == nil {
member = & event . MemberEventContent { }
2019-11-10 20:22:11 +01:00
}
2022-11-13 15:06:56 +01:00
content . EnsureHasHTML ( )
2023-05-02 18:59:10 +02:00
data , err := portal . bridge . Config . Bridge . Relay . FormatMessage ( content , userID , * member )
2019-11-10 20:22:11 +01:00
if err != nil {
portal . log . Errorln ( "Failed to apply relaybot format:" , err )
}
2020-05-08 21:32:22 +02:00
content . FormattedBody = data
2019-11-10 20:22:11 +01:00
return true
}
2021-06-22 19:33:30 +02:00
func addCodecToMime ( mimeType , codec string ) string {
mediaType , params , err := mime . ParseMediaType ( mimeType )
if err != nil {
return mimeType
}
if _ , ok := params [ "codecs" ] ; ! ok {
params [ "codecs" ] = codec
}
return mime . FormatMediaType ( mediaType , params )
}
2021-08-02 11:53:38 +02:00
func parseGeoURI ( uri string ) ( lat , long float64 , err error ) {
if ! strings . HasPrefix ( uri , "geo:" ) {
err = fmt . Errorf ( "uri doesn't have geo: prefix" )
return
}
// Remove geo: prefix and anything after ;
coordinates := strings . Split ( strings . TrimPrefix ( uri , "geo:" ) , ";" ) [ 0 ]
if splitCoordinates := strings . Split ( coordinates , "," ) ; len ( splitCoordinates ) != 2 {
err = fmt . Errorf ( "didn't find exactly two numbers separated by a comma" )
} else if lat , err = strconv . ParseFloat ( splitCoordinates [ 0 ] , 64 ) ; err != nil {
err = fmt . Errorf ( "latitude is not a number: %w" , err )
} else if long , err = strconv . ParseFloat ( splitCoordinates [ 1 ] , 64 ) ; err != nil {
err = fmt . Errorf ( "longitude is not a number: %w" , err )
}
return
}
2022-02-10 11:50:04 +01:00
func getUnstableWaveform ( content map [ string ] interface { } ) [ ] byte {
audioInfo , ok := content [ "org.matrix.msc1767.audio" ] . ( map [ string ] interface { } )
if ! ok {
return nil
}
waveform , ok := audioInfo [ "waveform" ] . ( [ ] interface { } )
if ! ok {
return nil
}
output := make ( [ ] byte , len ( waveform ) )
var val float64
for i , part := range waveform {
val , ok = part . ( float64 )
if ok {
output [ i ] = byte ( val / 4 )
}
}
return output
}
2022-11-17 23:20:14 +01:00
var (
2022-12-23 14:17:57 +01:00
TypeMSC3381PollStart = event . Type { Class : event . MessageEventType , Type : "org.matrix.msc3381.poll.start" }
TypeMSC3381PollResponse = event . Type { Class : event . MessageEventType , Type : "org.matrix.msc3381.poll.response" }
TypeMSC3381V2PollResponse = event . Type { Class : event . MessageEventType , Type : "org.matrix.msc3381.v2.poll.response" }
2022-11-17 23:20:14 +01:00
)
type PollResponseContent struct {
RelatesTo event . RelatesTo ` json:"m.relates_to" `
V1Response struct {
Answers [ ] string ` json:"answers" `
} ` json:"org.matrix.msc3381.poll.response" `
V2Selections [ ] string ` json:"org.matrix.msc3381.v2.selections" `
}
func ( content * PollResponseContent ) GetRelatesTo ( ) * event . RelatesTo {
return & content . RelatesTo
}
func ( content * PollResponseContent ) OptionalGetRelatesTo ( ) * event . RelatesTo {
if content . RelatesTo . Type == "" {
return nil
}
return & content . RelatesTo
}
func ( content * PollResponseContent ) SetRelatesTo ( rel * event . RelatesTo ) {
content . RelatesTo = * rel
}
2022-12-23 14:17:57 +01:00
type MSC1767Message struct {
Text string ` json:"org.matrix.msc1767.text,omitempty" `
HTML string ` json:"org.matrix.msc1767.html,omitempty" `
Message [ ] struct {
MimeType string ` json:"mimetype" `
Body string ` json:"body" `
} ` json:"org.matrix.msc1767.message,omitempty" `
}
func ( portal * Portal ) msc1767ToWhatsApp ( msg MSC1767Message , mentions bool ) ( string , [ ] string ) {
for _ , part := range msg . Message {
if part . MimeType == "text/html" && msg . HTML == "" {
msg . HTML = part . Body
} else if part . MimeType == "text/plain" && msg . Text == "" {
msg . Text = part . Body
}
}
if msg . HTML != "" {
if mentions {
2023-05-24 11:41:54 +02:00
return portal . bridge . Formatter . ParseMatrix ( msg . HTML , nil )
2022-12-23 14:17:57 +01:00
} else {
return portal . bridge . Formatter . ParseMatrixWithoutMentions ( msg . HTML ) , nil
}
}
return msg . Text , nil
}
type PollStartContent struct {
RelatesTo * event . RelatesTo ` json:"m.relates_to" `
PollStart struct {
Kind string ` json:"kind" `
MaxSelections int ` json:"max_selections" `
Question MSC1767Message ` json:"question" `
Answers [ ] struct {
ID string ` json:"id" `
MSC1767Message
} ` json:"answers" `
} ` json:"org.matrix.msc3381.poll.start" `
}
func ( content * PollStartContent ) GetRelatesTo ( ) * event . RelatesTo {
if content . RelatesTo == nil {
content . RelatesTo = & event . RelatesTo { }
}
return content . RelatesTo
}
func ( content * PollStartContent ) OptionalGetRelatesTo ( ) * event . RelatesTo {
return content . RelatesTo
}
func ( content * PollStartContent ) SetRelatesTo ( rel * event . RelatesTo ) {
content . RelatesTo = rel
}
2022-11-17 23:20:14 +01:00
func init ( ) {
2022-12-23 14:17:57 +01:00
event . TypeMap [ TypeMSC3381PollResponse ] = reflect . TypeOf ( PollResponseContent { } )
event . TypeMap [ TypeMSC3381V2PollResponse ] = reflect . TypeOf ( PollResponseContent { } )
event . TypeMap [ TypeMSC3381PollStart ] = reflect . TypeOf ( PollStartContent { } )
2022-11-17 23:20:14 +01:00
}
2022-12-23 14:17:57 +01:00
func ( portal * Portal ) convertMatrixPollVote ( _ context . Context , sender * User , evt * event . Event ) ( * waProto . Message , * User , * extraConvertMeta , error ) {
2022-11-17 23:20:14 +01:00
content , ok := evt . Content . Parsed . ( * PollResponseContent )
if ! ok {
2022-12-23 14:17:57 +01:00
return nil , sender , nil , fmt . Errorf ( "%w %T" , errUnexpectedParsedContentType , evt . Content . Parsed )
2022-11-17 23:20:14 +01:00
}
var answers [ ] string
if content . V1Response . Answers != nil {
answers = content . V1Response . Answers
} else if content . V2Selections != nil {
answers = content . V2Selections
}
pollMsg := portal . bridge . DB . Message . GetByMXID ( content . RelatesTo . EventID )
if pollMsg == nil {
2022-12-23 14:17:57 +01:00
return nil , sender , nil , errTargetNotFound
2022-11-17 23:20:14 +01:00
}
pollMsgInfo := & types . MessageInfo {
MessageSource : types . MessageSource {
Chat : portal . Key . JID ,
Sender : pollMsg . Sender ,
IsFromMe : pollMsg . Sender . User == sender . JID . User ,
IsGroup : portal . IsGroupChat ( ) ,
} ,
ID : pollMsg . JID ,
Type : "poll" ,
}
optionHashes := make ( [ ] [ ] byte , 0 , len ( answers ) )
2022-12-23 14:17:57 +01:00
if pollMsg . Type == database . MsgMatrixPoll {
mappedAnswers := pollMsg . GetPollOptionHashes ( answers )
for _ , selection := range answers {
hash , ok := mappedAnswers [ selection ]
if ok {
optionHashes = append ( optionHashes , hash [ : ] )
} else {
portal . log . Warnfln ( "Didn't find hash for option %s in %s's vote to %s" , selection , evt . Sender , pollMsg . MXID )
}
}
} else {
for _ , selection := range answers {
hash , _ := hex . DecodeString ( selection )
if hash != nil && len ( hash ) == 32 {
optionHashes = append ( optionHashes , hash )
}
2022-11-17 23:20:14 +01:00
}
}
pollUpdate , err := sender . Client . EncryptPollVote ( pollMsgInfo , & waProto . PollVoteMessage {
SelectedOptions : optionHashes ,
} )
2022-12-23 14:17:57 +01:00
return & waProto . Message { PollUpdateMessage : pollUpdate } , sender , nil , err
2022-11-17 23:20:14 +01:00
}
2022-12-23 14:17:57 +01:00
func ( portal * Portal ) convertMatrixPollStart ( _ context . Context , sender * User , evt * event . Event ) ( * waProto . Message , * User , * extraConvertMeta , error ) {
content , ok := evt . Content . Parsed . ( * PollStartContent )
2020-05-25 22:11:00 +02:00
if ! ok {
2022-12-23 14:17:57 +01:00
return nil , sender , nil , fmt . Errorf ( "%w %T" , errUnexpectedParsedContentType , evt . Content . Parsed )
}
maxAnswers := content . PollStart . MaxSelections
if maxAnswers >= len ( content . PollStart . Answers ) || maxAnswers < 0 {
maxAnswers = 0
}
fmt . Printf ( "%+v\n" , content . PollStart )
ctxInfo := portal . generateContextInfo ( content . RelatesTo )
var question string
question , ctxInfo . MentionedJid = portal . msc1767ToWhatsApp ( content . PollStart . Question , true )
if len ( question ) == 0 {
return nil , sender , nil , errPollMissingQuestion
}
options := make ( [ ] * waProto . PollCreationMessage_Option , len ( content . PollStart . Answers ) )
optionMap := make ( map [ [ 32 ] byte ] string , len ( options ) )
for i , opt := range content . PollStart . Answers {
body , _ := portal . msc1767ToWhatsApp ( opt . MSC1767Message , false )
hash := sha256 . Sum256 ( [ ] byte ( body ) )
if _ , alreadyExists := optionMap [ hash ] ; alreadyExists {
portal . log . Warnfln ( "Poll %s by %s has option %q more than once, rejecting" , evt . ID , evt . Sender , body )
return nil , sender , nil , errPollDuplicateOption
}
optionMap [ hash ] = opt . ID
options [ i ] = & waProto . PollCreationMessage_Option {
OptionName : proto . String ( body ) ,
}
}
secret := make ( [ ] byte , 32 )
_ , err := rand . Read ( secret )
return & waProto . Message {
PollCreationMessage : & waProto . PollCreationMessage {
Name : proto . String ( question ) ,
Options : options ,
SelectableOptionsCount : proto . Uint32 ( uint32 ( maxAnswers ) ) ,
ContextInfo : ctxInfo ,
} ,
MessageContextInfo : & waProto . MessageContextInfo {
MessageSecret : secret ,
} ,
} , sender , & extraConvertMeta { PollOptions : optionMap } , err
}
2018-08-28 23:40:54 +02:00
2022-12-23 14:17:57 +01:00
func ( portal * Portal ) generateContextInfo ( relatesTo * event . RelatesTo ) * waProto . ContextInfo {
2021-10-22 19:14:34 +02:00
var ctxInfo waProto . ContextInfo
2022-12-23 14:17:57 +01:00
replyToID := relatesTo . GetReplyTo ( )
2018-08-25 23:26:24 +02:00
if len ( replyToID ) > 0 {
2021-10-22 19:14:34 +02:00
replyToMsg := portal . bridge . DB . Message . GetByMXID ( replyToID )
2022-12-23 14:17:57 +01:00
if replyToMsg != nil && ! replyToMsg . IsFakeJID ( ) && ( replyToMsg . Type == database . MsgNormal || replyToMsg . Type == database . MsgMatrixPoll ) {
2021-10-22 19:14:34 +02:00
ctxInfo . StanzaId = & replyToMsg . JID
2021-11-01 15:28:32 +01:00
ctxInfo . Participant = proto . String ( replyToMsg . Sender . ToNonAD ( ) . String ( ) )
2021-08-19 18:19:56 +02:00
// Using blank content here seems to work fine on all official WhatsApp apps.
2021-11-01 15:28:32 +01:00
//
// We could probably invent a slightly more accurate version of the quoted message
// by fetching the Matrix event and converting it to the WhatsApp format, but that's
// a lot of work and this works fine.
ctxInfo . QuotedMessage = & waProto . Message { Conversation : proto . String ( "" ) }
2018-08-25 23:26:24 +02:00
}
2018-08-24 21:31:18 +02:00
}
2022-01-07 13:32:00 +01:00
if portal . ExpirationTime != 0 {
ctxInfo . Expiration = proto . Uint32 ( portal . ExpirationTime )
}
2022-12-23 14:17:57 +01:00
return & ctxInfo
}
type extraConvertMeta struct {
PollOptions map [ [ 32 ] byte ] string
2023-01-16 16:15:04 +01:00
EditRootMsg * database . Message
2022-12-23 14:17:57 +01:00
}
2023-04-06 13:15:44 +02:00
func getEditError ( rootMsg * database . Message , editer * User ) error {
switch {
case rootMsg == nil :
return errEditUnknownTarget
case rootMsg . Type != database . MsgNormal || rootMsg . IsFakeJID ( ) :
return errEditUnknownTargetType
case rootMsg . Sender . User != editer . JID . User :
return errEditDifferentSender
case time . Since ( rootMsg . Timestamp ) > whatsmeow . EditWindow :
return errEditTooOld
default :
return nil
}
}
2022-12-23 14:17:57 +01:00
func ( portal * Portal ) convertMatrixMessage ( ctx context . Context , sender * User , evt * event . Event ) ( * waProto . Message , * User , * extraConvertMeta , error ) {
if evt . Type == TypeMSC3381PollResponse || evt . Type == TypeMSC3381V2PollResponse {
return portal . convertMatrixPollVote ( ctx , sender , evt )
} else if evt . Type == TypeMSC3381PollStart {
return portal . convertMatrixPollStart ( ctx , sender , evt )
}
content , ok := evt . Content . Parsed . ( * event . MessageEventContent )
if ! ok {
return nil , sender , nil , fmt . Errorf ( "%w %T" , errUnexpectedParsedContentType , evt . Content . Parsed )
}
2023-01-16 16:15:04 +01:00
extraMeta := & extraConvertMeta { }
2023-05-02 18:59:10 +02:00
realSenderMXID := sender . MXID
2023-05-01 14:09:22 +02:00
isRelay := false
if ! sender . IsLoggedIn ( ) || ( portal . IsPrivateChat ( ) && sender . JID . User != portal . Key . Receiver . User ) {
if ! portal . HasRelaybot ( ) {
return nil , sender , extraMeta , errUserNotLoggedIn
}
sender = portal . GetRelayUser ( )
isRelay = true
}
2022-12-23 14:17:57 +01:00
var editRootMsg * database . Message
2023-04-07 11:22:04 +02:00
if editEventID := content . RelatesTo . GetReplaceID ( ) ; editEventID != "" {
2022-12-23 14:17:57 +01:00
editRootMsg = portal . bridge . DB . Message . GetByMXID ( editEventID )
2023-04-06 13:15:44 +02:00
if editErr := getEditError ( editRootMsg , sender ) ; editErr != nil {
return nil , sender , extraMeta , editErr
2022-12-23 14:17:57 +01:00
}
2023-01-16 16:15:04 +01:00
extraMeta . EditRootMsg = editRootMsg
2022-12-23 14:17:57 +01:00
if content . NewContent != nil {
content = content . NewContent
}
}
msg := & waProto . Message { }
ctxInfo := portal . generateContextInfo ( content . RelatesTo )
2023-05-02 18:59:10 +02:00
relaybotFormatted := isRelay && portal . addRelaybotFormat ( realSenderMXID , content )
2020-05-08 21:32:22 +02:00
if evt . Type == event . EventSticker {
2022-08-22 14:00:01 +02:00
if relaybotFormatted {
// Stickers can't have captions, so force relaybot stickers to be images
content . MsgType = event . MsgImage
} else {
content . MsgType = event . MessageType ( event . EventSticker . Type )
}
2021-07-23 21:44:35 +02:00
}
if content . MsgType == event . MsgImage && content . GetInfo ( ) . MimeType == "image/gif" {
2020-06-23 15:36:05 +02:00
content . MsgType = event . MsgVideo
2019-12-31 19:17:03 +01:00
}
2023-05-11 21:10:01 +02:00
if content . MsgType == event . MsgAudio && content . FileName != "" && content . Body != content . FileName {
// Send audio messages with captions as files since WhatsApp doesn't support captions on audio messages
content . MsgType = event . MsgFile
}
2020-05-24 16:28:30 +02:00
2020-05-08 21:32:22 +02:00
switch content . MsgType {
case event . MsgText , event . MsgEmote , event . MsgNotice :
text := content . Body
2021-02-26 15:10:57 +01:00
if content . MsgType == event . MsgNotice && ! portal . bridge . Config . Bridge . BridgeNotices {
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , errMNoticeDisabled
2021-02-26 15:10:57 +01:00
}
2020-05-08 21:32:22 +02:00
if content . Format == event . FormatHTML {
2023-05-24 11:41:54 +02:00
text , ctxInfo . MentionedJid = portal . bridge . Formatter . ParseMatrix ( content . FormattedBody , content . Mentions )
2018-08-23 23:52:06 +02:00
}
2020-05-08 21:32:22 +02:00
if content . MsgType == event . MsgEmote && ! relaybotFormatted {
2018-08-24 21:31:18 +02:00
text = "/me " + text
}
2022-02-15 15:28:20 +01:00
msg . ExtendedTextMessage = & waProto . ExtendedTextMessage {
Text : & text ,
2022-12-23 14:17:57 +01:00
ContextInfo : ctxInfo ,
2022-02-15 15:28:20 +01:00
}
2022-06-29 19:05:55 +02:00
hasPreview := portal . convertURLPreviewToWhatsApp ( ctx , sender , evt , msg . ExtendedTextMessage )
if ctx . Err ( ) != nil {
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , ctx . Err ( )
2022-06-29 19:05:55 +02:00
}
2022-02-15 15:28:20 +01:00
if ctxInfo . StanzaId == nil && ctxInfo . MentionedJid == nil && ctxInfo . Expiration == nil && ! hasPreview {
// No need for extended message
msg . ExtendedTextMessage = nil
2021-10-22 19:14:34 +02:00
msg . Conversation = & text
2018-08-25 23:26:24 +02:00
}
2020-05-08 21:32:22 +02:00
case event . MsgImage :
2022-06-29 19:05:55 +02:00
media , err := portal . preprocessMatrixMedia ( ctx , sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaImage )
2018-08-25 23:26:24 +02:00
if media == nil {
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , err
2018-08-24 21:31:18 +02:00
}
2020-07-31 13:30:58 +02:00
ctxInfo . MentionedJid = media . MentionedJIDs
2021-10-22 19:14:34 +02:00
msg . ImageMessage = & waProto . ImageMessage {
2022-12-23 14:17:57 +01:00
ContextInfo : ctxInfo ,
2018-08-25 23:26:24 +02:00
Caption : & media . Caption ,
JpegThumbnail : media . Thumbnail ,
Url : & media . URL ,
2023-02-15 15:04:05 +01:00
DirectPath : & media . DirectPath ,
2018-08-25 23:26:24 +02:00
MediaKey : media . MediaKey ,
2020-05-08 21:32:22 +02:00
Mimetype : & content . GetInfo ( ) . MimeType ,
2018-08-25 23:26:24 +02:00
FileEncSha256 : media . FileEncSHA256 ,
FileSha256 : media . FileSHA256 ,
2021-10-22 19:14:34 +02:00
FileLength : proto . Uint64 ( uint64 ( media . FileLength ) ) ,
2018-08-25 23:26:24 +02:00
}
2022-08-22 14:00:01 +02:00
case event . MessageType ( event . EventSticker . Type ) :
media , err := portal . preprocessMatrixMedia ( ctx , sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaImage )
if media == nil {
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , err
2022-08-22 14:00:01 +02:00
}
ctxInfo . MentionedJid = media . MentionedJIDs
msg . StickerMessage = & waProto . StickerMessage {
2022-12-23 14:17:57 +01:00
ContextInfo : ctxInfo ,
2022-08-22 14:00:01 +02:00
PngThumbnail : media . Thumbnail ,
Url : & media . URL ,
2023-02-15 15:04:05 +01:00
DirectPath : & media . DirectPath ,
2022-08-22 14:00:01 +02:00
MediaKey : media . MediaKey ,
Mimetype : & content . GetInfo ( ) . MimeType ,
FileEncSha256 : media . FileEncSHA256 ,
FileSha256 : media . FileSHA256 ,
FileLength : proto . Uint64 ( uint64 ( media . FileLength ) ) ,
}
2020-05-08 21:32:22 +02:00
case event . MsgVideo :
2020-06-23 15:36:05 +02:00
gifPlayback := content . GetInfo ( ) . MimeType == "image/gif"
2022-06-29 19:05:55 +02:00
media , err := portal . preprocessMatrixMedia ( ctx , sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaVideo )
2018-08-25 23:26:24 +02:00
if media == nil {
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , err
2018-08-24 21:31:18 +02:00
}
2021-06-22 19:03:22 +02:00
duration := uint32 ( content . GetInfo ( ) . Duration / 1000 )
2020-07-31 13:30:58 +02:00
ctxInfo . MentionedJid = media . MentionedJIDs
2021-10-22 19:14:34 +02:00
msg . VideoMessage = & waProto . VideoMessage {
2022-12-23 14:17:57 +01:00
ContextInfo : ctxInfo ,
2018-08-25 23:26:24 +02:00
Caption : & media . Caption ,
JpegThumbnail : media . Thumbnail ,
Url : & media . URL ,
2023-02-15 15:04:05 +01:00
DirectPath : & media . DirectPath ,
2018-08-25 23:26:24 +02:00
MediaKey : media . MediaKey ,
2020-05-08 21:32:22 +02:00
Mimetype : & content . GetInfo ( ) . MimeType ,
2020-06-23 15:36:05 +02:00
GifPlayback : & gifPlayback ,
2018-08-25 23:26:24 +02:00
Seconds : & duration ,
FileEncSha256 : media . FileEncSHA256 ,
FileSha256 : media . FileSHA256 ,
2021-10-22 19:14:34 +02:00
FileLength : proto . Uint64 ( uint64 ( media . FileLength ) ) ,
2018-08-25 23:26:24 +02:00
}
2020-05-08 21:32:22 +02:00
case event . MsgAudio :
2022-06-29 19:05:55 +02:00
media , err := portal . preprocessMatrixMedia ( ctx , sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaAudio )
2018-08-25 23:26:24 +02:00
if media == nil {
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , err
2018-08-24 21:31:18 +02:00
}
2021-06-22 19:03:22 +02:00
duration := uint32 ( content . GetInfo ( ) . Duration / 1000 )
2021-10-22 19:14:34 +02:00
msg . AudioMessage = & waProto . AudioMessage {
2022-12-23 14:17:57 +01:00
ContextInfo : ctxInfo ,
2018-08-25 23:26:24 +02:00
Url : & media . URL ,
2023-02-15 15:04:05 +01:00
DirectPath : & media . DirectPath ,
2018-08-25 23:26:24 +02:00
MediaKey : media . MediaKey ,
2020-05-08 21:32:22 +02:00
Mimetype : & content . GetInfo ( ) . MimeType ,
2018-08-25 23:26:24 +02:00
Seconds : & duration ,
FileEncSha256 : media . FileEncSHA256 ,
FileSha256 : media . FileSHA256 ,
2021-10-22 19:14:34 +02:00
FileLength : proto . Uint64 ( uint64 ( media . FileLength ) ) ,
2018-08-25 23:26:24 +02:00
}
2021-06-23 12:23:00 +02:00
_ , isMSC3245Voice := evt . Content . Raw [ "org.matrix.msc3245.voice" ]
2022-02-10 11:50:04 +01:00
if isMSC3245Voice {
msg . AudioMessage . Waveform = getUnstableWaveform ( evt . Content . Raw )
2021-10-22 19:14:34 +02:00
msg . AudioMessage . Ptt = proto . Bool ( true )
2021-06-22 19:33:30 +02:00
// hacky hack to add the codecs param that whatsapp seems to require
2021-10-22 19:14:34 +02:00
msg . AudioMessage . Mimetype = proto . String ( addCodecToMime ( content . GetInfo ( ) . MimeType , "opus" ) )
2021-06-22 19:33:30 +02:00
}
2020-05-08 21:32:22 +02:00
case event . MsgFile :
2022-06-29 19:05:55 +02:00
media , err := portal . preprocessMatrixMedia ( ctx , sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaDocument )
2018-08-25 23:26:24 +02:00
if media == nil {
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , err
2018-08-24 21:31:18 +02:00
}
2021-10-22 19:14:34 +02:00
msg . DocumentMessage = & waProto . DocumentMessage {
2022-12-23 14:17:57 +01:00
ContextInfo : ctxInfo ,
2022-07-11 13:38:47 +02:00
Caption : & media . Caption ,
2021-11-11 19:33:22 +01:00
JpegThumbnail : media . Thumbnail ,
2018-08-25 23:26:24 +02:00
Url : & media . URL ,
2023-02-15 15:04:05 +01:00
DirectPath : & media . DirectPath ,
2022-07-11 13:38:47 +02:00
Title : & media . FileName ,
FileName : & media . FileName ,
2018-08-25 23:26:24 +02:00
MediaKey : media . MediaKey ,
2020-05-08 21:32:22 +02:00
Mimetype : & content . GetInfo ( ) . MimeType ,
2018-08-25 23:26:24 +02:00
FileEncSha256 : media . FileEncSHA256 ,
FileSha256 : media . FileSHA256 ,
2021-10-22 19:14:34 +02:00
FileLength : proto . Uint64 ( uint64 ( media . FileLength ) ) ,
2018-08-25 23:26:24 +02:00
}
2022-07-30 10:30:55 +02:00
if media . Caption != "" {
msg . DocumentWithCaptionMessage = & waProto . FutureProofMessage {
Message : & waProto . Message {
DocumentMessage : msg . DocumentMessage ,
} ,
}
msg . DocumentMessage = nil
}
2021-08-02 11:53:38 +02:00
case event . MsgLocation :
lat , long , err := parseGeoURI ( content . GeoURI )
if err != nil {
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , fmt . Errorf ( "%w: %v" , errInvalidGeoURI , err )
2021-08-02 11:53:38 +02:00
}
2021-10-22 19:14:34 +02:00
msg . LocationMessage = & waProto . LocationMessage {
2021-08-02 11:53:38 +02:00
DegreesLatitude : & lat ,
DegreesLongitude : & long ,
Comment : & content . Body ,
2022-12-23 14:17:57 +01:00
ContextInfo : ctxInfo ,
2021-08-02 11:53:38 +02:00
}
2018-08-19 17:21:38 +02:00
default :
2023-01-16 16:15:04 +01:00
return nil , sender , extraMeta , fmt . Errorf ( "%w %q" , errUnknownMsgType , content . MsgType )
2020-05-24 16:28:30 +02:00
}
2022-10-08 16:46:42 +02:00
if editRootMsg != nil {
msg = & waProto . Message {
2023-04-06 13:21:17 +02:00
EditedMessage : & waProto . FutureProofMessage {
Message : & waProto . Message {
ProtocolMessage : & waProto . ProtocolMessage {
Key : & waProto . MessageKey {
FromMe : proto . Bool ( true ) ,
Id : proto . String ( editRootMsg . JID ) ,
RemoteJid : proto . String ( portal . Key . JID . String ( ) ) ,
} ,
Type : waProto . ProtocolMessage_MESSAGE_EDIT . Enum ( ) ,
EditedMessage : msg ,
TimestampMs : proto . Int64 ( evt . Timestamp ) ,
} ,
2022-10-08 16:46:42 +02:00
} ,
} ,
}
}
2023-01-16 16:15:04 +01:00
return msg , sender , extraMeta , nil
2020-05-24 16:28:30 +02:00
}
2021-10-22 19:14:34 +02:00
func ( portal * Portal ) generateMessageInfo ( sender * User ) * types . MessageInfo {
return & types . MessageInfo {
ID : whatsmeow . GenerateMessageID ( ) ,
Timestamp : time . Now ( ) ,
MessageSource : types . MessageSource {
Sender : sender . JID ,
Chat : portal . Key . JID ,
IsFromMe : true ,
IsGroup : portal . Key . JID . Server == types . GroupServer || portal . Key . JID . Server == types . BroadcastServer ,
} ,
}
}
2022-07-11 13:20:31 +02:00
func ( portal * Portal ) HandleMatrixMessage ( sender * User , evt * event . Event , timings messageTimings ) {
start := time . Now ( )
ms := metricSender { portal : portal , timings : & timings }
2022-12-23 14:17:57 +01:00
allowRelay := evt . Type != TypeMSC3381PollResponse && evt . Type != TypeMSC3381V2PollResponse && evt . Type != TypeMSC3381PollStart
2022-11-17 23:20:14 +01:00
if err := portal . canBridgeFrom ( sender , allowRelay ) ; err != nil {
2022-07-11 13:20:31 +02:00
go ms . sendMessageMetrics ( evt , err , "Ignoring" , true )
2018-08-19 17:21:38 +02:00
return
2022-06-13 19:01:39 +02:00
} else if portal . Key . JID == types . StatusBroadcastJID && portal . bridge . Config . Bridge . DisableStatusBroadcastSend {
2022-07-11 13:20:31 +02:00
go ms . sendMessageMetrics ( evt , errBroadcastSendDisabled , "Ignoring" , true )
2022-06-13 19:01:39 +02:00
return
2018-08-19 17:21:38 +02:00
}
2022-06-30 13:41:37 +02:00
2022-07-11 13:20:31 +02:00
messageAge := timings . totalReceive
2022-06-29 19:05:55 +02:00
origEvtID := evt . ID
var dbMsg * database . Message
if retryMeta := evt . Content . AsMessage ( ) . MessageSendRetry ; retryMeta != nil {
origEvtID = retryMeta . OriginalEventID
dbMsg = portal . bridge . DB . Message . GetByMXID ( origEvtID )
if dbMsg != nil && dbMsg . Sent {
2022-06-30 13:41:37 +02:00
portal . log . Debugfln ( "Ignoring retry request %s (#%d, age: %s) for %s/%s from %s as message was already sent" , evt . ID , retryMeta . RetryCount , messageAge , origEvtID , dbMsg . JID , evt . Sender )
go ms . sendMessageMetrics ( evt , nil , "" , true )
2022-06-29 19:05:55 +02:00
return
} else if dbMsg != nil {
2022-06-30 13:41:37 +02:00
portal . log . Debugfln ( "Got retry request %s (#%d, age: %s) for %s/%s from %s" , evt . ID , retryMeta . RetryCount , messageAge , origEvtID , dbMsg . JID , evt . Sender )
2022-06-29 19:05:55 +02:00
} else {
2022-06-30 13:41:37 +02:00
portal . log . Debugfln ( "Got retry request %s (#%d, age: %s) for %s from %s (original message not known)" , evt . ID , retryMeta . RetryCount , messageAge , origEvtID , evt . Sender )
2022-06-29 19:05:55 +02:00
}
} else {
2022-06-30 13:41:37 +02:00
portal . log . Debugfln ( "Received message %s from %s (age: %s)" , evt . ID , evt . Sender , messageAge )
}
2022-07-11 14:08:34 +02:00
errorAfter := portal . bridge . Config . Bridge . MessageHandlingTimeout . ErrorAfter
deadline := portal . bridge . Config . Bridge . MessageHandlingTimeout . Deadline
isScheduled , _ := evt . Content . Raw [ "com.beeper.scheduled" ] . ( bool )
if isScheduled {
portal . log . Debugfln ( "%s is a scheduled message, extending handling timeouts" , evt . ID )
errorAfter *= 10
deadline *= 10
}
if errorAfter > 0 {
remainingTime := errorAfter - messageAge
2022-06-30 13:41:37 +02:00
if remainingTime < 0 {
2022-07-01 10:02:35 +02:00
go ms . sendMessageMetrics ( evt , errTimeoutBeforeHandling , "Timeout handling" , true )
2022-06-30 13:41:37 +02:00
return
2022-06-30 13:46:46 +02:00
} else if remainingTime < 1 * time . Second {
2022-07-11 14:08:34 +02:00
portal . log . Warnfln ( "Message %s was delayed before reaching the bridge, only have %s (of %s timeout) until delay warning" , evt . ID , remainingTime , errorAfter )
2022-06-30 13:41:37 +02:00
}
go func ( ) {
time . Sleep ( remainingTime )
ms . sendMessageMetrics ( evt , errMessageTakingLong , "Timeout handling" , false )
} ( )
2022-06-29 19:05:55 +02:00
}
ctx := context . Background ( )
2022-07-11 14:08:34 +02:00
if deadline > 0 {
2022-06-29 19:05:55 +02:00
var cancel context . CancelFunc
2022-07-11 14:08:34 +02:00
ctx , cancel = context . WithTimeout ( ctx , deadline )
2022-06-29 19:05:55 +02:00
defer cancel ( )
}
2022-07-11 13:20:31 +02:00
timings . preproc = time . Since ( start )
start = time . Now ( )
2022-12-23 14:17:57 +01:00
msg , sender , extraMeta , err := portal . convertMatrixMessage ( ctx , sender , evt )
2022-07-11 13:20:31 +02:00
timings . convert = time . Since ( start )
2021-10-22 19:14:34 +02:00
if msg == nil {
2022-06-30 13:41:37 +02:00
go ms . sendMessageMetrics ( evt , err , "Error converting" , true )
2020-05-25 22:11:00 +02:00
return
}
2022-10-08 16:46:42 +02:00
dbMsgType := database . MsgNormal
2022-12-23 14:17:57 +01:00
if msg . PollCreationMessage != nil || msg . PollCreationMessageV2 != nil {
dbMsgType = database . MsgMatrixPoll
2023-04-06 13:21:17 +02:00
} else if msg . EditedMessage == nil {
2023-01-16 16:15:04 +01:00
portal . MarkDisappearing ( nil , origEvtID , time . Duration ( portal . ExpirationTime ) * time . Second , time . Now ( ) )
2022-10-08 16:46:42 +02:00
} else {
dbMsgType = database . MsgEdit
}
2021-10-22 19:14:34 +02:00
info := portal . generateMessageInfo ( sender )
2022-06-29 19:05:55 +02:00
if dbMsg == nil {
2022-10-08 16:46:42 +02:00
dbMsg = portal . markHandled ( nil , nil , info , evt . ID , false , true , dbMsgType , database . MsgNoError )
2022-06-29 19:05:55 +02:00
} else {
info . ID = dbMsg . JID
}
2022-12-23 14:17:57 +01:00
if dbMsgType == database . MsgMatrixPoll && extraMeta != nil && extraMeta . PollOptions != nil {
dbMsg . PutPollOptions ( extraMeta . PollOptions )
}
2021-10-22 19:14:34 +02:00
portal . log . Debugln ( "Sending event" , evt . ID , "to WhatsApp" , info . ID )
2022-07-11 13:20:31 +02:00
start = time . Now ( )
2023-01-01 12:30:09 +01:00
resp , err := sender . Client . SendMessage ( ctx , portal . Key . JID , msg , whatsmeow . SendRequestExtra { ID : info . ID } )
2022-07-11 13:20:31 +02:00
timings . totalSend = time . Since ( start )
timings . whatsmeow = resp . DebugTimings
2022-06-30 13:41:37 +02:00
go ms . sendMessageMetrics ( evt , err , "Error sending" , true )
2022-05-31 16:28:58 +02:00
if err == nil {
2022-07-11 13:20:31 +02:00
dbMsg . MarkSent ( resp . Timestamp )
2020-05-24 14:33:26 +02:00
}
2018-08-18 21:57:08 +02:00
}
2019-05-16 00:59:36 +02:00
2022-03-05 20:22:31 +01:00
func ( portal * Portal ) HandleMatrixReaction ( sender * User , evt * event . Event ) {
2022-05-31 16:28:58 +02:00
if err := portal . canBridgeFrom ( sender , false ) ; err != nil {
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , err , "Ignoring" , nil )
2022-05-22 15:15:54 +02:00
return
2022-06-13 19:01:39 +02:00
} else if portal . Key . JID . Server == types . BroadcastServer {
// TODO implement this, probably by only sending the reaction to the sender of the status message?
// (whatsapp hasn't published the feature yet)
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , errBroadcastReactionNotSupported , "Ignoring" , nil )
2022-06-13 19:01:39 +02:00
return
2022-05-22 15:15:54 +02:00
}
content , ok := evt . Content . Parsed . ( * event . ReactionEventContent )
if ok && strings . Contains ( content . RelatesTo . Key , "retry" ) || strings . HasPrefix ( content . RelatesTo . Key , "\u267b" ) { // ♻️
if retryRequested , _ := portal . requestMediaRetry ( sender , content . RelatesTo . EventID , nil ) ; retryRequested {
_ , _ = portal . MainIntent ( ) . RedactEvent ( portal . MXID , evt . ID , mautrix . ReqRedact {
Reason : "requested media from phone" ,
} )
// Errored media, don't try to send as reaction
return
}
}
2022-03-05 20:22:31 +01:00
portal . log . Debugfln ( "Received reaction event %s from %s" , evt . ID , evt . Sender )
2022-04-18 10:14:43 +02:00
err := portal . handleMatrixReaction ( sender , evt )
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , err , "Error sending" , nil )
2022-04-18 10:14:43 +02:00
}
func ( portal * Portal ) handleMatrixReaction ( sender * User , evt * event . Event ) error {
2022-03-05 20:22:31 +01:00
content , ok := evt . Content . Parsed . ( * event . ReactionEventContent )
if ! ok {
2022-04-18 10:14:43 +02:00
return fmt . Errorf ( "unexpected parsed content type %T" , evt . Content . Parsed )
2022-03-05 20:22:31 +01:00
}
target := portal . bridge . DB . Message . GetByMXID ( content . RelatesTo . EventID )
if target == nil || target . Type == database . MsgReaction {
2022-04-18 10:14:43 +02:00
return fmt . Errorf ( "unknown target event %s" , content . RelatesTo . EventID )
2022-03-05 20:22:31 +01:00
}
info := portal . generateMessageInfo ( sender )
2022-05-13 01:56:40 +02:00
dbMsg := portal . markHandled ( nil , nil , info , evt . ID , false , true , database . MsgReaction , database . MsgNoError )
2022-09-28 14:54:02 +02:00
portal . upsertReaction ( nil , nil , target . JID , sender . JID , evt . ID , info . ID )
2022-03-05 20:22:31 +01:00
portal . log . Debugln ( "Sending reaction" , evt . ID , "to WhatsApp" , info . ID )
2022-07-11 13:20:31 +02:00
resp , err := portal . sendReactionToWhatsApp ( sender , info . ID , target , content . RelatesTo . Key , evt . Timestamp )
2022-05-31 16:28:58 +02:00
if err == nil {
2022-07-11 13:20:31 +02:00
dbMsg . MarkSent ( resp . Timestamp )
2022-03-05 20:22:31 +01:00
}
2022-04-18 10:14:43 +02:00
return err
2022-03-05 20:22:31 +01:00
}
2022-07-11 13:20:31 +02:00
func ( portal * Portal ) sendReactionToWhatsApp ( sender * User , id types . MessageID , target * database . Message , key string , timestamp int64 ) ( whatsmeow . SendResponse , error ) {
2022-03-05 20:22:31 +01:00
var messageKeyParticipant * string
if ! portal . IsPrivateChat ( ) {
messageKeyParticipant = proto . String ( target . Sender . ToNonAD ( ) . String ( ) )
}
2022-03-18 00:12:23 +01:00
key = variationselector . Remove ( key )
2023-01-01 12:30:09 +01:00
return sender . Client . SendMessage ( context . TODO ( ) , portal . Key . JID , & waProto . Message {
2022-03-05 20:22:31 +01:00
ReactionMessage : & waProto . ReactionMessage {
Key : & waProto . MessageKey {
RemoteJid : proto . String ( portal . Key . JID . String ( ) ) ,
FromMe : proto . Bool ( target . Sender . User == sender . JID . User ) ,
Id : proto . String ( target . JID ) ,
Participant : messageKeyParticipant ,
} ,
Text : proto . String ( key ) ,
SenderTimestampMs : proto . Int64 ( timestamp ) ,
} ,
2023-01-01 12:30:09 +01:00
} , whatsmeow . SendRequestExtra { ID : id } )
2022-03-05 20:22:31 +01:00
}
2022-09-28 14:54:02 +02:00
func ( portal * Portal ) upsertReaction ( txn dbutil . Transaction , intent * appservice . IntentAPI , targetJID types . MessageID , senderJID types . JID , mxid id . EventID , jid types . MessageID ) {
2022-03-05 20:22:31 +01:00
dbReaction := portal . bridge . DB . Reaction . GetByTargetJID ( portal . Key , targetJID , senderJID )
if dbReaction == nil {
dbReaction = portal . bridge . DB . Reaction . New ( )
dbReaction . Chat = portal . Key
dbReaction . TargetJID = targetJID
dbReaction . Sender = senderJID
2022-09-28 14:07:09 +02:00
} else if intent != nil {
2022-03-05 20:22:31 +01:00
portal . log . Debugfln ( "Redacting old Matrix reaction %s after new one (%s) was sent" , dbReaction . MXID , mxid )
var err error
if intent != nil {
2022-06-30 19:56:25 +02:00
_ , err = intent . RedactEvent ( portal . MXID , dbReaction . MXID )
2022-03-05 20:22:31 +01:00
}
if intent == nil || errors . Is ( err , mautrix . MForbidden ) {
_ , err = portal . MainIntent ( ) . RedactEvent ( portal . MXID , dbReaction . MXID )
}
if err != nil {
portal . log . Warnfln ( "Failed to remove old reaction %s: %v" , dbReaction . MXID , err )
}
}
dbReaction . MXID = mxid
dbReaction . JID = jid
2022-09-28 14:54:02 +02:00
dbReaction . Upsert ( txn )
2022-03-05 20:22:31 +01:00
}
2020-05-08 21:32:22 +02:00
func ( portal * Portal ) HandleMatrixRedaction ( sender * User , evt * event . Event ) {
2022-05-31 16:28:58 +02:00
if err := portal . canBridgeFrom ( sender , true ) ; err != nil {
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , err , "Ignoring" , nil )
2019-05-16 00:59:36 +02:00
return
}
2021-11-01 15:46:03 +01:00
portal . log . Debugfln ( "Received redaction %s from %s" , evt . ID , evt . Sender )
senderLogIdentifier := sender . MXID
if ! sender . HasSession ( ) {
sender = portal . GetRelayUser ( )
senderLogIdentifier += " (through relaybot)"
}
2019-05-16 00:59:36 +02:00
msg := portal . bridge . DB . Message . GetByMXID ( evt . Redacts )
2021-10-27 20:34:22 +02:00
if msg == nil {
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , errTargetNotFound , "Ignoring" , nil )
2021-11-02 14:46:31 +01:00
} else if msg . IsFakeJID ( ) {
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , errTargetIsFake , "Ignoring" , nil )
2022-06-13 19:01:39 +02:00
} else if portal . Key . JID == types . StatusBroadcastJID && portal . bridge . Config . Bridge . DisableStatusBroadcastSend {
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , errBroadcastSendDisabled , "Ignoring" , nil )
2022-05-31 16:28:58 +02:00
} else if msg . Type == database . MsgReaction {
2022-07-30 10:30:55 +02:00
if msg . Sender . User != sender . JID . User {
go portal . sendMessageMetrics ( evt , errReactionSentBySomeoneElse , "Ignoring" , nil )
} else if reaction := portal . bridge . DB . Reaction . GetByMXID ( evt . Redacts ) ; reaction == nil {
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , errReactionDatabaseNotFound , "Ignoring" , nil )
2022-03-05 20:22:31 +01:00
} else if reactionTarget := reaction . GetTarget ( ) ; reactionTarget == nil {
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , errReactionTargetNotFound , "Ignoring" , nil )
2022-03-05 20:22:31 +01:00
} else {
portal . log . Debugfln ( "Sending redaction reaction %s of %s/%s to WhatsApp" , evt . ID , msg . MXID , msg . JID )
2022-05-31 16:28:58 +02:00
_ , err := portal . sendReactionToWhatsApp ( sender , "" , reactionTarget , "" , evt . Timestamp )
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , err , "Error sending" , nil )
2022-03-05 20:22:31 +01:00
}
} else {
2022-07-30 10:30:55 +02:00
key := & waProto . MessageKey {
FromMe : proto . Bool ( true ) ,
Id : proto . String ( msg . JID ) ,
RemoteJid : proto . String ( portal . Key . JID . String ( ) ) ,
}
if msg . Sender . User != sender . JID . User {
if portal . IsPrivateChat ( ) {
go portal . sendMessageMetrics ( evt , errDMSentByOtherUser , "Ignoring" , nil )
return
}
key . FromMe = proto . Bool ( false )
key . Participant = proto . String ( msg . Sender . ToNonAD ( ) . String ( ) )
}
2022-03-05 20:22:31 +01:00
portal . log . Debugfln ( "Sending redaction %s of %s/%s to WhatsApp" , evt . ID , msg . MXID , msg . JID )
2023-01-01 12:30:09 +01:00
_ , err := sender . Client . SendMessage ( context . TODO ( ) , portal . Key . JID , & waProto . Message {
2022-07-30 10:30:55 +02:00
ProtocolMessage : & waProto . ProtocolMessage {
Type : waProto . ProtocolMessage_REVOKE . Enum ( ) ,
Key : key ,
} ,
} )
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , err , "Error sending" , nil )
2019-05-16 00:59:36 +02:00
}
}
2019-05-16 19:14:32 +02:00
2022-10-05 20:18:48 +02:00
func ( portal * Portal ) HandleMatrixReadReceipt ( sender bridge . User , eventID id . EventID , receipt event . ReadReceipt ) {
portal . handleMatrixReadReceipt ( sender . ( * User ) , eventID , receipt . Timestamp , true )
2022-05-22 15:15:54 +02:00
}
func ( portal * Portal ) handleMatrixReadReceipt ( sender * User , eventID id . EventID , receiptTimestamp time . Time , isExplicit bool ) {
2021-12-07 15:02:51 +01:00
if ! sender . IsLoggedIn ( ) {
2022-01-17 11:00:02 +01:00
if isExplicit {
2022-07-09 10:16:43 +02:00
portal . log . Debugfln ( "Ignoring read receipt by %s/%s: user is not connected to WhatsApp" , sender . MXID , sender . JID )
2022-01-17 11:00:02 +01:00
}
2021-12-07 15:02:51 +01:00
return
}
2021-11-30 15:38:37 +01:00
maxTimestamp := receiptTimestamp
2022-01-17 11:00:02 +01:00
// Implicit read receipts don't have an event ID that's already bridged
if isExplicit {
if message := portal . bridge . DB . Message . GetByMXID ( eventID ) ; message != nil {
maxTimestamp = message . Timestamp
}
2021-11-30 15:38:37 +01:00
}
prevTimestamp := sender . GetLastReadTS ( portal . Key )
2022-01-17 09:38:44 +01:00
lastReadIsZero := false
2021-11-30 15:38:37 +01:00
if prevTimestamp . IsZero ( ) {
prevTimestamp = maxTimestamp . Add ( - 2 * time . Second )
2022-01-17 09:38:44 +01:00
lastReadIsZero = true
2021-11-30 15:38:37 +01:00
}
messages := portal . bridge . DB . Message . GetMessagesBetween ( portal . Key , prevTimestamp , maxTimestamp )
2021-12-01 20:14:37 +01:00
if len ( messages ) > 0 {
sender . SetLastReadTS ( portal . Key , messages [ len ( messages ) - 1 ] . Timestamp )
}
2021-11-30 15:38:37 +01:00
groupedMessages := make ( map [ types . JID ] [ ] types . MessageID )
for _ , msg := range messages {
2021-12-25 19:50:36 +01:00
var key types . JID
if msg . IsFakeJID ( ) || msg . Sender . User == sender . JID . User {
// Don't send read receipts for own messages or fake messages
continue
} else if ! portal . IsPrivateChat ( ) {
key = msg . Sender
} else if ! msg . BroadcastListJID . IsEmpty ( ) {
key = msg . BroadcastListJID
} // else: blank key (participant field isn't needed in direct chat read receipts)
groupedMessages [ key ] = append ( groupedMessages [ key ] , msg . JID )
2021-11-30 15:38:37 +01:00
}
2022-01-17 11:00:02 +01:00
// For explicit read receipts, log even if there are no targets. For implicit ones only log when there are targets
if len ( groupedMessages ) > 0 || isExplicit {
portal . log . Debugfln ( "Sending read receipts by %s (last read: %d, was zero: %t, explicit: %t): %v" ,
sender . JID , prevTimestamp . Unix ( ) , lastReadIsZero , isExplicit , groupedMessages )
}
2021-11-30 15:38:37 +01:00
for messageSender , ids := range groupedMessages {
2021-12-25 19:50:36 +01:00
chatJID := portal . Key . JID
if messageSender . Server == types . BroadcastServer {
chatJID = messageSender
messageSender = portal . Key . JID
}
err := sender . Client . MarkRead ( ids , receiptTimestamp , chatJID , messageSender )
2021-11-30 15:38:37 +01:00
if err != nil {
portal . log . Warnfln ( "Failed to mark %v as read by %s: %v" , ids , sender . JID , err )
}
}
}
2021-12-07 15:02:51 +01:00
func typingDiff ( prev , new [ ] id . UserID ) ( started , stopped [ ] id . UserID ) {
OuterNew :
for _ , userID := range new {
for _ , previousUserID := range prev {
if userID == previousUserID {
continue OuterNew
}
}
started = append ( started , userID )
}
OuterPrev :
for _ , userID := range prev {
for _ , previousUserID := range new {
if userID == previousUserID {
continue OuterPrev
}
}
stopped = append ( stopped , userID )
}
return
}
func ( portal * Portal ) setTyping ( userIDs [ ] id . UserID , state types . ChatPresence ) {
for _ , userID := range userIDs {
user := portal . bridge . GetUserByMXIDIfExists ( userID )
if user == nil || ! user . IsLoggedIn ( ) {
continue
}
portal . log . Debugfln ( "Bridging typing change from %s to chat presence %s" , state , user . MXID )
2022-03-09 15:44:25 +01:00
err := user . Client . SendChatPresence ( portal . Key . JID , state , types . ChatPresenceMediaText )
2021-12-07 15:02:51 +01:00
if err != nil {
portal . log . Warnln ( "Error sending chat presence:" , err )
}
2022-01-01 03:29:39 +01:00
if portal . bridge . Config . Bridge . SendPresenceOnTyping {
err = user . Client . SendPresence ( types . PresenceAvailable )
if err != nil {
user . log . Warnln ( "Failed to set presence:" , err )
}
}
2021-12-07 15:02:51 +01:00
}
}
func ( portal * Portal ) HandleMatrixTyping ( newTyping [ ] id . UserID ) {
portal . currentlyTypingLock . Lock ( )
defer portal . currentlyTypingLock . Unlock ( )
startedTyping , stoppedTyping := typingDiff ( portal . currentlyTyping , newTyping )
portal . currentlyTyping = newTyping
portal . setTyping ( startedTyping , types . ChatPresenceComposing )
portal . setTyping ( stoppedTyping , types . ChatPresencePaused )
}
2022-05-31 16:28:58 +02:00
func ( portal * Portal ) canBridgeFrom ( sender * User , allowRelay bool ) error {
2021-11-01 15:46:03 +01:00
if ! sender . IsLoggedIn ( ) {
2022-05-31 16:28:58 +02:00
if allowRelay && portal . HasRelaybot ( ) {
return nil
2021-11-01 15:46:03 +01:00
} else if sender . Session != nil {
2022-05-31 16:28:58 +02:00
return errUserNotConnected
2021-11-01 15:46:03 +01:00
} else {
2022-05-31 16:28:58 +02:00
return errUserNotLoggedIn
2021-11-01 15:46:03 +01:00
}
2022-05-31 16:28:58 +02:00
} else if portal . IsPrivateChat ( ) && sender . JID . User != portal . Key . Receiver . User && ( ! allowRelay || ! portal . HasRelaybot ( ) ) {
return errDifferentUser
2021-11-01 15:46:03 +01:00
}
2022-05-31 16:28:58 +02:00
return nil
2021-11-01 15:46:03 +01:00
}
2022-12-03 15:48:38 +01:00
func ( portal * Portal ) resetChildSpaceStatus ( ) {
for _ , childPortal := range portal . bridge . portalsByJID {
if childPortal . ParentGroup == portal . Key . JID {
if portal . MXID != "" && childPortal . InSpace {
go childPortal . removeSpaceParentEvent ( portal . MXID )
}
childPortal . InSpace = false
if childPortal . parentPortal == portal {
childPortal . parentPortal = nil
}
}
}
}
2019-05-16 19:14:32 +02:00
func ( portal * Portal ) Delete ( ) {
portal . Portal . Delete ( )
2020-05-28 19:35:43 +02:00
portal . bridge . portalsLock . Lock ( )
2019-05-16 19:14:32 +02:00
delete ( portal . bridge . portalsByJID , portal . Key )
if len ( portal . MXID ) > 0 {
delete ( portal . bridge . portalsByMXID , portal . MXID )
}
2022-12-03 15:48:38 +01:00
portal . resetChildSpaceStatus ( )
2020-05-28 19:35:43 +02:00
portal . bridge . portalsLock . Unlock ( )
2019-05-16 19:14:32 +02:00
}
2020-06-25 22:33:11 +02:00
func ( portal * Portal ) GetMatrixUsers ( ) ( [ ] id . UserID , error ) {
members , err := portal . MainIntent ( ) . JoinedMembers ( portal . MXID )
if err != nil {
2020-10-05 21:38:34 +02:00
return nil , fmt . Errorf ( "failed to get member list: %w" , err )
2020-06-25 22:33:11 +02:00
}
var users [ ] id . UserID
for userID := range members . Joined {
_ , isPuppet := portal . bridge . ParsePuppetMXID ( userID )
if ! isPuppet && userID != portal . bridge . Bot . UserID {
users = append ( users , userID )
}
}
return users , nil
}
func ( portal * Portal ) CleanupIfEmpty ( ) {
users , err := portal . GetMatrixUsers ( )
if err != nil {
portal . log . Errorfln ( "Failed to get Matrix user list to determine if portal needs to be cleaned up: %v" , err )
return
}
if len ( users ) == 0 {
portal . log . Infoln ( "Room seems to be empty, cleaning up..." )
portal . Delete ( )
portal . Cleanup ( false )
}
}
2019-05-16 19:14:32 +02:00
func ( portal * Portal ) Cleanup ( puppetsOnly bool ) {
if len ( portal . MXID ) == 0 {
return
}
intent := portal . MainIntent ( )
2023-01-04 11:48:16 +01:00
if portal . bridge . SpecVersions . UnstableFeatures [ "com.beeper.room_yeeting" ] {
err := intent . BeeperDeleteRoom ( portal . MXID )
if err == nil || errors . Is ( err , mautrix . MNotFound ) {
return
}
portal . log . Warnfln ( "Failed to delete %s using hungryserv yeet endpoint, falling back to normal behavior: %v" , portal . MXID , err )
}
2019-05-16 19:14:32 +02:00
members , err := intent . JoinedMembers ( portal . MXID )
if err != nil {
portal . log . Errorln ( "Failed to get portal members for cleanup:" , err )
return
}
2020-09-24 14:25:36 +02:00
for member := range members . Joined {
2019-05-21 22:44:14 +02:00
if member == intent . UserID {
continue
}
2019-05-16 19:14:32 +02:00
puppet := portal . bridge . GetPuppetByMXID ( member )
if puppet != nil {
2019-05-24 01:33:26 +02:00
_ , err = puppet . DefaultIntent ( ) . LeaveRoom ( portal . MXID )
2019-05-21 20:06:27 +02:00
if err != nil {
portal . log . Errorln ( "Error leaving as puppet while cleaning up portal:" , err )
}
2019-05-16 19:14:32 +02:00
} else if ! puppetsOnly {
_ , err = intent . KickUser ( portal . MXID , & mautrix . ReqKickUser { UserID : member , Reason : "Deleting portal" } )
2019-05-21 20:06:27 +02:00
if err != nil {
portal . log . Errorln ( "Error kicking user while cleaning up portal:" , err )
}
2019-05-16 19:14:32 +02:00
}
}
2019-05-21 22:44:14 +02:00
_ , err = intent . LeaveRoom ( portal . MXID )
if err != nil {
portal . log . Errorln ( "Error leaving with main intent while cleaning up portal:" , err )
}
2019-05-16 19:14:32 +02:00
}
2022-05-22 15:15:54 +02:00
func ( portal * Portal ) HandleMatrixLeave ( brSender bridge . User ) {
sender := brSender . ( * User )
2019-05-16 19:14:32 +02:00
if portal . IsPrivateChat ( ) {
portal . log . Debugln ( "User left private chat portal, cleaning up and deleting..." )
portal . Delete ( )
portal . Cleanup ( false )
return
2021-02-10 21:15:23 +01:00
} else if portal . bridge . Config . Bridge . BridgeMatrixLeave {
2021-11-05 11:17:56 +01:00
err := sender . Client . LeaveGroup ( portal . Key . JID )
if err != nil {
portal . log . Errorfln ( "Failed to leave group as %s: %v" , sender . MXID , err )
return
}
2021-10-22 19:14:34 +02:00
//portal.log.Infoln("Leave response:", <-resp)
2019-05-16 19:14:32 +02:00
}
2021-04-16 15:36:56 +02:00
portal . CleanupIfEmpty ( )
2019-05-16 19:14:32 +02:00
}
2022-05-22 15:15:54 +02:00
func ( portal * Portal ) HandleMatrixKick ( brSender bridge . User , brTarget bridge . Ghost ) {
sender := brSender . ( * User )
target := brTarget . ( * Puppet )
2022-01-15 12:59:20 +01:00
_ , err := sender . Client . UpdateGroupParticipants ( portal . Key . JID , map [ types . JID ] whatsmeow . ParticipantChange {
target . JID : whatsmeow . ParticipantChangeRemove ,
} )
if err != nil {
portal . log . Errorfln ( "Failed to kick %s from group as %s: %v" , target . JID , sender . MXID , err )
return
2020-06-25 22:33:11 +02:00
}
2022-01-15 12:59:20 +01:00
//portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp)
2019-05-16 19:14:32 +02:00
}
2020-06-25 22:58:35 +02:00
2022-05-22 15:15:54 +02:00
func ( portal * Portal ) HandleMatrixInvite ( brSender bridge . User , brTarget bridge . Ghost ) {
sender := brSender . ( * User )
target := brTarget . ( * Puppet )
2022-01-15 12:59:20 +01:00
_ , err := sender . Client . UpdateGroupParticipants ( portal . Key . JID , map [ types . JID ] whatsmeow . ParticipantChange {
target . JID : whatsmeow . ParticipantChangeAdd ,
} )
if err != nil {
portal . log . Errorfln ( "Failed to add %s to group as %s: %v" , target . JID , sender . MXID , err )
return
2020-06-25 22:58:35 +02:00
}
2022-01-15 12:59:20 +01:00
//portal.log.Infofln("Add %s response: %s", puppet.JID, <-resp)
2020-06-25 22:58:35 +02:00
}
2021-02-09 22:41:14 +01:00
2022-05-22 15:15:54 +02:00
func ( portal * Portal ) HandleMatrixMeta ( brSender bridge . User , evt * event . Event ) {
sender := brSender . ( * User )
if ! sender . Whitelisted || ! sender . IsLoggedIn ( ) {
return
}
2021-02-09 22:41:14 +01:00
switch content := evt . Content . Parsed . ( type ) {
case * event . RoomNameEventContent :
if content . Name == portal . Name {
return
}
2021-11-05 11:17:56 +01:00
portal . Name = content . Name
2022-02-10 11:46:25 +01:00
err := sender . Client . SetGroupName ( portal . Key . JID , content . Name )
if err != nil {
portal . log . Errorln ( "Failed to update group name:" , err )
}
2021-02-09 22:41:14 +01:00
case * event . TopicEventContent :
if content . Topic == portal . Topic {
return
}
2021-11-05 11:17:56 +01:00
portal . Topic = content . Topic
2022-02-10 11:46:25 +01:00
err := sender . Client . SetGroupTopic ( portal . Key . JID , "" , "" , content . Topic )
if err != nil {
portal . log . Errorln ( "Failed to update group description:" , err )
}
2021-02-09 22:41:14 +01:00
case * event . RoomAvatarEventContent :
2022-02-10 11:46:25 +01:00
portal . avatarLock . Lock ( )
defer portal . avatarLock . Unlock ( )
if content . URL == portal . AvatarURL || ( content . URL . IsEmpty ( ) && portal . Avatar == "remove" ) {
return
}
var data [ ] byte
var err error
if ! content . URL . IsEmpty ( ) {
data , err = portal . MainIntent ( ) . DownloadBytes ( content . URL )
if err != nil {
portal . log . Errorfln ( "Failed to download updated avatar %s: %v" , content . URL , err )
return
}
portal . log . Debugfln ( "%s set the group avatar to %s" , sender . MXID , content . URL )
} else {
portal . log . Debugfln ( "%s removed the group avatar" , sender . MXID )
}
newID , err := sender . Client . SetGroupPhoto ( portal . Key . JID , data )
if err != nil {
portal . log . Errorfln ( "Failed to update group avatar: %v" , err )
return
}
portal . log . Debugfln ( "Successfully updated group avatar to %s" , newID )
portal . Avatar = newID
portal . AvatarURL = content . URL
portal . UpdateBridgeInfo ( )
2022-05-13 01:56:40 +02:00
portal . Update ( nil )
2021-02-09 22:41:14 +01:00
}
}