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"
2018-09-01 22:38:03 +02:00
"encoding/gob"
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"
2021-10-31 12:30:19 +01:00
"io"
2020-06-10 13:58:57 +02:00
"math"
2018-08-24 18:46:14 +02:00
"mime"
"net/http"
2020-06-23 15:36:05 +02:00
"os"
"os/exec"
"path/filepath"
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
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"
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 ( ) )
}
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
}
2020-07-05 17:57:03 +02:00
func ( bridge * Bridge ) NewManualPortal ( key database . PortalKey ) * Portal {
portal := & Portal {
Portal : bridge . DB . Portal . New ( ) ,
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 ) ,
matrixMessages : make ( chan PortalMatrixMessage , bridge . Config . Bridge . PortalMessageBuffer ) ,
2020-07-05 17:57:03 +02:00
}
portal . Key = key
go portal . handleMessageLoop ( )
return portal
}
2018-08-28 23:40:54 +02:00
func ( bridge * Bridge ) NewPortal ( dbPortal * database . Portal ) * Portal {
2019-05-21 22:44:14 +02:00
portal := & Portal {
2018-08-16 14:59:18 +02:00
Portal : dbPortal ,
2018-08-28 23:40:54 +02:00
bridge : bridge ,
log : bridge . Log . Sub ( fmt . Sprintf ( "Portal/%s" , dbPortal . Key ) ) ,
2018-08-30 23:13:08 +02:00
2021-12-14 16:47:30 +01:00
messages : make ( chan PortalMessage , bridge . Config . Bridge . PortalMessageBuffer ) ,
matrixMessages : make ( chan PortalMatrixMessage , bridge . Config . Bridge . PortalMessageBuffer ) ,
2018-08-16 14:59:18 +02:00
}
2019-05-21 22:44:14 +02:00
go portal . handleMessageLoop ( )
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
}
2021-12-14 16:47:30 +01:00
type PortalMatrixMessage struct {
evt * event . Event
user * User
}
2021-10-27 17:31:33 +02:00
type recentlyHandledWrapper struct {
id types . MessageID
err bool
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
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
matrixMessages chan PortalMatrixMessage
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" )
err := portal . CreateMatrixRoom ( msg . source , nil , false )
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 ) {
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 )
}
}
func ( portal * Portal ) handleMessageLoop ( ) {
for {
select {
case msg := <- portal . messages :
portal . handleMessageLoopItem ( msg )
case msg := <- portal . matrixMessages :
portal . handleMatrixMessageLoopItem ( msg )
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 ||
2021-12-07 13:51:56 +01:00
waMsg . LiveLocationMessage != nil || waMsg . GroupInviteMessage != nil
2021-03-19 20:14:01 +01:00
}
2021-11-01 14:30:56 +01:00
func isPotentiallyInteresting ( waMsg * waProto . Message ) bool {
if waMsg == nil {
return false
}
2021-10-31 19:58:30 +01:00
// List of message types that aren't supported, but might potentially be interesting
// (so a warning should be logged if they are encountered).
2021-11-01 14:30:56 +01:00
return waMsg . Call != nil || waMsg . Chat != nil || waMsg . ContactsArrayMessage != nil ||
waMsg . HighlyStructuredMessage != nil || waMsg . SendPaymentMessage != nil || waMsg . LiveLocationMessage != nil ||
waMsg . RequestPaymentMessage != nil || waMsg . DeclinePaymentRequestMessage != nil ||
waMsg . CancelPaymentRequestMessage != nil || waMsg . TemplateMessage != nil ||
waMsg . TemplateButtonReplyMessage != nil || waMsg . ProductMessage != nil || waMsg . ListMessage != nil ||
waMsg . OrderMessage != nil || waMsg . ListResponseMessage != nil || waMsg . InvoiceMessage != nil ||
waMsg . ButtonsMessage != nil || waMsg . ButtonsResponseMessage != nil || waMsg . PaymentInviteMessage != nil ||
waMsg . InteractiveMessage != nil || waMsg . ReactionMessage != nil || waMsg . StickerSyncRmrMessage != nil
2021-10-31 19:58:30 +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"
case waMsg . Conversation != nil || waMsg . ExtendedTextMessage != nil :
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"
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"
case waMsg . ProtocolMessage != nil :
2021-10-26 16:01:10 +02:00
switch waMsg . GetProtocolMessage ( ) . GetType ( ) {
case waProto . ProtocolMessage_REVOKE :
return "revoke"
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 :
return "unknown_protocol"
}
2021-11-01 14:30:56 +01:00
case isPotentiallyInteresting ( waMsg ) :
2021-10-26 16:01:10 +02:00
return "unknown"
2021-10-31 19:58:30 +01:00
default :
return "ignore"
2021-10-22 19:14:34 +02:00
}
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 :
2021-10-26 16:01:10 +02:00
return portal . convertTextMessage ( intent , 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 ( ) )
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 ( ) )
2019-05-31 21:30:57 +02:00
default :
2021-10-26 16:01:10 +02:00
return nil
}
}
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
} else if portal . isRecentlyHandled ( evt . Info . ID , true ) {
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 )
}
portal . finishHandling ( nil , & evt . Info , resp . EventID , true )
}
2021-11-02 14:46:31 +01:00
func ( portal * Portal ) handleFakeMessage ( msg fakeMessage ) {
if portal . isRecentlyHandled ( msg . ID , false ) {
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 ,
} ,
} , resp . EventID , false )
}
}
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
2021-10-27 17:31:33 +02:00
} else if portal . isRecentlyHandled ( msgID , false ) {
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 {
if existingMsg . DecryptionError {
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 {
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 {
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 {
eventID = resp . EventID
}
}
if len ( eventID ) != 0 {
2021-10-27 17:31:33 +02:00
portal . finishHandling ( existingMsg , & evt . Info , resp . EventID , false )
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" ,
} )
2021-10-28 11:59:22 +02:00
existingMsg . UpdateMXID ( "net.maunium.whatsapp.fake::" + existingMsg . MXID , false )
2021-10-27 17:31:33 +02:00
}
2021-10-26 16:01:10 +02:00
} else {
2021-10-30 13:47:46 +02:00
portal . log . Warnfln ( "Unhandled message: %+v / %+v" , evt . Info , evt . Message )
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" ,
} )
2021-10-28 11:59:22 +02:00
existingMsg . UpdateMXID ( "net.maunium.whatsapp.fake::" + existingMsg . MXID , false )
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
}
2021-10-27 17:31:33 +02:00
func ( portal * Portal ) isRecentlyHandled ( id types . MessageID , decryptionError bool ) bool {
2018-08-30 23:13:08 +02:00
start := portal . recentlyHandledIndex
2021-10-27 17:31:33 +02:00
lookingForMsg := recentlyHandledWrapper { id , decryptionError }
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
}
2018-09-01 22:38:03 +02:00
func init ( ) {
gob . Register ( & waProto . Message { } )
}
2021-10-27 17:31:33 +02:00
func ( portal * Portal ) markHandled ( msg * database . Message , info * types . MessageInfo , mxid id . EventID , isSent , recent , decryptionError bool ) * database . Message {
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
msg . DecryptionError = decryptionError
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 {
msg . UpdateMXID ( mxid , decryptionError )
}
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 ( )
2021-10-27 17:31:33 +02:00
portal . recentlyHandled [ index ] = recentlyHandledWrapper { msg . JID , decryptionError }
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
}
2021-10-27 17:31:33 +02:00
func ( portal * Portal ) finishHandling ( existing * database . Message , message * types . MessageInfo , mxid id . EventID , decryptionError bool ) {
portal . markHandled ( existing , message , mxid , true , true , decryptionError )
2020-06-05 16:54:09 +02:00
portal . sendDeliveryReceipt ( mxid )
2021-10-27 17:31:33 +02:00
if ! decryptionError {
portal . log . Debugln ( "Handled message" , message . ID , "->" , mxid )
} else {
portal . log . Debugln ( "Handled message" , message . ID , "->" , mxid , "(undecryptable message error notice)" )
}
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 )
if user != nil {
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 {
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 {
portal . log . Warnln ( "Failed to set room topic:" , err )
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
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-10-28 12:57:15 +02:00
func ( portal * Portal ) ensureUserInvited ( user * User ) ( ok bool ) {
2021-08-18 15:24:13 +02:00
inviteContent := event . Content {
Parsed : & event . MemberEventContent {
Membership : event . MembershipInvite ,
2021-10-22 19:14:34 +02:00
IsDirect : portal . IsPrivateChat ( ) ,
2021-08-18 15:24:13 +02:00
} ,
Raw : map [ string ] interface { } { } ,
}
2019-05-30 16:22:03 +02:00
customPuppet := portal . bridge . GetPuppetByCustomMXID ( user . MXID )
2019-05-30 19:25:04 +02:00
if customPuppet != nil && customPuppet . CustomIntent ( ) != nil {
2021-08-18 15:24:13 +02:00
inviteContent . Raw [ "fi.mau.will_auto_accept" ] = true
}
_ , err := portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateMember , user . MXID . String ( ) , & inviteContent )
var httpErr mautrix . HTTPError
if err != nil && errors . As ( err , & httpErr ) && httpErr . RespError != nil && strings . Contains ( httpErr . RespError . Err , "is already in the room" ) {
portal . bridge . StateStore . SetMembership ( portal . MXID , user . MXID , event . MembershipJoin )
2021-10-28 12:57:15 +02:00
ok = true
2021-08-18 15:24:13 +02:00
} else if err != nil {
portal . log . Warnfln ( "Failed to invite %s: %v" , user . MXID , err )
2021-10-28 12:57:15 +02:00
} else {
ok = true
2021-08-18 15:24:13 +02:00
}
if customPuppet != nil && customPuppet . CustomIntent ( ) != nil {
err = customPuppet . CustomIntent ( ) . EnsureJoined ( portal . MXID )
if err != nil {
portal . log . Warnfln ( "Failed to auto-join portal as %s: %v" , user . MXID , err )
2021-10-28 12:57:15 +02:00
ok = false
} else {
ok = true
2021-08-18 15:24:13 +02:00
}
2019-05-30 16:22:03 +02:00
}
2021-10-28 12:57:15 +02:00
return
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 )
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" ,
AvatarURL : id . ContentURIString ( portal . bridge . Config . AppService . Bot . Avatar ) ,
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 )
}
}
2021-11-03 13:43:53 +01:00
func ( portal * Portal ) CreateMatrixRoom ( user * User , groupInfo * types . GroupInfo , isFullInfo 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 )
2021-10-31 18:59:23 +01:00
if err != nil {
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-28 21:14:09 +01:00
portal . addToSpace ( user . getSpaceRoom ( ) , portal . MXID )
2021-10-31 18:59:23 +01:00
if groupInfo != nil {
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 ( )
}
2018-08-18 21:57:08 +02:00
return nil
}
2021-12-28 21:14:09 +01:00
func ( portal * Portal ) addToSpace ( spaceID id . RoomID , portalID id . RoomID ) {
parentSpaceContent := make ( map [ string ] interface { } )
parentSpaceContent [ "via" ] = [ ] string { "matrix.wounn.xyz" }
portal . log . Errorln ( "adding room " + portalID + " to the space " + spaceID )
portal . MainIntent ( ) . SendStateEvent ( spaceID , event . Type { Type : "m.space.child" , Class : event . StateEventType } , portalID . String ( ) , parentSpaceContent )
}
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
}
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
}
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
}
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
ReplyTo types . MessageID
2021-10-26 16:01:10 +02:00
}
2018-08-23 00:12:26 +02:00
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) convertTextMessage ( intent * appservice . IntentAPI , 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
}
2021-11-05 10:47:51 +01:00
var replyTo types . MessageID
2021-10-26 16:01:10 +02:00
if msg . GetExtendedTextMessage ( ) != nil {
content . Body = msg . GetExtendedTextMessage ( ) . GetText ( )
2018-08-24 21:05:38 +02:00
2021-10-26 16:01:10 +02:00
contextInfo := msg . GetExtendedTextMessage ( ) . GetContextInfo ( )
2021-10-22 19:14:34 +02:00
if contextInfo != nil {
portal . bridge . Formatter . ParseWhatsApp ( content , contextInfo . GetMentionedJid ( ) )
2021-11-05 10:47:51 +01:00
replyTo = contextInfo . GetStanzaId ( )
2021-10-22 19:14:34 +02:00
}
2018-08-23 00:12:26 +02:00
}
2018-08-19 17:21:38 +02:00
2021-11-05 10:47:51 +01:00
return & ConvertedMessage { Intent : intent , Type : event . EventMessage , Content : content , ReplyTo : replyTo }
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 {
Intent : intent ,
Type : event . EventMessage ,
Content : content ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
}
}
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 {
2021-11-06 20:30:27 +01:00
Intent : intent ,
Type : event . EventMessage ,
2021-11-05 10:47:51 +01:00
Content : content ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
}
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"
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 ,
}
extraAttrs := map [ string ] interface { } {
inviteMetaField : map [ string ] interface { } {
"jid" : msg . GetGroupJid ( ) ,
"code" : msg . GetInviteCode ( ) ,
"expiration" : strconv . FormatInt ( msg . GetInviteExpiration ( ) , 10 ) ,
"inviter" : info . Sender . ToNonAD ( ) . String ( ) ,
} ,
}
2021-11-05 10:47:51 +01:00
return & ConvertedMessage {
2021-11-06 20:30:27 +01:00
Intent : intent ,
Type : event . EventMessage ,
2021-11-05 10:47:51 +01:00
Content : content ,
2021-11-06 20:30:27 +01:00
Extra : extraAttrs ,
2021-11-05 10:47:51 +01:00
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
}
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 {
2021-11-06 20:30:27 +01:00
Intent : intent ,
Type : event . EventMessage ,
2021-11-05 10:47:51 +01:00
Content : content ,
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
}
2020-06-20 21:24:27 +02:00
}
2020-06-10 14:26:14 +02:00
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
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) makeMediaBridgeFailureMessage ( intent * appservice . IntentAPI , info * types . MessageInfo , bridgeErr error , captionContent * event . MessageEventContent ) * ConvertedMessage {
portal . log . Errorfln ( "Failed to bridge media for %s: %v" , info . ID , bridgeErr )
return & ConvertedMessage { Intent : intent , Type : event . EventMessage , Content : & event . MessageEventContent {
MsgType : event . MsgNotice ,
Body : "Failed to bridge media" ,
} , Caption : captionContent }
}
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
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
}
2021-12-25 21:40:10 +01:00
// MimeExtensionSanityOverrides includes extensions for various common mimetypes.
//
// This is necessary because sometimes the OS mimetype database and Go interact in weird ways,
// which causes very obscure extensions to be first in the array for common mimetypes
// (e.g. image/jpeg -> .jpe, text/plain -> ,v).
var MimeExtensionSanityOverrides = map [ string ] string {
"image/png" : ".png" ,
"image/webp" : ".webp" ,
"image/jpeg" : ".jpg" ,
"image/tiff" : ".tiff" ,
"image/heif" : ".heic" ,
"image/heic" : ".heic" ,
"audio/mpeg" : ".mp3" ,
"audio/ogg" : ".ogg" ,
"audio/webm" : ".webm" ,
"video/mp4" : ".mp4" ,
"video/mpeg" : ".mpeg" ,
"video/webm" : ".webm" ,
"text/plain" : ".txt" ,
"text/html" : ".html" ,
"application/xml" : ".xml" ,
}
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) convertMediaMessage ( intent * appservice . IntentAPI , source * User , info * types . MessageInfo , msg MediaMessage ) * ConvertedMessage {
messageWithCaption , ok := msg . ( MediaMessageWithCaption )
var captionContent * event . MessageEventContent
if ok && len ( messageWithCaption . GetCaption ( ) ) > 0 {
captionContent = & event . MessageEventContent {
Body : messageWithCaption . GetCaption ( ) ,
MsgType : event . MsgNotice ,
}
portal . bridge . Formatter . ParseWhatsApp ( captionContent , msg . GetContextInfo ( ) . GetMentionedJid ( ) )
2018-08-19 17:21:38 +02:00
}
2018-08-23 00:12:26 +02:00
2021-10-22 19:14:34 +02:00
data , err := source . Client . Download ( msg )
// TODO can these errors still be handled?
//if errors.Is(err, whatsapp.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsapp.ErrMediaDownloadFailedWith410) {
// portal.log.Warnfln("Failed to download media for %s: %v. Calling LoadMediaInfo and retrying download...", msg.info.Id, err)
// _, err = source.Conn.LoadMediaInfo(msg.info.RemoteJid, msg.info.Id, msg.info.FromMe)
// if err != nil {
// portal.sendMediaBridgeFailure(source, intent, msg.info, fmt.Errorf("failed to load media info: %w", err))
// return true
// }
// data, err = msg.download()
//}
if errors . Is ( err , whatsmeow . ErrNoURLPresent ) {
portal . log . Debugfln ( "No URL present error for media message %s, ignoring..." , info . ID )
2021-10-26 16:01:10 +02:00
return nil
2019-12-31 19:17:03 +01:00
} else if err != nil {
2021-10-26 16:01:10 +02:00
return portal . makeMediaBridgeFailureMessage ( intent , info , err , captionContent )
2018-08-19 17:21:38 +02:00
}
2018-08-23 23:52:06 +02:00
2020-06-10 14:26:14 +02:00
var width , height int
2021-12-09 18:20:52 +01:00
messageWithDimensions , ok := msg . ( MediaMessageWithDimensions )
if ok {
width = int ( messageWithDimensions . GetWidth ( ) )
height = int ( messageWithDimensions . GetHeight ( ) )
}
if width == 0 && height == 0 && strings . HasPrefix ( msg . GetMimetype ( ) , "image/" ) {
2020-06-10 14:26:14 +02:00
cfg , _ , _ := image . DecodeConfig ( bytes . NewReader ( data ) )
width , height = cfg . Width , cfg . Height
}
2021-10-22 19:14:34 +02:00
data , uploadMimeType , file := portal . encryptFile ( data , msg . GetMimetype ( ) )
2020-06-10 14:26:14 +02:00
2021-04-15 15:23:40 +02:00
uploaded , err := intent . UploadBytes ( data , uploadMimeType )
2018-08-19 17:21:38 +02:00
if err != nil {
2020-09-24 14:25:36 +02:00
if errors . Is ( err , mautrix . MTooLarge ) {
2021-10-26 16:01:10 +02:00
return portal . makeMediaBridgeFailureMessage ( intent , info , errors . New ( "homeserver rejected too large file" ) , captionContent )
2021-03-04 19:46:08 +01:00
} else if httpErr , ok := err . ( mautrix . HTTPError ) ; ok && httpErr . IsStatus ( 413 ) {
2021-10-26 16:01:10 +02:00
return portal . makeMediaBridgeFailureMessage ( intent , info , errors . New ( "proxy rejected too large file" ) , captionContent )
2020-07-21 15:08:54 +02:00
} else {
2021-10-26 16:01:10 +02:00
return portal . makeMediaBridgeFailureMessage ( intent , info , fmt . Errorf ( "failed to upload media: %w" , err ) , captionContent )
2020-07-21 15:08:54 +02:00
}
2018-08-19 17:21:38 +02:00
}
2018-08-27 22:15:05 +02:00
2021-10-22 19:14:34 +02:00
content := & event . MessageEventContent {
File : file ,
Info : & event . FileInfo {
Size : len ( data ) ,
MimeType : msg . GetMimetype ( ) ,
Width : width ,
Height : height ,
} ,
}
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
2021-12-25 21:40:10 +01:00
ext , ok := MimeExtensionSanityOverrides [ strings . Split ( msg . GetMimetype ( ) , ";" ) [ 0 ] ]
if ! ok {
exts , _ := mime . ExtensionsByType ( msg . GetMimetype ( ) )
if len ( exts ) > 0 {
ext = exts [ 0 ]
}
2020-06-20 17:26:45 +02:00
}
2021-12-25 21:40:10 +01:00
content . Body += ext
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
2020-06-10 14:26:14 +02:00
if content . File != nil {
content . File . URL = uploaded . ContentURI . CUString ( )
} else {
content . URL = uploaded . ContentURI . CUString ( )
}
2018-08-23 23:52:06 +02:00
2021-10-26 16:01:10 +02:00
messageWithThumbnail , ok := msg . ( MediaMessageWithThumbnail )
if ok && messageWithThumbnail . GetJpegThumbnail ( ) != nil && portal . bridge . Config . Bridge . WhatsappThumbnail {
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 {
2021-10-22 19:14:34 +02:00
portal . log . Warnfln ( "Failed to upload thumbnail for %s: %v" , info . ID , 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
2021-10-26 16:01:10 +02:00
return & ConvertedMessage {
Intent : intent ,
Type : eventType ,
Content : content ,
Caption : captionContent ,
2021-11-05 10:47:51 +01:00
ReplyTo : msg . GetContextInfo ( ) . GetStanzaId ( ) ,
2018-08-27 22:15:05 +02:00
}
2018-08-19 17:21:38 +02:00
}
2021-11-11 19:33:22 +01:00
const thumbnailMaxSize = 72
const thumbnailMinSize = 24
func createJPEGThumbnail ( source [ ] byte ) ( [ ] byte , error ) {
src , _ , err := image . Decode ( bytes . NewReader ( source ) )
2020-05-08 21:32:22 +02:00
if err != nil {
2021-11-11 19:33:22 +01:00
return nil , 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 {
2021-11-11 19:33:22 +01:00
return nil , fmt . Errorf ( "failed to re-encode thumbnail: %w" , err )
}
return buf . Bytes ( ) , nil
}
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
}
2020-06-23 15:36:05 +02:00
func ( portal * Portal ) convertGifToVideo ( gif [ ] byte ) ( [ ] byte , error ) {
2021-10-31 12:30:19 +01:00
dir , err := os . MkdirTemp ( "" , "gif-convert-*" )
2020-06-23 15:36:05 +02:00
if err != nil {
2020-10-05 21:38:34 +02:00
return nil , fmt . Errorf ( "failed to make temp dir: %w" , err )
2020-06-23 15:36:05 +02:00
}
defer os . RemoveAll ( dir )
inputFile , err := os . OpenFile ( filepath . Join ( dir , "input.gif" ) , os . O_CREATE | os . O_EXCL | os . O_WRONLY , 0600 )
if err != nil {
2020-10-05 21:38:34 +02:00
return nil , fmt . Errorf ( "failed open input file: %w" , err )
2020-06-23 15:36:05 +02:00
}
_ , err = inputFile . Write ( gif )
if err != nil {
_ = inputFile . Close ( )
2020-10-05 21:38:34 +02:00
return nil , fmt . Errorf ( "failed to write gif to input file: %w" , err )
2020-06-23 15:36:05 +02:00
}
_ = inputFile . Close ( )
outputFileName := filepath . Join ( dir , "output.mp4" )
cmd := exec . Command ( "ffmpeg" , "-hide_banner" , "-loglevel" , "warning" ,
"-f" , "gif" , "-i" , inputFile . Name ( ) ,
"-pix_fmt" , "yuv420p" , "-c:v" , "libx264" , "-movflags" , "+faststart" ,
"-filter:v" , "crop='floor(in_w/2)*2:floor(in_h/2)*2'" ,
outputFileName )
2021-02-18 22:36:14 +01:00
vcLog := portal . log . Sub ( "VideoConverter" ) . Writer ( log . LevelWarn )
2020-06-23 15:36:05 +02:00
cmd . Stdout = vcLog
cmd . Stderr = vcLog
err = cmd . Run ( )
if err != nil {
2020-10-05 21:38:34 +02:00
return nil , fmt . Errorf ( "failed to run ffmpeg: %w" , err )
2020-06-23 15:36:05 +02:00
}
outputFile , err := os . OpenFile ( filepath . Join ( dir , "output.mp4" ) , os . O_RDONLY , 0 )
if err != nil {
2020-10-05 21:38:34 +02:00
return nil , fmt . Errorf ( "failed to open output file: %w" , err )
2020-06-23 15:36:05 +02:00
}
defer func ( ) {
_ = outputFile . Close ( )
_ = os . Remove ( outputFile . Name ( ) )
} ( )
2021-10-31 12:30:19 +01:00
mp4 , err := io . ReadAll ( outputFile )
2020-06-23 15:36:05 +02:00
if err != nil {
2020-10-05 21:38:34 +02:00
return nil , fmt . Errorf ( "failed to read mp4 from output file: %w" , err )
2020-06-23 15:36:05 +02:00
}
return mp4 , 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" {
2020-06-23 15:36:05 +02:00
data , err = portal . convertGifToVideo ( data )
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
}
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 )
2021-11-02 14:46:31 +01:00
if replyToMsg != nil && ! replyToMsg . IsFakeJID ( ) {
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
}
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
}
2018-08-25 23:26:24 +02:00
if ctxInfo . StanzaId != nil || ctxInfo . MentionedJid != nil {
2021-10-22 19:14:34 +02:00
msg . ExtendedTextMessage = & waProto . ExtendedTextMessage {
2018-08-25 23:26:24 +02:00
Text : & text ,
2021-10-22 19:14:34 +02:00
ContextInfo : & ctxInfo ,
2018-08-25 23:26:24 +02:00
}
} else {
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" ]
_ , isMSC2516Voice := evt . Content . Raw [ "org.matrix.msc2516.voice" ]
if isMSC3245Voice || isMSC2516Voice {
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
}
2021-10-22 19:14:34 +02:00
info := portal . generateMessageInfo ( sender )
2021-10-27 17:31:33 +02:00
dbMsg := portal . markHandled ( nil , info , evt . ID , false , true , false )
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
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
}
2021-10-27 20:34:22 +02:00
portal . log . Debugfln ( "Sending redaction %s of %s/%s to WhatsApp" , evt . ID , msg . MXID , msg . JID )
2021-10-31 22:36:41 +01:00
_ , 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
2021-11-30 15:38:37 +01:00
func ( portal * Portal ) HandleMatrixReadReceipt ( sender * User , eventID id . EventID , receiptTimestamp time . Time ) {
2021-12-07 15:02:51 +01:00
if ! sender . IsLoggedIn ( ) {
portal . log . Debugfln ( "Ignoring read receipt by %s: user is not connected to WhatsApp" , sender . JID )
return
}
2021-11-30 15:38:37 +01:00
maxTimestamp := receiptTimestamp
if message := portal . bridge . DB . Message . GetByMXID ( eventID ) ; message != nil {
maxTimestamp = message . Timestamp
}
prevTimestamp := sender . GetLastReadTS ( portal . Key )
if prevTimestamp . IsZero ( ) {
prevTimestamp = maxTimestamp . Add ( - 2 * time . Second )
}
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
}
portal . log . Debugfln ( "Sending read receipts by %s: %v" , sender . JID , groupedMessages )
for messageSender , ids := range groupedMessages {
2021-12-25 19:50:36 +01:00
chatJID := portal . Key . JID
if messageSender . Server == types . BroadcastServer {
chatJID = messageSender
messageSender = portal . Key . JID
}
err := sender . Client . MarkRead ( ids , receiptTimestamp , chatJID , messageSender )
2021-11-30 15:38:37 +01:00
if err != nil {
portal . log . Warnfln ( "Failed to mark %v as read by %s: %v" , ids , sender . JID , err )
}
}
}
2021-12-07 15:02:51 +01:00
func typingDiff ( prev , new [ ] id . UserID ) ( started , stopped [ ] id . UserID ) {
OuterNew :
for _ , userID := range new {
for _ , previousUserID := range prev {
if userID == previousUserID {
continue OuterNew
}
}
started = append ( started , userID )
}
OuterPrev :
for _ , userID := range prev {
for _ , previousUserID := range new {
if userID == previousUserID {
continue OuterPrev
}
}
stopped = append ( stopped , userID )
}
return
}
func ( portal * Portal ) setTyping ( userIDs [ ] id . UserID , state types . ChatPresence ) {
for _ , userID := range userIDs {
user := portal . bridge . GetUserByMXIDIfExists ( userID )
if user == nil || ! user . IsLoggedIn ( ) {
continue
}
portal . log . Debugfln ( "Bridging typing change from %s to chat presence %s" , state , user . MXID )
err := user . Client . SendChatPresence ( state , portal . Key . JID )
if err != nil {
portal . log . Warnln ( "Error sending chat presence:" , err )
}
}
}
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 {
portal . log . Debugln ( "Ignoring %s from %s as user is not connected" , evtType , sender . MXID )
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
}
2020-06-25 22:58:35 +02:00
func ( portal * Portal ) HandleMatrixKick ( sender * User , evt * event . Event ) {
puppet := portal . bridge . GetPuppetByMXID ( id . UserID ( evt . GetStateKey ( ) ) )
2020-06-25 22:33:11 +02:00
if puppet != nil {
2021-11-05 11:17:56 +01:00
_ , err := sender . Client . UpdateGroupParticipants ( portal . Key . JID , map [ types . JID ] whatsmeow . ParticipantChange {
puppet . JID : whatsmeow . ParticipantChangeRemove ,
} )
if err != nil {
portal . log . Errorfln ( "Failed to kick %s from group as %s: %v" , puppet . JID , sender . MXID , err )
return
}
2021-10-22 19:14:34 +02:00
//portal.log.Infoln("Kick %s response: %s", puppet.JID, <-resp)
2020-06-25 22:33:11 +02:00
}
2019-05-16 19:14:32 +02:00
}
2020-06-25 22:58:35 +02:00
func ( portal * Portal ) HandleMatrixInvite ( sender * User , evt * event . Event ) {
puppet := portal . bridge . GetPuppetByMXID ( id . UserID ( evt . GetStateKey ( ) ) )
if puppet != nil {
2021-11-05 11:17:56 +01:00
_ , err := sender . Client . UpdateGroupParticipants ( portal . Key . JID , map [ types . JID ] whatsmeow . ParticipantChange {
puppet . JID : whatsmeow . ParticipantChangeAdd ,
} )
if err != nil {
portal . log . Errorfln ( "Failed to add %s to group as %s: %v" , puppet . JID , sender . MXID , err )
return
}
2021-10-22 19:14:34 +02: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 ) {
var err error
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
err = sender . Client . SetGroupName ( portal . Key . JID , content . Name )
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
err = sender . Client . SetGroupTopic ( portal . Key . JID , "" , "" , content . Topic )
2021-02-09 22:41:14 +01:00
case * event . RoomAvatarEventContent :
2021-11-05 11:17:56 +01:00
// TODO implement
2021-02-09 22:41:14 +01:00
return
}
if err != nil {
portal . log . Errorln ( "Failed to update metadata:" , err )
} else {
2021-10-22 19:14:34 +02:00
//out := <-resp
//portal.log.Debugln("Successfully updated metadata:", out)
2021-02-09 22:41:14 +01:00
}
}