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-05-13 01:56:40 +02:00
"database/sql"
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"
2019-11-10 20:22:11 +01:00
"html"
2018-08-24 18:46:14 +02:00
"image"
2021-11-11 19:33:22 +01:00
_ "image/gif"
2018-08-25 23:26:24 +02:00
"image/jpeg"
"image/png"
2020-06-10 13:58:57 +02:00
"math"
2018-08-24 18:46:14 +02:00
"mime"
"net/http"
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-02-10 18:18:49 +01:00
"github.com/tidwall/gjson"
2021-11-11 19:33:22 +01:00
"golang.org/x/image/draw"
2021-10-06 20:11:37 +02:00
"golang.org/x/image/webp"
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"
"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-05-22 15:15:54 +02:00
portal . matrixMessages <- PortalMatrixMessage { user : user . ( * User ) , evt : evt }
}
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
}
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-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 {
evt * event . Event
user * User
}
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
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
2021-10-28 12:57:15 +02:00
relayUser * User
2018-08-30 23:13:08 +02:00
}
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 ) {
2022-05-22 15:15:54 +02:00
portal . handleMatrixReadReceipt ( msg . user , "" , time . UnixMilli ( msg . evt . Timestamp ) , false )
2021-12-14 16:47:30 +01:00
switch msg . evt . Type {
case event . EventMessage , event . EventSticker :
portal . HandleMatrixMessage ( msg . user , msg . evt )
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 ||
waMsg . ListMessage != nil || waMsg . ListResponseMessage != 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"
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-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 ( ) )
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"
} else {
msg := fmt . Sprintf ( "Set the disappearing message timer to %s" , formatDuration ( time . Duration ( portal . ExpirationTime ) * time . Second ) )
if ! portal . bridge . Config . Bridge . DisappearingMessagesInGroups && portal . IsGroupChat ( ) {
msg += ". However, this bridge is not configured to disappear messages in group chats."
}
return msg
}
}
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
} )
2021-10-27 17:31:33 +02:00
intent := portal . getMessageIntent ( source , & evt . Info )
2022-05-19 11:08:30 +02:00
if intent == nil {
return
} else if ! intent . IsCustomPuppet && portal . IsPrivateChat ( ) && evt . Info . Sender . User == portal . Key . Receiver . User {
2021-11-06 13:20:56 +01:00
portal . log . Debugfln ( "Not handling %s (undecryptable): user doesn't have double puppeting enabled" , evt . Info . ID )
return
}
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 {
portal . log . Errorln ( "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 )
2021-11-06 13:20:56 +01:00
if ! intent . IsCustomPuppet && portal . IsPrivateChat ( ) && msg . Sender . User == portal . Key . Receiver . User {
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
}
}
2021-10-26 16:01:10 +02:00
intent := portal . getMessageIntent ( source , & evt . Info )
2022-05-19 11:08:30 +02:00
if intent == nil {
return
} else if ! intent . IsCustomPuppet && portal . IsPrivateChat ( ) && evt . Info . Sender . User == portal . Key . Receiver . User {
2021-11-06 13:20:56 +01:00
portal . log . Debugfln ( "Not handling %s (%s): user doesn't have double puppeting enabled" , msgID , msgType )
return
}
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 {
2022-01-07 13:32:00 +01:00
portal . MarkDisappearing ( existingMsg . MXID , converted . ExpiresIn , false )
2021-10-27 17:31:33 +02:00
converted . Content . SetEdit ( existingMsg . MXID )
2021-11-05 10:47:51 +01:00
} else if len ( converted . ReplyTo ) > 0 {
portal . SetReply ( converted . Content , converted . ReplyTo )
2021-10-27 17:31:33 +02:00
}
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-01-07 13:32:00 +01:00
portal . MarkDisappearing ( resp . EventID , converted . ExpiresIn , false )
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
if converted . Caption != nil && existingMsg == 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 {
2022-01-07 13:32:00 +01:00
portal . MarkDisappearing ( resp . EventID , converted . ExpiresIn , false )
2022-05-23 09:18:00 +02:00
lastEventID = resp . EventID
2021-10-26 16:01:10 +02:00
}
}
2022-01-03 15:11:39 +01:00
if converted . MultiEvent != nil && existingMsg == nil {
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 {
portal . MarkDisappearing ( resp . EventID , converted . ExpiresIn , false )
2022-05-23 09:18:00 +02:00
lastEventID = resp . EventID
2022-01-03 15:11:39 +01:00
}
}
}
2022-05-23 09:18:00 +02:00
if source . MXID == intent . UserID {
// 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-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-03-05 20:22:31 +01:00
portal . finishHandling ( existingMsg , & evt . Info , eventID , database . MsgNormal , converted . Error )
2021-10-26 16:01:10 +02:00
}
2022-03-05 20:22:31 +01:00
} else if msgType == "reaction" {
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-05-13 01:56:40 +02:00
func ( portal * Portal ) markHandled ( txn * sql . Tx , 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 )
2021-10-26 16:01:10 +02:00
} else {
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
}
2021-10-22 19:14:34 +02:00
func ( portal * Portal ) getMessageIntent ( user * User , info * types . MessageInfo ) * appservice . IntentAPI {
2022-05-19 11:08:30 +02:00
puppet := portal . getMessagePuppet ( user , info )
if puppet == nil {
return nil
}
return puppet . IntentFor ( portal )
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)
//}
func ( portal * Portal ) SyncParticipants ( source * User , metadata * types . GroupInfo ) {
2018-08-26 15:11:48 +02:00
changed := false
levels , err := portal . MainIntent ( ) . PowerLevels ( portal . MXID )
if err != nil {
levels = portal . GetBasePowerLevels ( )
changed = true
}
2022-05-12 09:45:23 +02:00
changed = portal . applyPowerLevelFixes ( levels ) || changed
2021-10-22 19:14:34 +02:00
participantMap := make ( map [ types . JID ] bool )
2018-08-23 00:12:26 +02:00
for _ , participant := range metadata . Participants {
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 )
2022-05-13 10:34:45 +02:00
puppet . SyncContact ( source , true , false , "group participant" )
2021-10-28 12:57:15 +02:00
user := portal . bridge . GetUserByJID ( participant . JID )
2022-01-17 15:26:26 +01:00
if user != nil && user != source {
2021-10-28 12:57:15 +02:00
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 )
}
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 {
changed = levels . EnsureUserLevel ( user . MXID , expectedLevel ) || changed
2018-08-26 15:11:48 +02:00
}
}
if changed {
2018-08-30 00:10:26 +02:00
_ , err = portal . MainIntent ( ) . SetPowerLevels ( portal . MXID , levels )
if err != nil {
portal . log . Errorln ( "Failed to change power levels:" , err )
}
2018-08-23 00:12:26 +02:00
}
2021-02-21 13:45:33 +01:00
portal . kickExtraUsers ( participantMap )
2018-08-23 00:12:26 +02:00
}
2018-08-19 17:21:38 +02:00
2022-06-28 15:22:10 +02:00
func ( user * User ) updateAvatar ( jid types . JID , avatarID * string , avatarURL * id . ContentURI , avatarSet * bool , log log . Logger , intent * appservice . IntentAPI ) bool {
currentID := ""
if * avatarSet && * avatarID != "remove" && * avatarID != "unauthorized" {
currentID = * avatarID
}
avatar , err := user . Client . GetProfilePictureInfo ( jid , false , currentID )
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 ) {
* avatarURL = id . ContentURI { }
2021-10-22 19:14:34 +02:00
avatar = & types . ProfilePictureInfo { ID : "remove" }
2022-06-28 15:22:10 +02:00
// Fall through to the rest of the avatar handling code
} 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 ( )
changed := user . updateAvatar ( portal . Key . JID , & portal . Avatar , & portal . AvatarURL , & portal . AvatarSet , portal . log , portal . MainIntent ( ) )
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 )
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
}
2022-07-05 10:05:22 +02:00
if portal . Name != name || ( ! portal . NameSet && len ( portal . MXID ) > 0 ) {
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
if len ( portal . MXID ) > 0 {
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 ( )
}
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
}
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-01-07 14:05:09 +01:00
if portal . ExpirationTime != groupInfo . DisappearingTimer {
update = true
portal . ExpirationTime = groupInfo . DisappearingTimer
}
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
}
2020-05-08 21:32:22 +02:00
func ( portal * Portal ) ensureMXIDInvited ( mxid id . UserID ) {
2019-11-10 20:22:11 +01:00
err := portal . MainIntent ( ) . EnsureInvited ( portal . MXID , mxid )
2019-05-30 16:22:03 +02:00
if err != nil {
2019-11-10 20:22:11 +01:00
portal . log . Warnfln ( "Failed to ensure %s is invited to %s: %v" , mxid , portal . MXID , err )
2019-05-30 16:22:03 +02:00
}
2019-11-10 20:22:11 +01:00
}
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 )
2021-12-29 20:40:08 +01:00
go portal . addToSpace ( 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 ( )
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 {
2020-05-08 21:32:22 +02:00
event . StateRoomName . Type : anyone ,
event . StateRoomAvatar . Type : anyone ,
event . StateTopic . Type : anyone ,
2022-05-12 09:45:23 +02:00
event . EventReaction . Type : anyone ,
event . EventRedaction . 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
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-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-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" )
2019-06-01 19:03:29 +02:00
if portal . bridge . Config . Bridge . PrivateChatPortalMeta {
portal . Name = puppet . Displayname
portal . AvatarURL = puppet . AvatarURL
portal . Avatar = puppet . Avatar
} else {
portal . Name = ""
}
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
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
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 {
Parsed : portal . GetBasePowerLevels ( ) ,
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
} }
2020-05-08 21:32:22 +02:00
if ! portal . AvatarURL . IsEmpty ( ) {
initialState = append ( initialState , & event . Event {
Type : event . StateRoomAvatar ,
Content : event . Content {
Parsed : event . RoomAvatarEventContent { URL : portal . AvatarURL } ,
2018-08-26 15:11:48 +02:00
} ,
2019-05-22 22:27:58 +02:00
} )
2022-06-28 13:37:49 +02:00
portal . AvatarSet = true
2019-05-22 22:27:58 +02:00
}
2021-08-18 15:24:13 +02:00
var invite [ ] id . UserID
2019-11-10 20:22:11 +01:00
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 )
}
}
2021-11-01 10:17:44 +01:00
creationContent := make ( map [ string ] interface { } )
if ! portal . bridge . Config . Bridge . FederateRooms {
creationContent [ "m.federate" ] = false
}
2019-05-22 22:27:58 +02:00
resp , err := intent . CreateRoom ( & 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 ,
2018-08-18 21:57:08 +02:00
} )
if err != nil {
return err
}
2022-06-28 13:37:49 +02:00
portal . NameSet = len ( portal . Name ) > 0
portal . TopicSet = len ( portal . 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 )
portal . log . Infoln ( "Matrix room created:" , portal . MXID )
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.
2021-08-18 15:24:13 +02:00
for _ , userID := range invite {
portal . bridge . StateStore . SetMembership ( portal . MXID , userID , event . MembershipInvite )
2020-05-12 21:25:55 +02:00
}
2021-08-18 15:24:13 +02:00
portal . ensureUserInvited ( user )
2021-10-28 11:59:22 +02:00
user . syncChatDoublePuppetDetails ( portal , true )
2021-08-18 15:24:13 +02:00
2021-12-29 20:40:08 +01:00
go portal . addToSpace ( user )
2021-12-28 21:14:09 +01:00
2021-10-31 18:59:23 +01:00
if groupInfo != nil {
2022-04-22 15:45:30 +02:00
if groupInfo . IsEphemeral {
portal . ExpirationTime = groupInfo . DisappearingTimer
2022-05-13 01:56:40 +02:00
portal . Update ( nil )
2022-04-22 15:45:30 +02:00
}
2021-10-31 18:59:23 +01:00
portal . SyncParticipants ( user , groupInfo )
if groupInfo . IsAnnounce {
portal . RestrictMessageSending ( groupInfo . IsAnnounce )
2021-10-22 19:14:34 +02:00
}
2021-10-31 18:59:23 +01:00
if groupInfo . IsLocked {
portal . RestrictMetadataChanges ( groupInfo . IsLocked )
2020-10-12 12:59:14 +02:00
}
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 } } )
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 }
user . EnqueueImmedateBackfills ( portals )
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
}
2021-12-29 20:40:08 +01:00
func ( portal * Portal ) addToSpace ( user * User ) {
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
}
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
}
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
}
2021-11-05 10:47:51 +01:00
func ( portal * Portal ) SetReply ( content * event . MessageEventContent , replyToID types . MessageID ) bool {
2021-10-22 19:14:34 +02:00
if len ( replyToID ) == 0 {
2021-11-05 10:47:51 +01:00
return false
2018-08-23 23:52:06 +02:00
}
2021-10-22 19:14:34 +02:00
message := portal . bridge . DB . Message . GetByJID ( portal . Key , replyToID )
2021-11-05 10:47:51 +01:00
if message == nil || message . IsFakeMXID ( ) {
return false
}
evt , err := portal . MainIntent ( ) . GetEvent ( portal . MXID , message . MXID )
if err != nil {
portal . log . Warnln ( "Failed to get reply target:" , err )
2022-05-29 18:22:07 +02:00
content . RelatesTo = ( & event . RelatesTo { } ) . SetReplyTo ( message . MXID )
2021-11-05 10:47:51 +01:00
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 )
portal . upsertReaction ( intent , target . JID , info . Sender , resp . EventID , info . ID )
}
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
}
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 )
}
}
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-01-07 13:32:00 +01:00
ReplyTo types . MessageID
ExpiresIn uint32
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 )
2022-02-15 12:15:41 +01:00
replyTo := contextInfo . GetStanzaId ( )
expiresIn := contextInfo . GetExpiration ( )
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 ,
ReplyTo : replyTo ,
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 ,
} ,
ReplyTo : tplMsg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : tplMsg . GetContextInfo ( ) . GetExpiration ( ) ,
}
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 ) {
case * waProto . HydratedFourRowTemplate_DocumentMessage :
convertedTitle = portal . convertMediaMessage ( intent , source , info , title . DocumentMessage , "file attachment" , false )
case * waProto . HydratedFourRowTemplate_ImageMessage :
convertedTitle = portal . convertMediaMessage ( intent , source , info , title . ImageMessage , "photo" , false )
case * waProto . HydratedFourRowTemplate_VideoMessage :
convertedTitle = portal . convertMediaMessage ( intent , source , info , title . VideoMessage , "video attachment" , false )
case * waProto . HydratedFourRowTemplate_LocationMessage :
content = fmt . Sprintf ( "Unsupported location message\n\n%s" , content )
case * waProto . HydratedFourRowTemplate_HydratedTitleText :
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 ( ) ,
} ,
} ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
}
}
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 ,
} ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
}
body := msg . GetDescription ( )
if msg . GetTitle ( ) != "" {
if body == "" {
body = msg . GetTitle ( )
} else {
body = fmt . Sprintf ( "%s\n\n%s" , msg . GetTitle ( ) , body )
}
}
randomID := appservice . RandomString ( 64 )
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 ( ) ,
} ,
} ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
}
}
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 ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
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 ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
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 )
2021-10-31 19:47:30 +01:00
htmlMessage := fmt . Sprintf ( inviteMsg , html . EscapeString ( 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 ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
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 ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
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 ) ,
} ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
2022-01-07 13:32:00 +01:00
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
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 } )
}
}
}
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-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 {
2022-05-14 13:28:55 +02:00
if errors . Is ( bridgeErr , whatsmeow . ErrMediaDownloadFailedWith404 ) || errors . Is ( bridgeErr , whatsmeow . ErrMediaDownloadFailedWith410 ) {
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-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 ,
}
}
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 ,
}
}
}
2021-10-22 19:14:34 +02:00
_ , isSticker := msg . ( * waProto . StickerMessage )
switch strings . ToLower ( strings . Split ( msg . GetMimetype ( ) , "/" ) [ 0 ] ) {
2018-08-23 23:52:06 +02:00
case "image" :
2021-10-22 19:14:34 +02:00
if ! isSticker {
2020-05-08 21:32:22 +02:00
content . MsgType = event . MsgImage
2019-10-04 20:01:53 +02:00
}
2018-08-23 23:52:06 +02:00
case "video" :
2020-05-08 21:32:22 +02:00
content . MsgType = event . MsgVideo
2018-08-23 23:52:06 +02:00
case "audio" :
2020-05-08 21:32:22 +02:00
content . MsgType = event . MsgAudio
2018-08-23 23:52:06 +02:00
default :
2020-05-08 21:32:22 +02:00
content . MsgType = event . MsgFile
2018-08-23 23:52:06 +02:00
}
2020-05-08 21:32:22 +02:00
eventType := event . EventMessage
2021-10-22 19:14:34 +02:00
if isSticker {
2020-05-08 21:32:22 +02:00
eventType = event . EventSticker
2019-10-04 20:01:53 +02:00
}
2018-08-27 22:15:05 +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 ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
ExpiresIn : msg . GetContextInfo ( ) . GetExpiration ( ) ,
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
}
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 )
data , err := source . Client . Download ( msg )
if errors . Is ( err , whatsmeow . ErrMediaDownloadFailedWith404 ) || errors . Is ( err , whatsmeow . ErrMediaDownloadFailedWith410 ) {
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-05-02 14:00:57 +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 {
errorName := waProto . MediaRetryNotification_MediaRetryNotificationResultType_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-02-04 21:19:55 +01:00
func createJPEGThumbnailAndGetSize ( source [ ] byte ) ( [ ] 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
2021-11-11 19:33:22 +01:00
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
}
func createJPEGThumbnail ( source [ ] byte ) ( [ ] byte , error ) {
data , _ , _ , err := createJPEGThumbnailAndGetSize ( source )
return data , err
2021-11-11 19:33:22 +01:00
}
2022-06-29 19:05:55 +02:00
func ( portal * Portal ) downloadThumbnail ( ctx context . Context , original [ ] byte , thumbnailURL id . ContentURIString , eventID id . EventID ) ( [ ] 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 {
return createJPEGThumbnail ( thumbnail )
2018-08-25 23:26:24 +02:00
}
2021-11-11 19:33:22 +01:00
return createJPEGThumbnail ( original )
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
if err := png . Encode ( & pngBuffer , webpDecoded ) ; err != nil {
return nil , fmt . Errorf ( "failed to encode png image: %w" , err )
}
return pngBuffer . Bytes ( ) , nil
}
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 ) {
2019-11-10 20:22:11 +01:00
var caption string
2021-10-22 19:14:34 +02:00
var mentionedJIDs [ ] string
2019-11-10 20:22:11 +01:00
if relaybotFormatted {
2020-07-31 13:30:58 +02:00
caption , mentionedJIDs = portal . bridge . Formatter . ParseMatrix ( content . FormattedBody )
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
}
}
2021-10-22 19:14:34 +02:00
if mediaType == whatsmeow . MediaVideo && content . GetInfo ( ) . MimeType == "image/gif" {
2022-06-29 19:05:55 +02:00
data , err = ffmpeg . ConvertBytes ( ctx , data , ".mp4" , [ ] string { "-f" , "gif" } , [ ] string {
2022-01-04 01:02:06 +01:00
"-pix_fmt" , "yuv420p" , "-c:v" , "libx264" , "-movflags" , "+faststart" ,
"-filter:v" , "crop='floor(in_w/2)*2:floor(in_h/2)*2'" ,
} , content . GetInfo ( ) . MimeType )
2020-06-23 15:36:05 +02:00
if err != nil {
2022-06-21 20:56:08 +02:00
return nil , util . NewDualError ( fmt . Errorf ( "%w (gif to mp4)" , errMediaConvertFailed ) , err )
2020-06-23 15:36:05 +02:00
}
content . Info . MimeType = "video/mp4"
}
2021-10-22 19:14:34 +02:00
if mediaType == whatsmeow . MediaImage && content . GetInfo ( ) . MimeType == "image/webp" {
2021-10-06 20:11:37 +02:00
data , err = portal . convertWebPtoPNG ( data )
if err != nil {
2022-06-21 20:56:08 +02:00
return nil , util . NewDualError ( fmt . Errorf ( "%w (webp to png)" , errMediaConvertFailed ) , err )
2021-10-06 20:11:37 +02:00
}
content . Info . MimeType = "image/png"
}
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-06-29 19:05:55 +02:00
thumbnail , err = portal . downloadThumbnail ( ctx , data , content . GetInfo ( ) . ThumbnailURL , eventID )
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 ,
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
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
}
2020-05-08 21:32:22 +02:00
func ( portal * Portal ) addRelaybotFormat ( sender * User , content * event . MessageEventContent ) bool {
member := portal . MainIntent ( ) . Member ( portal . MXID , sender . MXID )
2021-11-05 19:08:49 +01:00
if member == nil {
member = & event . MemberEventContent { }
2019-11-10 20:22:11 +01:00
}
2020-05-08 21:32:22 +02:00
if content . Format != event . FormatHTML {
content . FormattedBody = strings . Replace ( html . EscapeString ( content . Body ) , "\n" , "<br/>" , - 1 )
content . Format = event . FormatHTML
2019-11-10 20:22:11 +01:00
}
2021-11-05 19:08:49 +01:00
data , err := portal . bridge . Config . Bridge . Relay . FormatMessage ( content , sender . MXID , * 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-06-29 19:05:55 +02:00
func ( portal * Portal ) convertMatrixMessage ( ctx context . Context , sender * User , evt * event . Event ) ( * waProto . Message , * User , error ) {
2020-05-25 22:11:00 +02:00
content , ok := evt . Content . Parsed . ( * event . MessageEventContent )
if ! ok {
2022-05-31 16:28:58 +02:00
return nil , sender , fmt . Errorf ( "%w %T" , errUnexpectedParsedContentType , evt . Content . Parsed )
2020-05-08 21:32:22 +02:00
}
2018-08-28 23:40:54 +02:00
2021-10-22 19:14:34 +02:00
var msg waProto . Message
var ctxInfo waProto . ContextInfo
2020-05-08 21:32:22 +02:00
replyToID := content . 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-03-05 20:22:31 +01:00
if replyToMsg != nil && ! replyToMsg . IsFakeJID ( ) && replyToMsg . Type == database . MsgNormal {
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 )
}
2019-11-10 20:22:11 +01:00
relaybotFormatted := false
2021-12-09 14:32:11 +01:00
if ! sender . IsLoggedIn ( ) || ( portal . IsPrivateChat ( ) && sender . JID . User != portal . Key . Receiver . User ) {
2019-11-10 20:22:11 +01:00
if ! portal . HasRelaybot ( ) {
2022-05-31 16:28:58 +02:00
return nil , sender , errUserNotLoggedIn
2019-11-10 20:22:11 +01:00
}
2021-10-28 12:57:15 +02:00
relaybotFormatted = portal . addRelaybotFormat ( sender , content )
sender = portal . GetRelayUser ( )
2019-11-10 20:22:11 +01:00
}
2020-05-08 21:32:22 +02:00
if evt . Type == event . EventSticker {
content . MsgType = event . MsgImage
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
}
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 {
2022-05-31 16:28:58 +02:00
return nil , sender , errMNoticeDisabled
2021-02-26 15:10:57 +01:00
}
2020-05-08 21:32:22 +02:00
if content . Format == event . FormatHTML {
2020-07-31 13:30:58 +02:00
text , ctxInfo . MentionedJid = portal . bridge . Formatter . ParseMatrix ( content . FormattedBody )
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 ,
ContextInfo : & ctxInfo ,
}
2022-06-29 19:05:55 +02:00
hasPreview := portal . convertURLPreviewToWhatsApp ( ctx , sender , evt , msg . ExtendedTextMessage )
if ctx . Err ( ) != nil {
return nil , nil , ctx . Err ( )
}
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 {
2022-05-31 16:28:58 +02:00
return nil , sender , 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 {
ContextInfo : & ctxInfo ,
2018-08-25 23:26:24 +02:00
Caption : & media . Caption ,
JpegThumbnail : media . Thumbnail ,
Url : & media . URL ,
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
}
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 {
2022-05-31 16:28:58 +02:00
return nil , sender , 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 {
ContextInfo : & ctxInfo ,
2018-08-25 23:26:24 +02:00
Caption : & media . Caption ,
JpegThumbnail : media . Thumbnail ,
Url : & media . URL ,
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 {
2022-05-31 16:28:58 +02:00
return nil , sender , 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 {
ContextInfo : & ctxInfo ,
2018-08-25 23:26:24 +02:00
Url : & media . URL ,
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 {
2022-05-31 16:28:58 +02:00
return nil , sender , err
2018-08-24 21:31:18 +02:00
}
2021-10-22 19:14:34 +02:00
msg . DocumentMessage = & waProto . DocumentMessage {
ContextInfo : & ctxInfo ,
2021-11-11 19:33:22 +01:00
JpegThumbnail : media . Thumbnail ,
2018-08-25 23:26:24 +02:00
Url : & media . URL ,
2020-11-02 16:18:18 +01:00
Title : & content . Body ,
2020-05-08 21:32:22 +02:00
FileName : & content . Body ,
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
}
2021-08-02 11:53:38 +02:00
case event . MsgLocation :
lat , long , err := parseGeoURI ( content . GeoURI )
if err != nil {
2022-05-31 16:28:58 +02:00
return nil , sender , 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 ,
2021-10-22 19:14:34 +02:00
ContextInfo : & ctxInfo ,
2021-08-02 11:53:38 +02:00
}
2018-08-19 17:21:38 +02:00
default :
2022-05-31 16:28:58 +02:00
return nil , sender , fmt . Errorf ( "%w %q" , errUnknownMsgType , content . MsgType )
2020-05-24 16:28:30 +02:00
}
2022-05-31 16:28:58 +02:00
return & msg , sender , 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 ,
} ,
}
}
2020-05-24 16:28:30 +02:00
func ( portal * Portal ) HandleMatrixMessage ( 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 )
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-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , errBroadcastSendDisabled , "Ignoring" , nil )
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-01 10:17:10 +02:00
messageAge := time . Since ( time . UnixMilli ( evt . Timestamp ) )
2022-06-30 13:41:37 +02:00
ms := metricSender { portal : portal }
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 )
}
if portal . bridge . Config . Bridge . MessageHandlingTimeout . ErrorAfter > 0 {
remainingTime := portal . bridge . Config . Bridge . MessageHandlingTimeout . ErrorAfter - messageAge
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 {
portal . log . Warnfln ( "Message %s was delayed before reaching the bridge, only have %s (of %s timeout) until delay warning" , evt . ID , remainingTime , portal . bridge . Config . Bridge . MessageHandlingTimeout . 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-06-30 13:41:37 +02:00
if portal . bridge . Config . Bridge . MessageHandlingTimeout . Deadline > 0 {
2022-06-29 19:05:55 +02:00
var cancel context . CancelFunc
2022-06-30 13:41:37 +02:00
ctx , cancel = context . WithTimeout ( ctx , portal . bridge . Config . Bridge . MessageHandlingTimeout . Deadline )
2022-06-29 19:05:55 +02:00
defer cancel ( )
}
msg , sender , err := portal . convertMatrixMessage ( ctx , sender , evt )
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-06-29 19:05:55 +02:00
portal . MarkDisappearing ( origEvtID , portal . ExpirationTime , true )
2021-10-22 19:14:34 +02:00
info := portal . generateMessageInfo ( sender )
2022-06-29 19:05:55 +02:00
if dbMsg == nil {
dbMsg = portal . markHandled ( nil , nil , info , evt . ID , false , true , database . MsgNormal , database . MsgNoError )
} else {
info . ID = dbMsg . JID
}
2021-10-22 19:14:34 +02:00
portal . log . Debugln ( "Sending event" , evt . ID , "to WhatsApp" , info . ID )
2022-06-29 19:05:55 +02:00
ts , err := sender . Client . SendMessage ( ctx , portal . Key . JID , info . ID , msg )
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 {
2021-10-31 22:36:41 +01:00
dbMsg . MarkSent ( ts )
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-03-05 20:22:31 +01:00
portal . upsertReaction ( nil , target . JID , sender . JID , evt . ID , info . ID )
portal . log . Debugln ( "Sending reaction" , evt . ID , "to WhatsApp" , info . ID )
ts , err := portal . sendReactionToWhatsApp ( sender , info . ID , target , content . RelatesTo . Key , evt . Timestamp )
2022-05-31 16:28:58 +02:00
if err == nil {
2022-03-05 20:22:31 +01:00
dbMsg . MarkSent ( ts )
}
2022-04-18 10:14:43 +02:00
return err
2022-03-05 20:22:31 +01:00
}
func ( portal * Portal ) sendReactionToWhatsApp ( sender * User , id types . MessageID , target * database . Message , key string , timestamp int64 ) ( time . Time , error ) {
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 )
2022-06-29 19:05:55 +02:00
return sender . Client . SendMessage ( context . TODO ( ) , portal . Key . JID , id , & 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 ) ,
} ,
} )
}
func ( portal * Portal ) upsertReaction ( intent * appservice . IntentAPI , targetJID types . MessageID , senderJID types . JID , mxid id . EventID , jid types . MessageID ) {
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
} else {
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
dbReaction . Upsert ( )
}
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 )
2021-10-27 20:34:22 +02:00
} else if msg . Sender . User != sender . JID . User {
2022-06-30 13:41:37 +02:00
go portal . sendMessageMetrics ( evt , errTargetSentBySomeoneElse , "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-06-13 19:01:39 +02:00
return
2022-05-31 16:28:58 +02:00
} else if msg . Type == database . MsgReaction {
2022-03-05 20:22:31 +01:00
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 {
portal . log . Debugfln ( "Sending redaction %s of %s/%s to WhatsApp" , evt . ID , msg . MXID , msg . JID )
2022-05-31 16:28:58 +02:00
_ , err := sender . Client . RevokeMessage ( portal . Key . JID , msg . JID )
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-05-22 15:15:54 +02:00
func ( portal * Portal ) HandleMatrixReadReceipt ( sender bridge . User , eventID id . EventID , receiptTimestamp time . Time ) {
portal . handleMatrixReadReceipt ( sender . ( * User ) , eventID , receiptTimestamp , true )
}
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 {
portal . log . Debugfln ( "Ignoring read receipt by %s: user is not connected to WhatsApp" , sender . JID )
}
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 )
}
}
2022-01-17 11:00:02 +01:00
if isExplicit {
portal . ScheduleDisappearing ( )
}
2021-11-30 15:38:37 +01:00
}
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
}
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 )
}
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
}
if portal . IsPrivateChat ( ) {
_ , err := portal . MainIntent ( ) . LeaveRoom ( portal . MXID )
if err != nil {
portal . log . Warnln ( "Failed to leave private chat portal with main intent:" , err )
}
return
}
intent := portal . MainIntent ( )
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
}
}