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-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"
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
2020-05-08 21:32:22 +02:00
func ( bridge * Bridge ) GetPortalByMXID ( mxid id . RoomID ) * Portal {
2018-08-28 23:40:54 +02:00
bridge . portalsLock . Lock ( )
defer bridge . portalsLock . Unlock ( )
portal , ok := bridge . portalsByMXID [ mxid ]
2018-08-16 14:59:18 +02:00
if ! ok {
2019-05-28 20:31:25 +02:00
return bridge . loadDBPortal ( bridge . DB . Portal . GetByMXID ( mxid ) , nil )
2018-08-16 14:59:18 +02:00
}
return portal
}
2018-08-28 23:40:54 +02:00
func ( bridge * Bridge ) GetPortalByJID ( key database . PortalKey ) * Portal {
bridge . portalsLock . Lock ( )
defer bridge . portalsLock . Unlock ( )
portal , ok := bridge . portalsByJID [ key ]
2018-08-16 14:59:18 +02:00
if ! ok {
2019-05-28 20:31:25 +02:00
return bridge . loadDBPortal ( bridge . DB . Portal . GetByJID ( key ) , & key )
2018-08-16 14:59:18 +02:00
}
return portal
}
2018-08-28 23:40:54 +02:00
func ( bridge * Bridge ) GetAllPortals ( ) [ ] * Portal {
2019-06-01 19:03:29 +02:00
return bridge . dbPortalsToPortals ( bridge . DB . Portal . GetAll ( ) )
}
2022-04-19 04:50:21 +02:00
func ( bridge * Bridge ) GetAllPortalsForUser ( userID id . UserID ) [ ] * Portal {
return bridge . dbPortalsToPortals ( bridge . DB . Portal . GetAllForUser ( userID ) )
}
2021-10-22 19:14:34 +02:00
func ( bridge * Bridge ) GetAllPortalsByJID ( jid types . JID ) [ ] * Portal {
2019-06-01 19:03:29 +02:00
return bridge . dbPortalsToPortals ( bridge . DB . Portal . GetAllByJID ( jid ) )
}
func ( bridge * Bridge ) dbPortalsToPortals ( dbPortals [ ] * database . Portal ) [ ] * Portal {
2018-08-28 23:40:54 +02:00
bridge . portalsLock . Lock ( )
defer bridge . 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
}
2018-08-28 23:40:54 +02:00
portal , ok := bridge . portalsByJID [ dbPortal . Key ]
2018-08-16 14:59:18 +02:00
if ! ok {
2019-05-28 20:31:25 +02:00
portal = bridge . loadDBPortal ( dbPortal , nil )
2018-08-16 14:59:18 +02:00
}
output [ index ] = portal
}
return output
}
2019-05-28 20:31:25 +02:00
func ( bridge * Bridge ) loadDBPortal ( dbPortal * database . Portal , key * database . PortalKey ) * Portal {
if dbPortal == nil {
if key == nil {
return nil
}
dbPortal = bridge . DB . Portal . New ( )
dbPortal . Key = * key
dbPortal . Insert ( )
}
portal := bridge . NewPortal ( dbPortal )
bridge . portalsByJID [ portal . Key ] = portal
if len ( portal . MXID ) > 0 {
bridge . portalsByMXID [ portal . MXID ] = portal
}
return portal
}
func ( portal * Portal ) GetUsers ( ) [ ] * User {
return nil
}
2022-02-10 18:18:49 +01:00
func ( bridge * Bridge ) newBlankPortal ( key database . PortalKey ) * Portal {
2020-07-05 17:57:03 +02:00
portal := & Portal {
bridge : bridge ,
log : bridge . Log . Sub ( fmt . Sprintf ( "Portal/%s" , key ) ) ,
2021-12-14 16:47:30 +01:00
messages : make ( chan PortalMessage , bridge . Config . Bridge . PortalMessageBuffer ) ,
2022-01-19 13:18:32 +01:00
receipts : make ( chan PortalReceipt , bridge . Config . Bridge . PortalMessageBuffer ) ,
2021-12-14 16:47:30 +01:00
matrixMessages : make ( chan PortalMatrixMessage , bridge . Config . Bridge . PortalMessageBuffer ) ,
2022-02-10 18:18:49 +01:00
mediaRetries : make ( chan PortalMediaRetry , bridge . Config . Bridge . PortalMessageBuffer ) ,
mediaErrorCache : make ( map [ types . MessageID ] * FailedMediaMeta ) ,
2020-07-05 17:57:03 +02:00
}
go portal . handleMessageLoop ( )
return portal
}
2022-02-10 18:18:49 +01:00
func ( bridge * Bridge ) NewManualPortal ( key database . PortalKey ) * Portal {
portal := bridge . newBlankPortal ( key )
portal . Portal = bridge . DB . Portal . New ( )
portal . Key = key
return portal
}
2018-08-30 23:13:08 +02:00
2022-02-10 18:18:49 +01:00
func ( bridge * Bridge ) NewPortal ( dbPortal * database . Portal ) * Portal {
portal := bridge . newBlankPortal ( dbPortal . Key )
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
2021-11-02 14:46:31 +01:00
fake * fakeMessage
2021-10-27 17:31:33 +02:00
source * User
}
2022-01-19 13:18:32 +01:00
type PortalReceipt struct {
evt * events . Receipt
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
bridge * Bridge
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
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
2019-05-30 16:48:22 +02:00
privateChatBackfillInvitePuppet func ( )
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
2022-01-19 13:18:32 +01:00
receipts chan PortalReceipt
2021-12-14 16:47:30 +01:00
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
}
}
if msg . evt != nil {
portal . handleMessage ( msg . source , msg . evt )
} else if msg . undecryptable != nil {
portal . handleUndecryptableMessage ( msg . source , msg . undecryptable )
} else if msg . fake != nil {
msg . fake . ID = "FAKE::" + msg . fake . ID
portal . handleFakeMessage ( * msg . fake )
} else {
portal . log . Warnln ( "Unexpected PortalMessage with no message: %+v" , msg )
}
}
func ( portal * Portal ) handleMatrixMessageLoopItem ( msg PortalMatrixMessage ) {
2022-01-17 11:00:02 +01: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 )
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 {
err := intent . SetReadMarkers ( portal . MXID , makeReadMarkerContent ( msg . MXID , intent . IsCustomPuppet ) )
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 )
2022-01-19 13:18:32 +01:00
case receipt := <- portal . receipts :
portal . handleReceipt ( receipt . evt , receipt . source )
2021-12-14 16:47:30 +01:00
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-01-03 15:11:39 +01:00
waMsg . LiveLocationMessage != nil || waMsg . GroupInviteMessage != nil || waMsg . ContactsArrayMessage != 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 )
}
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) convertMessage ( intent * appservice . IntentAPI , source * User , info * types . MessageInfo , waMsg * waProto . Message ) * 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 )
2021-10-22 19:14:34 +02:00
case waMsg . ImageMessage != nil :
2021-10-26 16:01:10 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetImageMessage ( ) )
2021-10-22 19:14:34 +02:00
case waMsg . StickerMessage != nil :
2021-10-26 16:01:10 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetStickerMessage ( ) )
2021-10-22 19:14:34 +02:00
case waMsg . VideoMessage != nil :
2021-10-26 16:01:10 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetVideoMessage ( ) )
2021-10-22 19:14:34 +02:00
case waMsg . AudioMessage != nil :
2021-10-26 16:01:10 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetAudioMessage ( ) )
2021-10-22 19:14:34 +02:00
case waMsg . DocumentMessage != nil :
2021-10-26 16:01:10 +02:00
return portal . convertMediaMessage ( intent , source , info , waMsg . GetDocumentMessage ( ) )
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 ( )
portal . Update ( )
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 ) {
portal . ExpirationTime = timer
portal . Update ( )
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
}
intent := portal . getMessageIntent ( source , & evt . Info )
2021-11-06 13:20:56 +01:00
if ! intent . IsCustomPuppet && portal . IsPrivateChat ( ) && evt . Info . Sender . User == portal . Key . Receiver . User {
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 {
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 )
2021-11-06 13:20:56 +01:00
if ! intent . IsCustomPuppet && portal . IsPrivateChat ( ) && evt . Info . Sender . User == portal . Key . Receiver . User {
portal . log . Debugfln ( "Not handling %s (%s): user doesn't have double puppeting enabled" , msgID , msgType )
return
}
2021-10-26 16:01:10 +02:00
converted := portal . convertMessage ( intent , source , & evt . Info , evt . Message )
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 ( )
}
2021-10-26 16:01:10 +02:00
var eventID 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
}
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-02-10 18:18:49 +01:00
//eventID = 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-01-03 15:11:39 +01:00
}
}
}
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-03-05 20:22:31 +01:00
existingMsg . UpdateMXID ( "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-03-05 20:22:31 +01:00
existingMsg . UpdateMXID ( "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-03-05 20:22:31 +01:00
func ( portal * Portal ) markHandled ( msg * database . Message , info * types . MessageInfo , mxid id . EventID , isSent , recent bool , msgType database . MessageType , error 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-02-10 18:18:49 +01:00
msg . Error = error
2021-12-25 19:50:36 +01:00
if info . IsIncomingBroadcast ( ) {
msg . BroadcastListJID = info . Chat
}
2021-10-27 17:31:33 +02:00
msg . Insert ( )
} else {
2022-03-05 20:22:31 +01:00
msg . UpdateMXID ( mxid , msgType , error )
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-02-10 18:18:49 +01:00
portal . recentlyHandled [ index ] = recentlyHandledWrapper { msg . JID , error }
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
}
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) getMessagePuppet ( user * User , info * types . MessageInfo ) * Puppet {
if info . IsFromMe {
return portal . bridge . GetPuppetByJID ( user . JID )
} else if portal . IsPrivateChat ( ) {
return portal . bridge . GetPuppetByJID ( portal . Key . JID )
} else {
puppet := portal . bridge . GetPuppetByJID ( info . Sender )
2021-11-08 12:04:39 +01:00
puppet . SyncContact ( user , true , "handling message" )
2021-10-26 16:01:10 +02:00
return puppet
}
}
2021-10-22 19:14:34 +02:00
func ( portal * Portal ) getMessageIntent ( user * User , info * types . MessageInfo ) * appservice . IntentAPI {
2021-11-08 12:04:39 +01:00
return portal . getMessagePuppet ( user , info ) . IntentFor ( portal )
2020-06-10 13:58:57 +02:00
}
2022-03-05 20:22:31 +01:00
func ( portal * Portal ) finishHandling ( existing * database . Message , message * types . MessageInfo , mxid id . EventID , msgType database . MessageType , error database . MessageErrorType ) {
portal . markHandled ( existing , message , mxid , true , true , msgType , error )
2020-06-05 16:54:09 +02:00
portal . sendDeliveryReceipt ( mxid )
2022-02-10 18:18:49 +01:00
var suffix string
if error == database . MsgErrDecryptionFailed {
suffix = "(undecryptable message error notice)"
} else if error == database . MsgErrMediaNotFound {
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
}
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 )
2021-11-08 12:04:39 +01:00
puppet . SyncContact ( source , true , "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
2021-10-28 11:59:22 +02:00
func ( portal * Portal ) UpdateAvatar ( user * User , setBy types . JID , updateInfo bool ) bool {
2022-02-10 11:46:25 +01:00
portal . avatarLock . Lock ( )
defer portal . avatarLock . Unlock ( )
2021-10-22 19:14:34 +02:00
avatar , err := user . Client . GetProfilePictureInfo ( portal . Key . JID , false )
if err != nil {
if ! errors . Is ( err , whatsmeow . ErrProfilePictureUnauthorized ) {
portal . log . Warnln ( "Failed to get avatar URL:" , err )
2018-08-26 00:55:21 +02:00
}
2018-08-23 00:12:26 +02:00
return false
2021-10-22 19:14:34 +02:00
} else if avatar == nil {
if portal . Avatar == "remove" {
2021-02-09 22:41:14 +01:00
return false
}
2021-10-22 19:14:34 +02:00
portal . AvatarURL = id . ContentURI { }
avatar = & types . ProfilePictureInfo { ID : "remove" }
} else if avatar . ID == portal . Avatar {
return false
} else if len ( avatar . URL ) == 0 {
portal . log . Warnln ( "Didn't get URL in response to avatar query" )
return false
} else {
url , err := reuploadAvatar ( portal . MainIntent ( ) , avatar . URL )
2021-02-09 22:41:14 +01:00
if err != nil {
2021-10-22 19:14:34 +02:00
portal . log . Warnln ( "Failed to reupload avatar:" , err )
2021-02-09 22:41:14 +01:00
return false
}
2021-10-22 19:14:34 +02:00
portal . AvatarURL = url
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 )
}
_ , err = intent . SetRoomAvatar ( portal . MXID , portal . AvatarURL )
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 )
2019-05-22 22:27:58 +02:00
return false
}
2018-08-23 00:12:26 +02:00
}
2021-10-22 19:14:34 +02:00
portal . Avatar = avatar . ID
2020-06-15 13:56:52 +02:00
if updateInfo {
portal . UpdateBridgeInfo ( )
}
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
}
2018-08-26 00:55:21 +02:00
if portal . Name != name {
2021-02-09 22:41:14 +01:00
portal . log . Debugfln ( "Updating name %s -> %s" , portal . Name , name )
portal . Name = name
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 . SetRoomName ( portal . MXID , name )
2021-10-28 11:59:22 +02:00
if errors . Is ( err , mautrix . MForbidden ) && intent != portal . MainIntent ( ) {
_ , err = portal . MainIntent ( ) . SetRoomName ( portal . MXID , name )
}
2018-08-23 00:12:26 +02:00
if err == nil {
2020-06-15 13:56:52 +02:00
if updateInfo {
portal . UpdateBridgeInfo ( )
}
2018-08-23 00:12:26 +02:00
return true
2021-02-09 22:41:14 +01:00
} else {
portal . Name = ""
portal . log . Warnln ( "Failed to set room name:" , err )
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 {
2018-08-26 00:55:21 +02:00
if portal . Topic != topic {
2021-02-09 22:41:14 +01:00
portal . log . Debugfln ( "Updating topic %s -> %s" , portal . Topic , topic )
portal . Topic = topic
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 {
2020-06-15 13:56:52 +02:00
if updateInfo {
portal . UpdateBridgeInfo ( )
}
2018-08-23 00:12:26 +02:00
return true
2021-02-09 22:41:14 +01:00
} else {
portal . Topic = ""
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
2021-02-21 13:45:33 +01:00
if ! portal . IsPrivateChat ( ) && ! portal . IsBroadcastList ( ) && portal . Avatar == "" {
2021-10-28 11:59:22 +02:00
update = portal . UpdateAvatar ( user , types . EmptyJID , false ) || update
2019-03-13 23:38:11 +01:00
}
2018-08-23 00:12:26 +02:00
if update {
2018-08-19 17:21:38 +02:00
portal . Update ( )
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 ,
2018-08-26 15:11:48 +02:00
} ,
}
}
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
}
changed := false
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
if levels . EventsDefault == newLevel {
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
}
changed := false
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
}
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
} ,
}
bridgeInfoStateKey := fmt . Sprintf ( "net.maunium.whatsapp://whatsapp/%s" , portal . Key . JID )
return bridgeInfoStateKey , bridgeInfo
}
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-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 )
2021-11-08 12:04:39 +01:00
puppet . SyncContact ( user , true , "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-04-27 16:29:15 +02:00
user . bridge . DB . BackfillQuery . DeleteAllForPortal ( user . MXID , portal . Key )
user . bridge . DB . HistorySyncQuery . 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
} )
}
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 {
Parsed : event . EncryptionEventContent { Algorithm : id . AlgorithmMegolmV1 } ,
} ,
} )
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
}
portal . MXID = resp . RoomID
portal . Update ( )
2020-05-28 19:35:43 +02:00
portal . bridge . portalsLock . Lock ( )
portal . bridge . portalsByMXID [ portal . MXID ] = portal
portal . bridge . portalsLock . Unlock ( )
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
portal . Update ( )
}
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
portal . Update ( )
}
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 )
user . EnqueueMediaBackfills ( portals )
2022-03-25 07:15:52 +01:00
user . BackfillQueue . ReCheckQueue <- true
}
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 )
content . RelatesTo = & event . RelatesTo {
EventID : message . MXID ,
Type : event . RelReply ,
2018-08-24 18:46:14 +02:00
}
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
type sendReactionContent struct {
event . ReactionEventContent
DoublePuppet string ` json:"fi.mau.double_puppet_source,omitempty" `
}
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-03-12 18:47:47 +01:00
extra := make ( map [ string ] interface { } )
if intent . IsCustomPuppet {
extra [ doublePuppetKey ] = doublePuppetValue
}
resp , err := intent . RedactEvent ( portal . MXID , existing . MXID , mautrix . ReqRedact { Extra : extra } )
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-03-12 18:47:47 +01:00
var content sendReactionContent
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
}
if intent . IsCustomPuppet {
content . DoublePuppet = doublePuppetValue
}
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 )
2021-12-15 18:37:01 +01:00
redactionReq := mautrix . ReqRedact { Extra : map [ string ] interface { } { } }
if intent . IsCustomPuppet {
redactionReq . Extra [ doublePuppetKey ] = doublePuppetValue
}
_ , err := intent . RedactEvent ( portal . MXID , msg . MXID , redactionReq )
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 ) {
2021-12-15 18:37:01 +01:00
_ , err = portal . MainIntent ( ) . RedactEvent ( portal . MXID , msg . MXID , redactionReq )
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
}
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) encrypt ( content * event . Content , eventType event . Type ) ( event . Type , error ) {
2020-05-09 01:03:59 +02:00
if portal . Encrypted && portal . bridge . Crypto != nil {
2021-02-25 16:22:29 +01:00
// TODO maybe the locking should be inside mautrix-go?
portal . encryptLock . Lock ( )
2021-10-26 16:01:10 +02:00
encrypted , err := portal . bridge . Crypto . Encrypt ( portal . MXID , eventType , * content )
2021-02-25 16:22:29 +01:00
portal . encryptLock . Unlock ( )
2020-05-09 01:03:59 +02:00
if err != nil {
2021-10-26 16:01:10 +02:00
return eventType , fmt . Errorf ( "failed to encrypt event: %w" , err )
2020-05-09 01:03:59 +02:00
}
eventType = event . EventEncrypted
2021-10-26 16:01:10 +02:00
content . Parsed = encrypted
}
return eventType , nil
}
2021-12-15 10:51:26 +01:00
const doublePuppetKey = "fi.mau.double_puppet_source"
2021-12-15 12:51:20 +01:00
const doublePuppetValue = "mautrix-whatsapp"
2021-11-03 20:34:06 +01: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
if timestamp != 0 && intent . IsCustomPuppet {
2021-10-31 19:42:53 +01:00
if wrappedContent . Raw == nil {
wrappedContent . Raw = map [ string ] interface { } { }
2021-10-26 16:01:10 +02:00
}
2021-12-15 10:51:26 +01:00
if intent . IsCustomPuppet {
2021-12-15 12:51:20 +01:00
wrappedContent . Raw [ doublePuppetKey ] = doublePuppetValue
2021-12-15 10:51:26 +01:00
}
2021-10-26 16:01:10 +02:00
}
var err error
eventType , err = portal . encrypt ( & wrappedContent , eventType )
if err != nil {
return nil , err
2020-05-09 01:03:59 +02:00
}
2022-01-04 01:02:06 +01:00
2022-02-15 12:36:44 +01:00
if eventType == event . EventEncrypted {
// Clear other custom keys if the event was encrypted, but keep the double puppet identifier
if intent . IsCustomPuppet {
wrappedContent . Raw = map [ string ] interface { } { doublePuppetKey : doublePuppetValue }
} else {
wrappedContent . Raw = nil
}
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
2021-10-26 16:01:10 +02:00
}
2018-08-23 00:12:26 +02:00
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-02-16 00:00:49 +01:00
portal . bridge . Formatter . ParseWhatsApp ( portal . MXID , content , contextInfo . GetMentionedJid ( ) )
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
}
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"
data , uploadMimeType , file := portal . encryptFile ( 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 {
_ , _ = portal . leaveWithPuppetMeta ( targetIntent )
}
}
} else {
_ , err := portal . leaveWithPuppetMeta ( targetIntent )
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 ) leaveWithPuppetMeta ( intent * appservice . IntentAPI ) ( * mautrix . RespSendEvent , error ) {
content := event . Content {
Parsed : event . MemberEventContent {
Membership : event . MembershipLeave ,
} ,
Raw : map [ string ] interface { } {
2021-12-15 12:51:20 +01:00
doublePuppetKey : doublePuppetValue ,
2021-10-28 11:59:22 +02:00
} ,
}
2021-11-05 11:17:56 +01:00
// Bypass IntentAPI, we don't want to EnsureJoined here
return intent . Client . SendStateEvent ( portal . MXID , event . StateMember , intent . UserID . String ( ) , & content )
2021-10-28 11:59:22 +02:00
}
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 )
2021-11-08 12:04:39 +01:00
puppet . SyncContact ( source , true , "handling whatsapp invite" )
2021-10-28 11:59:22 +02:00
content := event . Content {
Parsed : event . MemberEventContent {
Membership : "invite" ,
Displayname : puppet . Displayname ,
AvatarURL : puppet . AvatarURL . CUString ( ) ,
} ,
Raw : map [ string ] interface { } {
2021-12-15 12:51:20 +01:00
doublePuppetKey : doublePuppetValue ,
2021-10-28 11:59:22 +02:00
} ,
}
resp , err := intent . SendStateEvent ( portal . MXID , event . StateMember , puppet . MXID . String ( ) , & content )
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 {
2021-10-26 16:01:10 +02:00
portal . log . Errorfln ( "Failed to bridge media for %s: %v" , info . ID , bridgeErr )
2022-02-10 18:18:49 +01:00
if keys != nil {
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
}
func ( portal * Portal ) encryptFile ( data [ ] byte , mimeType string ) ( [ ] byte , string , * event . EncryptedFileInfo ) {
if ! portal . Encrypted {
return data , mimeType , nil
}
file := & event . EncryptedFileInfo {
EncryptedFile : * attachment . NewEncryptedFile ( ) ,
URL : "" ,
}
return file . Encrypt ( data ) , "application/octet-stream" , file
}
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 )
thumbnail , thumbnailUploadMime , thumbnailFile := portal . encryptFile ( thumbnailData , thumbnailMime )
2020-06-10 14:26:14 +02:00
uploadedThumbnail , err := intent . UploadBytes ( thumbnail , thumbnailUploadMime )
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-02-16 00:00:49 +01:00
portal . bridge . Formatter . ParseWhatsApp ( portal . MXID , captionContent , msg . GetContextInfo ( ) . GetMentionedJid ( ) )
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 {
data , uploadMimeType , file := portal . encryptFile ( data , content . Info . MimeType )
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
}
func ( portal * Portal ) convertMediaMessage ( intent * appservice . IntentAPI , source * User , info * types . MessageInfo , msg MediaMessage ) * ConvertedMessage {
converted := portal . convertMediaMessageContent ( intent , msg )
data , err := source . Client . Download ( msg )
if errors . Is ( err , whatsmeow . ErrMediaDownloadFailedWith404 ) || errors . Is ( err , whatsmeow . ErrMediaDownloadFailedWith410 ) {
//portal.log.Warnfln("Failed to download media for %s: %v. Requesting retry", info.ID, err)
//err = source.Client.SendMediaRetryReceipt(info, msg.GetMediaKey())
//if err != nil {
// portal.log.Errorfln("Failed to send media retry receipt for %s: %v", info.ID, err)
//}
converted . Error = database . MsgErrMediaNotFound
2022-04-21 00:59:59 +02:00
errorText := "Old photo or attachment."
if portal . bridge . Config . Bridge . HistorySync . BackfillMedia {
if len ( portal . bridge . Config . Bridge . HistorySync . Media ) > 0 {
errorText += " Media will be requested from your phone later."
} else {
errorText += ` React with the \u267b (recycle) emoji or the text "click to retry" to request this media from your phone or use the backfill command to request all missing media for this chat. `
}
} else {
errorText += ` Automatic media backfill is disabled. React with the \u267b (recycle) emoji or the text "click to retry" to request this media from your phone. `
}
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
}
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
}
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 )
return
} else if retryData . GetResult ( ) != waProto . MediaRetryNotification_SUCCESS {
portal . log . Warnfln ( "Got error response in media retry notification for %s: %s" , retry . MessageID , waProto . MediaRetryNotification_MediaRetryNotificationResultType_name [ int32 ( retryData . GetResult ( ) ) ] )
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 )
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 )
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 )
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-03-05 20:22:31 +01:00
msg . UpdateMXID ( resp . EventID , database . MsgNormal , database . MsgNoError )
2022-02-10 18:18:49 +01:00
}
func ( portal * Portal ) requestMediaRetry ( user * User , eventID id . EventID ) {
msg := portal . bridge . DB . Message . GetByMXID ( eventID )
if msg == nil {
portal . log . Debugfln ( "%s requested a media retry for unknown event %s" , user . MXID , eventID )
return
} else if msg . Error != database . MsgErrMediaNotFound {
portal . log . Debugfln ( "%s requested a media retry for non-errored event %s" , user . MXID , eventID )
return
}
evt , err := portal . fetchMediaRetryEvent ( msg )
if err != nil {
portal . log . Warnfln ( "Can't send media retry request for %s: %v" , msg . JID , err )
return
}
err = user . Client . SendMediaRetryReceipt ( & types . MessageInfo {
ID : msg . JID ,
MessageSource : types . MessageSource {
IsFromMe : msg . Sender . User == user . JID . User ,
IsGroup : ! portal . IsPrivateChat ( ) ,
Sender : msg . Sender ,
Chat : portal . Key . JID ,
} ,
} , evt . Media . Key )
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 )
}
}
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
}
func ( portal * Portal ) downloadThumbnail ( original [ ] byte , thumbnailURL id . ContentURIString , eventID id . EventID ) ( [ ] byte , error ) {
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 )
} else if thumbnail , err := portal . MainIntent ( ) . DownloadBytes ( mxc ) ; err != nil {
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
}
2021-10-22 19:14:34 +02:00
func ( portal * Portal ) preprocessMatrixMedia ( sender * User , relaybotFormatted bool , content * event . MessageEventContent , eventID id . EventID , mediaType whatsmeow . MediaType ) * MediaUpload {
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 {
2020-05-20 15:43:55 +02:00
portal . log . Errorln ( "Malformed content URL in %s: %v" , eventID , err )
2020-05-25 22:11:00 +02:00
return nil
2020-05-08 21:32:22 +02:00
}
data , err := portal . MainIntent ( ) . DownloadBytes ( mxc )
if err != nil {
2020-05-20 15:43:55 +02:00
portal . log . Errorfln ( "Failed to download media in %s: %v" , eventID , err )
2018-08-25 23:26:24 +02:00
return nil
2018-08-24 21:31:18 +02:00
}
2020-05-20 15:43:55 +02:00
if file != nil {
data , err = file . Decrypt ( data )
if err != nil {
portal . log . Errorfln ( "Failed to decrypt media in %s: %v" , eventID , err )
return nil
}
}
2021-10-22 19:14:34 +02:00
if mediaType == whatsmeow . MediaVideo && content . GetInfo ( ) . MimeType == "image/gif" {
2022-01-04 01:02:06 +01:00
data , err = ffmpeg . ConvertBytes ( data , ".mp4" , [ ] string { "-f" , "gif" } , [ ] string {
"-pix_fmt" , "yuv420p" , "-c:v" , "libx264" , "-movflags" , "+faststart" ,
"-filter:v" , "crop='floor(in_w/2)*2:floor(in_h/2)*2'" ,
} , content . GetInfo ( ) . MimeType )
2020-06-23 15:36:05 +02:00
if err != nil {
portal . log . Errorfln ( "Failed to convert gif to mp4 in %s: %v" , eventID , err )
return nil
}
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 {
portal . log . Errorfln ( "Failed to convert webp to png in %s: %v" , eventID , err )
return nil
}
content . Info . MimeType = "image/png"
}
2021-10-22 19:14:34 +02:00
uploadResp , err := sender . Client . Upload ( context . Background ( ) , data , mediaType )
2018-08-25 23:26:24 +02:00
if err != nil {
2020-05-20 15:43:55 +02:00
portal . log . Errorfln ( "Failed to upload media in %s: %v" , eventID , err )
2018-08-25 23:26:24 +02:00
return nil
}
2021-11-11 19:33:22 +01:00
// Audio doesn't have thumbnails
var thumbnail [ ] byte
if mediaType != whatsmeow . MediaAudio {
thumbnail , err = portal . downloadThumbnail ( data , content . GetInfo ( ) . ThumbnailURL , eventID )
// 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 ) {
portal . log . Errorfln ( "Failed to generate thumbnail for %s: %v" , eventID , err )
}
}
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 ) ,
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
}
2021-10-22 19:14:34 +02:00
func ( portal * Portal ) convertMatrixMessage ( sender * User , evt * event . Event ) ( * waProto . Message , * User ) {
2020-05-25 22:11:00 +02:00
content , ok := evt . Content . Parsed . ( * event . MessageEventContent )
if ! ok {
portal . log . Debugfln ( "Failed to handle event %s: unexpected parsed content type %T" , evt . ID , evt . Content . Parsed )
return nil , sender
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 ( ) {
2021-11-01 15:46:03 +01:00
portal . log . Warnln ( "Ignoring message from" , sender . MXID , "in chat with no relaybot (convertMatrixMessage)" )
2021-10-28 12:57:15 +02:00
return nil , sender
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 {
return nil , sender
}
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 ,
}
hasPreview := portal . convertURLPreviewToWhatsApp ( sender , evt , msg . ExtendedTextMessage )
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 :
2021-10-22 19:14:34 +02:00
media := portal . preprocessMatrixMedia ( sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaImage )
2018-08-25 23:26:24 +02:00
if media == nil {
2020-05-25 22:11:00 +02:00
return nil , sender
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"
2021-10-22 19:14:34 +02:00
media := portal . preprocessMatrixMedia ( sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaVideo )
2018-08-25 23:26:24 +02:00
if media == nil {
2020-05-25 22:11:00 +02:00
return nil , sender
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 :
2021-10-22 19:14:34 +02:00
media := portal . preprocessMatrixMedia ( sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaAudio )
2018-08-25 23:26:24 +02:00
if media == nil {
2020-05-25 22:11:00 +02:00
return nil , sender
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 :
2021-10-22 19:14:34 +02:00
media := portal . preprocessMatrixMedia ( sender , relaybotFormatted , content , evt . ID , whatsmeow . MediaDocument )
2018-08-25 23:26:24 +02:00
if media == nil {
2020-05-25 22:11:00 +02:00
return nil , sender
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 {
portal . log . Debugfln ( "Invalid geo URI on Matrix event %s: %v" , evt . ID , err )
return nil , sender
}
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 :
2021-05-12 12:39:24 +02:00
portal . log . Debugfln ( "Unhandled Matrix event %s: unknown msgtype %s" , evt . ID , content . MsgType )
2020-05-25 22:11:00 +02:00
return nil , sender
2020-05-24 16:28:30 +02:00
}
2021-10-22 19:14:34 +02:00
return & msg , sender
2020-05-24 16:28:30 +02:00
}
2021-06-15 11:05:11 +02:00
func ( portal * Portal ) sendErrorMessage ( message string , confirmed bool ) id . EventID {
certainty := "may not have been"
if confirmed {
certainty = "was not"
}
2021-10-31 19:42:53 +01:00
resp , err := portal . sendMainIntentMessage ( & event . MessageEventContent {
2020-05-24 16:28:30 +02:00
MsgType : event . MsgNotice ,
2021-06-15 11:05:11 +02:00
Body : fmt . Sprintf ( "\u26a0 Your message %s bridged: %v" , certainty , message ) ,
2020-05-24 16:28:30 +02:00
} )
if err != nil {
portal . log . Warnfln ( "Failed to send bridging error message:" , err )
return ""
}
return resp . EventID
}
2020-05-25 10:17:47 +02:00
func ( portal * Portal ) sendDeliveryReceipt ( eventID id . EventID ) {
if portal . bridge . Config . Bridge . DeliveryReceipts {
err := portal . bridge . Bot . MarkRead ( portal . MXID , eventID )
if err != nil {
portal . log . Debugfln ( "Failed to send delivery receipt for %s: %v" , eventID , err )
}
}
}
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 ) {
2021-11-01 15:46:03 +01:00
if ! portal . canBridgeFrom ( sender , "message" ) {
2018-08-19 17:21:38 +02:00
return
}
2021-11-01 15:46:03 +01:00
portal . log . Debugfln ( "Received event %s from %s" , evt . ID , evt . Sender )
2021-10-22 19:14:34 +02:00
msg , sender := portal . convertMatrixMessage ( sender , evt )
if msg == nil {
2020-05-25 22:11:00 +02:00
return
}
2022-01-07 13:32:00 +01:00
portal . MarkDisappearing ( evt . ID , portal . ExpirationTime , true )
2021-10-22 19:14:34 +02:00
info := portal . generateMessageInfo ( sender )
2022-03-05 20:22:31 +01:00
dbMsg := portal . markHandled ( nil , info , evt . ID , false , true , database . MsgNormal , database . MsgNoError )
2021-10-22 19:14:34 +02:00
portal . log . Debugln ( "Sending event" , evt . ID , "to WhatsApp" , info . ID )
2021-10-31 22:36:41 +01:00
ts , err := sender . Client . SendMessage ( portal . Key . JID , info . ID , msg )
2018-08-19 17:21:38 +02:00
if err != nil {
2021-11-22 10:03:09 +01:00
portal . log . Errorfln ( "Error sending message: %v" , err )
2021-10-22 19:14:34 +02:00
portal . sendErrorMessage ( err . Error ( ) , true )
2021-12-21 21:40:08 +01:00
status := appservice . StatusPermFailure
if errors . Is ( err , whatsmeow . ErrBroadcastListUnsupported ) {
status = appservice . StatusUnsupported
}
checkpoint := appservice . NewMessageSendCheckpoint ( evt , appservice . StepRemote , status , 0 )
checkpoint . Info = err . Error ( )
go checkpoint . Send ( portal . bridge . AS )
2020-05-24 14:33:26 +02:00
} else {
2020-05-24 16:28:30 +02:00
portal . log . Debugfln ( "Handled Matrix event %s" , evt . ID )
2021-12-04 01:50:55 +01:00
portal . bridge . AS . SendMessageSendCheckpoint ( evt , appservice . StepRemote , 0 )
2020-05-25 10:17:47 +02:00
portal . sendDeliveryReceipt ( evt . ID )
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 ) {
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 )
if err != nil {
portal . log . Errorfln ( "Error sending reaction %s: %v" , evt . ID , err )
portal . bridge . AS . SendErrorMessageSendCheckpoint ( evt , appservice . StepRemote , err , true , 0 )
} else {
portal . log . Debugfln ( "Handled Matrix reaction %s" , evt . ID )
portal . bridge . AS . SendMessageSendCheckpoint ( evt , appservice . StepRemote , 0 )
portal . sendDeliveryReceipt ( evt . ID )
}
}
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 )
dbMsg := portal . markHandled ( nil , info , evt . ID , false , true , database . MsgReaction , database . MsgNoError )
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 )
if err != nil {
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-03-05 20:22:31 +01:00
return sender . Client . SendMessage ( portal . Key . JID , id , & waProto . Message {
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 ) ,
GroupingKey : proto . String ( key ) , // TODO is this correct?
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-03-12 18:47:47 +01:00
extra := make ( map [ string ] interface { } )
if intent . IsCustomPuppet {
extra [ doublePuppetKey ] = doublePuppetValue
}
_ , err = intent . RedactEvent ( portal . MXID , dbReaction . MXID , mautrix . ReqRedact { Extra : extra } )
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 ) {
2021-11-01 15:46:03 +01:00
if ! portal . canBridgeFrom ( sender , "redaction" ) {
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 {
2021-12-08 15:00:01 +01:00
portal . log . Debugfln ( "Ignoring redaction %s of unknown event by %s" , evt . ID , senderLogIdentifier )
2021-12-04 01:50:55 +01:00
portal . bridge . AS . SendErrorMessageSendCheckpoint ( evt , appservice . StepRemote , errors . New ( "target not found" ) , true , 0 )
2021-10-27 20:34:22 +02:00
return
2021-11-02 14:46:31 +01:00
} else if msg . IsFakeJID ( ) {
2021-12-08 15:00:01 +01:00
portal . log . Debugfln ( "Ignoring redaction %s of fake event by %s" , evt . ID , senderLogIdentifier )
2021-12-04 01:50:55 +01:00
portal . bridge . AS . SendErrorMessageSendCheckpoint ( evt , appservice . StepRemote , errors . New ( "target is a fake event" ) , true , 0 )
2021-11-02 14:46:31 +01:00
return
2021-10-27 20:34:22 +02:00
} else if msg . Sender . User != sender . JID . User {
2021-11-01 15:46:03 +01:00
portal . log . Debugfln ( "Ignoring redaction %s of %s/%s by %s: message was sent by someone else (%s, not %s)" , evt . ID , msg . MXID , msg . JID , senderLogIdentifier , msg . Sender , sender . JID )
2021-12-04 01:50:55 +01:00
portal . bridge . AS . SendErrorMessageSendCheckpoint ( evt , appservice . StepRemote , errors . New ( "message was sent by someone else" ) , true , 0 )
2019-05-16 00:59:36 +02:00
return
}
2022-03-05 20:22:31 +01:00
var err error
if msg . Type == database . MsgReaction {
if reaction := portal . bridge . DB . Reaction . GetByMXID ( evt . Redacts ) ; reaction == nil {
portal . log . Debugfln ( "Ignoring redaction of reaction %s: reaction database entry not found" , evt . ID )
portal . bridge . AS . SendErrorMessageSendCheckpoint ( evt , appservice . StepRemote , errors . New ( "reaction database entry not found" ) , true , 0 )
return
} else if reactionTarget := reaction . GetTarget ( ) ; reactionTarget == nil {
portal . log . Debugfln ( "Ignoring redaction of reaction %s: reaction target message not found" , evt . ID )
portal . bridge . AS . SendErrorMessageSendCheckpoint ( evt , appservice . StepRemote , errors . New ( "reaction target message not found" ) , true , 0 )
return
} else {
portal . log . Debugfln ( "Sending redaction reaction %s of %s/%s to WhatsApp" , evt . ID , msg . MXID , msg . JID )
_ , err = portal . sendReactionToWhatsApp ( sender , "" , reactionTarget , "" , evt . Timestamp )
}
} else {
portal . log . Debugfln ( "Sending redaction %s of %s/%s to WhatsApp" , evt . ID , msg . MXID , msg . JID )
_ , err = sender . Client . RevokeMessage ( portal . Key . JID , msg . JID )
}
2019-05-16 00:59:36 +02:00
if err != nil {
2020-05-24 14:33:26 +02:00
portal . log . Errorfln ( "Error handling Matrix redaction %s: %v" , evt . ID , err )
2021-12-04 01:50:55 +01:00
portal . bridge . AS . SendErrorMessageSendCheckpoint ( evt , appservice . StepRemote , err , true , 0 )
2019-05-16 00:59:36 +02:00
} else {
2021-10-27 20:34:22 +02:00
portal . log . Debugfln ( "Handled Matrix redaction %s of %s" , evt . ID , evt . Redacts )
2021-12-04 01:50:55 +01:00
portal . bridge . AS . SendMessageSendCheckpoint ( evt , appservice . StepRemote , 0 )
2020-05-25 10:17:47 +02:00
portal . sendDeliveryReceipt ( evt . ID )
2019-05-16 00:59:36 +02:00
}
}
2019-05-16 19:14:32 +02:00
2022-01-17 11:00:02 +01:00
func ( portal * Portal ) HandleMatrixReadReceipt ( sender * User , eventID id . EventID , receiptTimestamp time . Time , isExplicit bool ) {
2021-12-07 15:02:51 +01:00
if ! sender . IsLoggedIn ( ) {
2022-01-17 11:00:02 +01:00
if isExplicit {
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 )
}
2021-11-01 15:46:03 +01:00
func ( portal * Portal ) canBridgeFrom ( sender * User , evtType string ) bool {
if ! sender . IsLoggedIn ( ) {
if portal . HasRelaybot ( ) {
return true
} else if sender . Session != nil {
2022-02-17 13:09:40 +01:00
portal . log . Debugfln ( "Ignoring %s from %s as user is not connected" , evtType , sender . MXID )
2021-11-01 15:46:03 +01:00
msg := format . RenderMarkdown ( fmt . Sprintf ( "\u26a0 You are not connected to WhatsApp, so your %s was not bridged." , evtType ) , true , false )
msg . MsgType = event . MsgNotice
_ , err := portal . sendMainIntentMessage ( & msg )
if err != nil {
portal . log . Errorln ( "Failed to send bridging failure message:" , err )
}
} else {
portal . log . Debugfln ( "Ignoring %s from non-logged-in user %s in chat with no relay user" , evtType , sender . MXID )
}
return false
2021-12-09 14:32:11 +01:00
} else if portal . IsPrivateChat ( ) && sender . JID . User != portal . Key . Receiver . User && ! portal . HasRelaybot ( ) {
2021-11-01 15:46:03 +01:00
portal . log . Debugfln ( "Ignoring %s from different user %s/%s in private chat with no relay user" , evtType , sender . MXID , sender . JID )
return false
}
return true
}
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
}
func ( portal * Portal ) HandleMatrixLeave ( sender * User ) {
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-01-15 12:59:20 +01:00
func ( portal * Portal ) HandleMatrixKick ( sender * User , target * Puppet ) {
_ , 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-01-15 12:59:20 +01:00
func ( portal * Portal ) HandleMatrixInvite ( sender * User , target * Puppet ) {
_ , 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
func ( portal * Portal ) HandleMatrixMeta ( sender * User , evt * event . Event ) {
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 ( )
portal . Update ( )
2021-02-09 22:41:14 +01:00
}
}