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"
2018-08-25 23:26:24 +02:00
"image/gif"
"image/jpeg"
"image/png"
2020-06-23 15:36:05 +02:00
"io/ioutil"
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-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-10-27 17:31:33 +02:00
"maunium.net/go/mautrix/format"
2021-10-22 19:14:34 +02:00
"go.mau.fi/whatsmeow"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
2019-05-16 00:59:36 +02:00
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"
"maunium.net/go/mautrix/id"
2019-01-11 20:17:31 +01:00
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 ) ) ,
2020-11-18 12:29:47 +01:00
messages : make ( chan PortalMessage , 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
2020-11-18 12:29:47 +01:00
messages : make ( chan PortalMessage , 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
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
source * User
}
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 ( )
2019-05-21 22:44:14 +02:00
messages chan PortalMessage
2021-10-28 12:57:15 +02:00
relayUser * User
2018-08-30 23:13:08 +02:00
}
2019-05-21 22:44:14 +02:00
func ( portal * Portal ) handleMessageLoop ( ) {
2020-11-18 12:29:47 +01:00
for msg := range portal . messages {
if len ( portal . MXID ) == 0 {
2021-10-22 19:14:34 +02:00
if ! portal . shouldCreateRoom ( msg ) {
2021-03-19 20:14:01 +01:00
portal . log . Debugln ( "Not creating portal room for incoming message: message is not a chat message" )
2020-11-18 12:29:47 +01:00
continue
}
portal . log . Debugln ( "Creating Matrix room from incoming message" )
err := portal . CreateMatrixRoom ( msg . source )
if err != nil {
portal . log . Errorln ( "Failed to create portal room:" , err )
2021-06-19 01:22:21 +02:00
continue
2020-11-18 12:29:47 +01:00
}
2020-11-16 13:28:08 +01:00
}
2021-10-27 17:31:33 +02:00
if msg . evt != nil {
portal . handleMessage ( msg . source , msg . evt )
} else if msg . undecryptable != nil {
portal . handleUndecryptableMessage ( msg . source , msg . undecryptable )
} else {
portal . log . Warnln ( "Unexpected PortalMessage with no message: %+v" , msg )
}
2020-11-16 13:28:08 +01:00
}
}
2021-03-19 20:14:01 +01:00
func ( portal * Portal ) shouldCreateRoom ( msg PortalMessage ) bool {
2021-10-27 17:31:33 +02:00
if msg . undecryptable != nil {
return true
}
2021-10-22 19:14:34 +02:00
waMsg := msg . evt . Message
supportedMessages := [ ] interface { } {
waMsg . Conversation ,
waMsg . ExtendedTextMessage ,
waMsg . ImageMessage ,
waMsg . StickerMessage ,
waMsg . VideoMessage ,
waMsg . AudioMessage ,
waMsg . VideoMessage ,
waMsg . DocumentMessage ,
waMsg . ContactMessage ,
waMsg . LocationMessage ,
}
for _ , message := range supportedMessages {
if message != nil {
return true
}
2021-03-19 20:14:01 +01:00
}
2021-10-22 19:14:34 +02:00
return false
2021-03-19 20:14:01 +01:00
}
2021-10-26 16:01:10 +02:00
func ( portal * Portal ) getMessageType ( waMsg * waProto . Message ) string {
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"
case waMsg . GetProtocolMessage ( ) != nil :
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"
}
default :
return "unknown"
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 ( ) )
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-10-27 17:44:17 +02:00
content := undecryptableMessageContent
2021-10-27 17:31:33 +02:00
resp , err := portal . sendMessage ( intent , event . EventMessage , & content , evt . Info . Timestamp . UnixMilli ( ) )
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-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
msgType := portal . getMessageType ( evt . Message )
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 )
converted := portal . convertMessage ( intent , source , & evt . Info , evt . Message )
if converted != nil {
var eventID id . EventID
2021-10-27 17:31:33 +02:00
if existingMsg != nil {
converted . Content . SetEdit ( existingMsg . MXID )
}
2021-10-26 16:01:10 +02:00
resp , err := portal . sendMessage ( converted . Intent , converted . Type , converted . Content , evt . Info . Timestamp . UnixMilli ( ) )
if err != nil {
portal . log . Errorln ( "Failed to send %s to Matrix: %v" , msgID , err )
} 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-26 16:01:10 +02:00
resp , err = portal . sendMessage ( converted . Intent , converted . Type , converted . Content , evt . Info . Timestamp . UnixMilli ( ) )
if err != nil {
portal . log . Errorln ( "Failed to send caption of %s to Matrix: %v" , msgID , err )
} 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 {
portal . log . Warnln ( "Unhandled message:" , 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
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 )
puppet . SyncContact ( user , true )
return puppet
}
}
2021-10-22 19:14:34 +02:00
func ( portal * Portal ) getMessageIntent ( user * User , info * types . MessageInfo ) * appservice . IntentAPI {
if info . IsFromMe {
2020-06-10 13:58:57 +02:00
return portal . bridge . GetPuppetByJID ( user . JID ) . IntentFor ( portal )
} else if portal . IsPrivateChat ( ) {
return portal . MainIntent ( )
}
2021-10-22 19:14:34 +02:00
puppet := portal . bridge . GetPuppetByJID ( info . Sender )
puppet . SyncContact ( user , true )
2021-06-15 11:34:55 +02:00
return puppet . 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-10-22 19:14:34 +02:00
puppet . SyncContact ( source , true )
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-10-22 19:14:34 +02:00
if participant . JID == metadata . OwnerJID {
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
}
2018-08-28 23:40:54 +02:00
func ( portal * Portal ) UpdateMetadata ( user * User ) 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-10-22 19:14:34 +02:00
metadata , err := user . Client . GetGroupInfo ( portal . Key . JID )
2018-08-23 00:12:26 +02:00
if err != nil {
2021-10-22 19:14:34 +02:00
portal . log . Errorln ( "Failed to get group info:" , err )
2018-12-05 09:20:39 +01:00
return false
}
2021-03-19 19:55:08 +01:00
portal . SyncParticipants ( user , metadata )
2018-08-23 00:12:26 +02:00
update := false
2021-10-28 11:59:22 +02:00
update = portal . UpdateName ( metadata . Name , metadata . NameSetBy , false ) || update
update = portal . UpdateTopic ( metadata . Topic , metadata . TopicSetBy , false ) || update
2020-10-12 12:59:14 +02:00
2021-10-22 19:14:34 +02:00
portal . RestrictMessageSending ( metadata . IsAnnounce )
portal . RestrictMetadataChanges ( metadata . 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-10-22 19:14:34 +02:00
func ( portal * Portal ) Sync ( user * User ) bool {
2019-05-22 15:46:18 +02:00
portal . log . Infoln ( "Syncing portal for" , user . MXID )
2018-08-28 23:40:54 +02:00
2018-08-19 17:21:38 +02:00
if len ( portal . MXID ) == 0 {
2019-03-13 23:38:11 +01:00
err := portal . CreateMatrixRoom ( user )
2018-08-19 17:21:38 +02:00
if err != nil {
portal . log . Errorln ( "Failed to create portal room:" , err )
2021-06-01 14:28:15 +02:00
return false
2018-08-19 17:21:38 +02:00
}
2018-08-30 23:13:08 +02:00
} else {
2019-05-30 16:22:03 +02:00
portal . ensureUserInvited ( user )
2018-08-19 17:21:38 +02:00
}
2018-08-23 00:12:26 +02:00
update := false
2018-08-28 23:40:54 +02:00
update = portal . UpdateMetadata ( user ) || 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-10-26 16:01:10 +02:00
func ( portal * Portal ) parseWebMessageInfo ( webMsg * waProto . WebMessageInfo ) * types . MessageInfo {
info := types . MessageInfo {
MessageSource : types . MessageSource {
Chat : portal . Key . JID ,
IsFromMe : webMsg . GetKey ( ) . GetFromMe ( ) ,
IsGroup : false ,
} ,
ID : webMsg . GetKey ( ) . GetId ( ) ,
PushName : webMsg . GetPushName ( ) ,
Timestamp : time . Unix ( int64 ( webMsg . GetMessageTimestamp ( ) ) , 0 ) ,
}
var err error
if info . IsFromMe {
info . Sender = portal . Key . Receiver
} else if portal . IsPrivateChat ( ) {
info . Sender = portal . Key . JID
} else if webMsg . GetParticipant ( ) != "" {
info . Sender , err = types . ParseJID ( webMsg . GetParticipant ( ) )
} else if webMsg . GetKey ( ) . GetParticipant ( ) != "" {
info . Sender , err = types . ParseJID ( webMsg . GetKey ( ) . GetParticipant ( ) )
}
if info . Sender . IsEmpty ( ) {
portal . log . Warnfln ( "Failed to get sender of message %s (parse error: %v)" , info . ID , err )
return nil
}
return & info
}
const backfillIDField = "net.maunium.whatsapp.id"
func ( portal * Portal ) wrapBatchEvent ( info * types . MessageInfo , intent * appservice . IntentAPI , eventType event . Type , content * event . MessageEventContent ) ( * event . Event , error ) {
wrappedContent := event . Content {
Parsed : content ,
Raw : map [ string ] interface { } {
backfillIDField : info . ID ,
} ,
}
if intent . IsCustomPuppet {
wrappedContent . Raw [ "net.maunium.whatsapp.puppet" ] = intent . IsCustomPuppet
}
newEventType , err := portal . encrypt ( & wrappedContent , eventType )
if err != nil {
return nil , err
}
return & event . Event {
Sender : intent . UserID ,
Type : newEventType ,
Timestamp : info . Timestamp . UnixMilli ( ) ,
Content : wrappedContent ,
} , nil
}
func ( portal * Portal ) appendBatchEvents ( converted * ConvertedMessage , info * types . MessageInfo , eventsArray * [ ] * event . Event , infoArray * [ ] * types . MessageInfo ) error {
mainEvt , err := portal . wrapBatchEvent ( info , converted . Intent , converted . Type , converted . Content )
if err != nil {
return err
}
if converted . Caption != nil {
captionEvt , err := portal . wrapBatchEvent ( info , converted . Intent , converted . Type , converted . Caption )
if err != nil {
return err
}
* eventsArray = append ( * eventsArray , mainEvt , captionEvt )
* infoArray = append ( * infoArray , nil , info )
} else {
* eventsArray = append ( * eventsArray , mainEvt )
* infoArray = append ( * infoArray , info )
}
return nil
}
func ( portal * Portal ) finishBatch ( eventIDs [ ] id . EventID , infos [ ] * types . MessageInfo ) {
if len ( eventIDs ) != len ( infos ) {
portal . log . Errorfln ( "Length of event IDs (%d) and message infos (%d) doesn't match! Using slow path for mapping event IDs" , len ( eventIDs ) , len ( infos ) )
infoMap := make ( map [ types . MessageID ] * types . MessageInfo , len ( infos ) )
for _ , info := range infos {
infoMap [ info . ID ] = info
}
for _ , eventID := range eventIDs {
if evt , err := portal . MainIntent ( ) . GetEvent ( portal . MXID , eventID ) ; err != nil {
portal . log . Warnfln ( "Failed to get event %s to register it in the database: %v" , eventID , err )
} else if msgID , ok := evt . Content . Raw [ backfillIDField ] . ( string ) ; ! ok {
portal . log . Warnfln ( "Event %s doesn't include the WhatsApp message ID" , eventID )
} else if info , ok := infoMap [ types . MessageID ( msgID ) ] ; ! ok {
portal . log . Warnfln ( "Didn't find info of message %s (event %s) to register it in the database" , msgID , eventID )
} else {
2021-10-27 17:31:33 +02:00
portal . markHandled ( nil , info , eventID , true , false , false )
2021-10-26 16:01:10 +02:00
}
}
} else {
for i := 0 ; i < len ( infos ) ; i ++ {
if infos [ i ] != nil {
2021-10-27 17:31:33 +02:00
portal . markHandled ( nil , infos [ i ] , eventIDs [ i ] , true , false , false )
2021-10-26 16:01:10 +02:00
}
}
portal . log . Infofln ( "Successfully sent %d events" , len ( eventIDs ) )
}
}
func ( portal * Portal ) backfill ( source * User , messages [ ] * waProto . HistorySyncMsg ) {
portal . backfillLock . Lock ( )
defer portal . backfillLock . Unlock ( )
var historyBatch , newBatch mautrix . ReqBatchSend
var historyBatchInfos , newBatchInfos [ ] * types . MessageInfo
firstMsgTimestamp := time . Unix ( int64 ( messages [ len ( messages ) - 1 ] . GetMessage ( ) . GetMessageTimestamp ( ) ) , 0 )
2021-10-26 20:30:42 +02:00
historyBatch . StateEventsAtStart = make ( [ ] * event . Event , 1 )
newBatch . StateEventsAtStart = make ( [ ] * event . Event , 1 )
2021-10-27 18:48:14 +02:00
// TODO remove the dummy state events after https://github.com/matrix-org/synapse/pull/11188
2021-10-26 20:30:42 +02:00
emptyStr := ""
dummyStateEvent := event . Event {
Type : BackfillDummyStateEvent ,
Sender : portal . MainIntent ( ) . UserID ,
StateKey : & emptyStr ,
Timestamp : firstMsgTimestamp . UnixMilli ( ) ,
Content : event . Content { } ,
}
historyBatch . StateEventsAtStart [ 0 ] = & dummyStateEvent
newBatch . StateEventsAtStart [ 0 ] = & dummyStateEvent
2021-10-27 18:48:14 +02:00
addedMembers := make ( map [ id . UserID ] * event . MemberEventContent )
2021-10-26 16:01:10 +02:00
addMember := func ( puppet * Puppet ) {
2021-10-26 20:30:42 +02:00
if _ , alreadyAdded := addedMembers [ puppet . MXID ] ; alreadyAdded {
2021-10-26 16:01:10 +02:00
return
}
mxid := puppet . MXID . String ( )
content := event . MemberEventContent {
Membership : event . MembershipJoin ,
Displayname : puppet . Displayname ,
AvatarURL : puppet . AvatarURL . CUString ( ) ,
}
inviteContent := content
inviteContent . Membership = event . MembershipInvite
historyBatch . StateEventsAtStart = append ( historyBatch . StateEventsAtStart , & event . Event {
Type : event . StateMember ,
Sender : portal . MainIntent ( ) . UserID ,
StateKey : & mxid ,
Timestamp : firstMsgTimestamp . UnixMilli ( ) ,
Content : event . Content { Parsed : & inviteContent } ,
} , & event . Event {
Type : event . StateMember ,
Sender : puppet . MXID ,
StateKey : & mxid ,
Timestamp : firstMsgTimestamp . UnixMilli ( ) ,
Content : event . Content { Parsed : & content } ,
} )
2021-10-26 20:30:42 +02:00
addedMembers [ puppet . MXID ] = & content
2021-10-26 16:01:10 +02:00
}
firstMessage := portal . bridge . DB . Message . GetFirstInChat ( portal . Key )
lastMessage := portal . bridge . DB . Message . GetLastInChat ( portal . Key )
var historyMaxTs , newMinTs time . Time
if portal . FirstEventID != "" || portal . NextBatchID != "" {
historyBatch . PrevEventID = portal . FirstEventID
historyBatch . BatchID = portal . NextBatchID
if firstMessage == nil && lastMessage == nil {
historyMaxTs = time . Now ( )
} else {
historyMaxTs = firstMessage . Timestamp
}
}
if lastMessage != nil {
newBatch . PrevEventID = lastMessage . MXID
newMinTs = lastMessage . Timestamp
}
portal . log . Infofln ( "Processing history sync with %d messages" , len ( messages ) )
// The messages are ordered newest to oldest, so iterate them in reverse order.
for i := len ( messages ) - 1 ; i >= 0 ; i -- {
wrappedMsg := messages [ i ]
webMsg := wrappedMsg . GetMessage ( )
msgType := portal . getMessageType ( webMsg . GetMessage ( ) )
if msgType == "unknown" || msgType == "ignore" {
if msgType == "unknown" {
portal . log . Debugfln ( "Skipping message %s with unknown type in backfill" , webMsg . GetKey ( ) . GetId ( ) )
}
continue
}
info := portal . parseWebMessageInfo ( webMsg )
if info == nil {
continue
}
var batch * mautrix . ReqBatchSend
var infos * [ ] * types . MessageInfo
var history bool
if ! historyMaxTs . IsZero ( ) && info . Timestamp . Before ( historyMaxTs ) {
batch , infos , history = & historyBatch , & historyBatchInfos , true
} else if ! newMinTs . IsZero ( ) && info . Timestamp . After ( newMinTs ) {
batch , infos = & newBatch , & newBatchInfos
} else {
continue
}
puppet := portal . getMessagePuppet ( source , info )
var intent * appservice . IntentAPI
if portal . Key . JID == puppet . JID {
intent = puppet . DefaultIntent ( )
} else {
intent = puppet . IntentFor ( portal )
2021-10-28 11:59:22 +02:00
if intent . IsCustomPuppet && ! portal . bridge . Config . Bridge . HistorySync . DoublePuppetBackfill {
2021-10-26 16:01:10 +02:00
intent = puppet . DefaultIntent ( )
addMember ( puppet )
}
}
converted := portal . convertMessage ( intent , source , info , webMsg . GetMessage ( ) )
if converted == nil {
portal . log . Debugfln ( "Skipping unsupported message %s in backfill" , info . ID )
continue
}
if history && ! portal . IsPrivateChat ( ) && ! portal . bridge . StateStore . IsInRoom ( portal . MXID , puppet . MXID ) {
addMember ( puppet )
}
err := portal . appendBatchEvents ( converted , info , & batch . Events , infos )
if err != nil {
portal . log . Errorfln ( "Error handling message %s during backfill: %v" , info . ID , err )
}
}
2021-10-27 18:48:14 +02:00
if len ( historyBatch . Events ) > 0 && len ( historyBatch . PrevEventID ) > 0 {
2021-10-26 16:01:10 +02:00
portal . log . Infofln ( "Sending %d historical messages..." , len ( historyBatch . Events ) )
historyResp , err := portal . MainIntent ( ) . BatchSend ( portal . MXID , & historyBatch )
if err != nil {
portal . log . Errorln ( "Error sending batch of historical messages:" , err )
} else {
portal . finishBatch ( historyResp . EventIDs , historyBatchInfos )
portal . NextBatchID = historyResp . NextBatchID
portal . Update ( )
2021-10-26 20:30:42 +02:00
// If batchID is non-empty, it means this is backfilling very old messages, and we don't need a post-backfill dummy.
if historyBatch . BatchID == "" {
portal . sendPostBackfillDummy ( time . UnixMilli ( historyBatch . Events [ len ( historyBatch . Events ) - 1 ] . Timestamp ) )
}
2021-10-26 16:01:10 +02:00
}
}
2021-10-26 20:30:42 +02:00
2021-10-27 18:48:14 +02:00
if len ( newBatch . Events ) > 0 && len ( newBatch . PrevEventID ) > 0 {
2021-10-26 20:30:42 +02:00
portal . log . Debugln ( "Sending a dummy event to avoid forward extremity errors with forward backfill" )
_ , err := portal . MainIntent ( ) . SendMessageEvent ( portal . MXID , ForwardBackfillDummyEvent , struct { } { } )
if err != nil {
portal . log . Warnln ( "Error sending dummy event for forward backfill:" , err )
}
2021-10-26 16:01:10 +02:00
portal . log . Infofln ( "Sending %d new messages..." , len ( newBatch . Events ) )
newResp , err := portal . MainIntent ( ) . BatchSend ( portal . MXID , & newBatch )
if err != nil {
portal . log . Errorln ( "Error sending batch of new messages:" , err )
} else {
portal . finishBatch ( newResp . EventIDs , newBatchInfos )
2021-10-26 20:30:42 +02:00
portal . sendPostBackfillDummy ( time . UnixMilli ( newBatch . Events [ len ( newBatch . Events ) - 1 ] . Timestamp ) )
2021-10-26 16:01:10 +02:00
}
}
}
2021-10-26 20:30:42 +02:00
func ( portal * Portal ) sendPostBackfillDummy ( lastTimestamp time . Time ) {
resp , err := portal . MainIntent ( ) . SendMessageEvent ( portal . MXID , BackfillEndDummyEvent , struct { } { } )
2020-06-08 19:51:24 +02:00
if err != nil {
2021-10-26 20:30:42 +02:00
portal . log . Errorln ( "Error sending post-backfill dummy event:" , err )
2020-06-08 19:51:24 +02:00
return
}
2021-10-26 20:30:42 +02:00
msg := portal . bridge . DB . Message . New ( )
msg . Chat = portal . Key
msg . MXID = resp . EventID
msg . JID = types . MessageID ( resp . EventID )
msg . Timestamp = lastTimestamp . Add ( 1 * time . Second )
msg . Sent = true
msg . Insert ( )
2020-06-08 19:51:24 +02:00
}
2020-06-01 14:09:58 +02:00
type BridgeInfoSection struct {
ID string ` json:"id" `
2020-07-03 12:47:02 +02:00
DisplayName string ` json:"displayname,omitempty" `
2020-06-01 14:09:58 +02:00
AvatarURL id . ContentURIString ` json:"avatar_url,omitempty" `
ExternalURL string ` json:"external_url,omitempty" `
}
type BridgeInfoContent struct {
BridgeBot id . UserID ` json:"bridgebot" `
Creator id . UserID ` json:"creator,omitempty" `
Protocol BridgeInfoSection ` json:"protocol" `
Network * BridgeInfoSection ` json:"network,omitempty" `
Channel BridgeInfoSection ` json:"channel" `
}
var (
StateBridgeInfo = event . Type { Type : "m.bridge" , Class : event . StateEventType }
StateHalfShotBridgeInfo = event . Type { Type : "uk.half-shot.bridge" , Class : event . StateEventType }
)
2020-06-15 19:38:41 +02:00
func ( portal * Portal ) getBridgeInfo ( ) ( string , BridgeInfoContent ) {
bridgeInfo := BridgeInfoContent {
BridgeBot : portal . bridge . Bot . UserID ,
Creator : portal . MainIntent ( ) . UserID ,
Protocol : BridgeInfoSection {
ID : "whatsapp" ,
DisplayName : "WhatsApp" ,
AvatarURL : id . ContentURIString ( portal . bridge . Config . AppService . Bot . Avatar ) ,
ExternalURL : "https://www.whatsapp.com/" ,
} ,
Channel : 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 ( )
_ , err := portal . MainIntent ( ) . SendStateEvent ( portal . MXID , StateBridgeInfo , stateKey , content )
if err != nil {
portal . log . Warnln ( "Failed to update m.bridge:" , err )
}
_ , err = portal . MainIntent ( ) . SendStateEvent ( portal . MXID , StateHalfShotBridgeInfo , stateKey , content )
if err != nil {
portal . log . Warnln ( "Failed to update uk.half-shot.bridge:" , err )
}
}
2021-10-26 20:30:42 +02:00
var PortalCreationDummyEvent = event . Type { Type : "fi.mau.dummy.portal_created" , Class : event . MessageEventType }
var BackfillDummyStateEvent = event . Type { Type : "fi.mau.dummy.blank_backfill_state" , Class : event . StateEventType }
var BackfillEndDummyEvent = event . Type { Type : "fi.mau.dummy.backfill_end" , Class : event . MessageEventType }
var ForwardBackfillDummyEvent = event . Type { Type : "fi.mau.dummy.pre_forward_backfill" , Class : event . MessageEventType }
2021-10-26 16:01:10 +02:00
2019-03-13 23:38:11 +01:00
func ( portal * Portal ) CreateMatrixRoom ( user * User ) 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 metadata * types . GroupInfo
//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-10-22 19:14:34 +02:00
puppet . SyncContact ( user , true )
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 {
2019-05-22 22:27:58 +02:00
var err error
2021-10-22 19:14:34 +02:00
metadata , err = user . Client . GetGroupInfo ( portal . Key . JID )
if err == nil {
2019-03-13 23:38:11 +01:00
portal . Name = metadata . Name
portal . Topic = metadata . Topic
}
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
} , {
2020-06-15 13:56:52 +02:00
Type : StateBridgeInfo ,
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
2020-06-15 13:56:52 +02:00
Type : StateHalfShotBridgeInfo ,
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 )
}
}
2019-05-22 22:27:58 +02:00
resp , err := intent . CreateRoom ( & mautrix . ReqCreateRoom {
Visibility : "private" ,
Name : portal . Name ,
Topic : portal . Topic ,
2019-11-10 20:22:11 +01:00
Invite : invite ,
2019-05-22 22:27:58 +02:00
Preset : "private_chat" ,
2020-05-12 21:25:55 +02:00
IsDirect : portal . IsPrivateChat ( ) ,
2019-05-22 22:27:58 +02:00
InitialState : initialState ,
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
2019-05-22 22:27:58 +02:00
if metadata != nil {
2021-03-19 19:55:08 +01:00
portal . SyncParticipants ( user , metadata )
2021-10-22 19:14:34 +02:00
if metadata . IsAnnounce {
portal . RestrictMessageSending ( metadata . IsAnnounce )
}
if metadata . IsLocked {
portal . RestrictMetadataChanges ( metadata . 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
}
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-10-22 19:14:34 +02:00
func ( portal * Portal ) SetReply ( content * event . MessageEventContent , replyToID types . MessageID ) {
if len ( replyToID ) == 0 {
2018-08-23 23:52:06 +02:00
return
}
2021-10-22 19:14:34 +02:00
message := portal . bridge . DB . Message . GetByJID ( portal . Key , replyToID )
2021-02-09 22:41:14 +01:00
if message != nil && ! message . IsFakeMXID ( ) {
2020-05-08 21:32:22 +02:00
evt , err := portal . MainIntent ( ) . GetEvent ( portal . MXID , message . MXID )
2018-08-24 18:46:14 +02:00
if err != nil {
portal . log . Warnln ( "Failed to get reply target:" , err )
return
}
2021-10-28 12:57:15 +02:00
_ = evt . Content . ParseRaw ( evt . Type )
2020-06-30 15:26:13 +02:00
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-05-08 21:32:22 +02:00
content . SetReply ( evt )
2018-08-23 23:52:06 +02:00
}
return
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 )
2019-05-16 00:59:36 +02:00
_ , err := intent . RedactEvent ( portal . MXID , msg . MXID )
if err != nil {
2021-10-27 20:09:36 +02:00
if errors . Is ( err , mautrix . MForbidden ) {
_ , err = portal . MainIntent ( ) . RedactEvent ( portal . MXID , msg . MXID )
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-22 19:14:34 +02:00
//func (portal *Portal) HandleFakeMessage(_ *User, message FakeMessage) bool {
// if portal.isRecentlyHandled(message.ID) {
// return false
// }
//
// content := event.MessageEventContent{
// MsgType: event.MsgNotice,
// Body: message.Text,
// }
// if message.Alert {
// content.MsgType = event.MsgText
// }
// _, err := portal.sendMainIntentMessage(content)
// if err != nil {
// portal.log.Errorfln("Failed to handle fake message %s: %v", message.ID, err)
// return true
// }
//
// portal.recentlyHandledLock.Lock()
// index := portal.recentlyHandledIndex
// portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % recentlyHandledLength
// portal.recentlyHandledLock.Unlock()
// portal.recentlyHandled[index] = message.ID
// return true
//}
2019-05-30 16:00:36 +02:00
2020-05-09 01:03:59 +02:00
func ( portal * Portal ) sendMainIntentMessage ( content interface { } ) ( * mautrix . RespSendEvent , error ) {
return portal . sendMessage ( portal . MainIntent ( ) , event . EventMessage , content , 0 )
}
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
}
func ( portal * Portal ) sendMessage ( intent * appservice . IntentAPI , eventType event . Type , content interface { } , timestamp int64 ) ( * mautrix . RespSendEvent , error ) {
wrappedContent := event . Content { Parsed : content }
if timestamp != 0 && intent . IsCustomPuppet {
wrappedContent . Raw = map [ string ] interface { } {
"net.maunium.whatsapp.puppet" : intent . IsCustomPuppet ,
}
}
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
Caption * event . MessageEventContent
}
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-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 ( ) )
portal . SetReply ( content , contextInfo . GetStanzaId ( ) )
}
2018-08-23 00:12:26 +02:00
}
2018-08-19 17:21:38 +02:00
2021-10-26 16:01:10 +02:00
return & ConvertedMessage { Intent : intent , Type : event . EventMessage , Content : content }
2021-02-09 22:41:14 +01:00
}
2021-10-22 19:14:34 +02:00
//func (portal *Portal) HandleStubMessage(source *User, message whatsapp.StubMessage, isBackfill bool) bool {
// if portal.bridge.Config.Bridge.ChatMetaSync && (!portal.IsBroadcastList() || isBackfill) {
// // Chat meta sync is enabled, so we use chat update commands and full-syncs instead of message history
// // However, broadcast lists don't have update commands, so we handle these if it's not a backfill
// return false
// }
// intent := portal.startHandling(source, message.Info, fmt.Sprintf("stub %s", message.Type.String()))
// if intent == nil {
// return false
// }
// var senderJID string
// if message.Info.FromMe {
// senderJID = source.JID
// } else {
// senderJID = message.Info.SenderJid
// }
// var eventID id.EventID
// // TODO find more real event IDs
// // TODO timestamp massaging
// switch message.Type {
// case waProto.WebMessageInfo_GROUP_CHANGE_SUBJECT:
// portal.UpdateName(message.FirstParam, "", intent, true)
// case waProto.WebMessageInfo_GROUP_CHANGE_ICON:
// portal.UpdateAvatar(source, nil, true)
// case waProto.WebMessageInfo_GROUP_CHANGE_DESCRIPTION:
// if isBackfill {
// // TODO fetch topic from server
// }
// //portal.UpdateTopic(message.FirstParam, "", intent, true)
// case waProto.WebMessageInfo_GROUP_CHANGE_ANNOUNCE:
// eventID = portal.RestrictMessageSending(message.FirstParam == "on")
// case waProto.WebMessageInfo_GROUP_CHANGE_RESTRICT:
// eventID = portal.RestrictMetadataChanges(message.FirstParam == "on")
// case waProto.WebMessageInfo_GROUP_PARTICIPANT_ADD, waProto.WebMessageInfo_GROUP_PARTICIPANT_INVITE, waProto.WebMessageInfo_BROADCAST_ADD:
// eventID = portal.HandleWhatsAppInvite(source, senderJID, intent, message.Params)
// case waProto.WebMessageInfo_GROUP_PARTICIPANT_REMOVE, waProto.WebMessageInfo_GROUP_PARTICIPANT_LEAVE, waProto.WebMessageInfo_BROADCAST_REMOVE:
// portal.HandleWhatsAppKick(source, senderJID, message.Params)
// case waProto.WebMessageInfo_GROUP_PARTICIPANT_PROMOTE:
// eventID = portal.ChangeAdminStatus(message.Params, true)
// case waProto.WebMessageInfo_GROUP_PARTICIPANT_DEMOTE:
// eventID = portal.ChangeAdminStatus(message.Params, false)
// default:
// return false
// }
// if len(eventID) == 0 {
// eventID = id.EventID(fmt.Sprintf("net.maunium.whatsapp.fake::%s", message.Info.Id))
// }
// portal.markHandled(source, message.Info.Source, eventID, true)
// return true
//}
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-10-22 19:14:34 +02:00
portal . SetReply ( content , msg . GetContextInfo ( ) . GetStanzaId ( ) )
2020-06-10 13:58:57 +02:00
2021-10-26 16:01:10 +02:00
return & ConvertedMessage { Intent : intent , Type : event . EventMessage , Content : content }
2020-06-10 13:58:57 +02:00
}
2020-06-10 13:06:36 +02: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-10-22 19:14:34 +02:00
portal . SetReply ( content , msg . GetContextInfo ( ) . GetStanzaId ( ) )
2020-06-10 13:06:36 +02:00
2021-10-26 16:01:10 +02:00
return & ConvertedMessage { Intent : intent , Type : event . EventMessage , Content : content }
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 {
if source != nil && source . JID . User == jid . User {
portal . log . Debugln ( "Ignoring self-kick by" , source . MXID )
continue
}
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 { } {
"net.maunium.whatsapp.puppet" : true ,
} ,
}
return intent . SendStateEvent ( portal . MXID , event . StateMember , intent . UserID . String ( ) , & content )
}
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 )
puppet . SyncContact ( source , true )
content := event . Content {
Parsed : event . MemberEventContent {
Membership : "invite" ,
Displayname : puppet . Displayname ,
AvatarURL : puppet . AvatarURL . CUString ( ) ,
} ,
Raw : map [ string ] interface { } {
"net.maunium.whatsapp.puppet" : true ,
} ,
}
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
GetCaption ( ) string
}
2021-10-26 16:01:10 +02:00
type MediaMessageWithCaption interface {
MediaMessage
GetCaption ( ) string
}
2021-10-22 19:14:34 +02:00
type MediaMessageWithFileName interface {
MediaMessage
GetFileName ( ) string
}
type MediaMessageWithDuration interface {
MediaMessage
GetSeconds ( ) uint32
}
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-10-22 19:14:34 +02:00
if 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-10-22 19:14:34 +02:00
exts , _ := mime . ExtensionsByType ( msg . GetMimetype ( ) )
2020-06-20 17:26:45 +02:00
if exts != nil && len ( exts ) > 0 {
2021-10-22 19:14:34 +02:00
content . Body += exts [ 0 ]
2020-06-20 17:26:45 +02:00
}
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 ( )
}
2021-10-22 19:14:34 +02:00
portal . SetReply ( content , msg . GetContextInfo ( ) . GetStanzaId ( ) )
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 ,
2018-08-27 22:15:05 +02:00
}
2018-08-19 17:21:38 +02:00
}
2020-05-08 21:32:22 +02:00
func ( portal * Portal ) downloadThumbnail ( content * event . MessageEventContent , id id . EventID ) [ ] byte {
if len ( content . GetInfo ( ) . ThumbnailURL ) == 0 {
2018-08-25 23:26:24 +02:00
return nil
}
2020-05-08 21:32:22 +02:00
mxc , err := content . GetInfo ( ) . ThumbnailURL . Parse ( )
2018-08-25 23:26:24 +02:00
if err != nil {
2020-05-08 21:32:22 +02:00
portal . log . Errorln ( "Malformed thumbnail URL in %s: %v" , id , err )
}
thumbnail , err := portal . MainIntent ( ) . DownloadBytes ( mxc )
if err != nil {
portal . log . Errorln ( "Failed to download thumbnail in %s: %v" , id , err )
2018-08-25 23:26:24 +02:00
return nil
}
thumbnailType := http . DetectContentType ( thumbnail )
var img image . Image
switch thumbnailType {
case "image/png" :
img , err = png . Decode ( bytes . NewReader ( thumbnail ) )
case "image/gif" :
img , err = gif . Decode ( bytes . NewReader ( thumbnail ) )
case "image/jpeg" :
return thumbnail
default :
return nil
}
var buf bytes . Buffer
err = jpeg . Encode ( & buf , img , & jpeg . Options {
Quality : jpeg . DefaultQuality ,
} )
if err != nil {
2020-05-08 21:32:22 +02:00
portal . log . Errorln ( "Failed to re-encode thumbnail in %s: %v" , id , err )
2018-08-25 23:26:24 +02:00
return nil
}
return buf . Bytes ( )
}
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 ) {
dir , err := ioutil . TempDir ( "" , "gif-convert-*" )
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 ( ) )
} ( )
mp4 , err := ioutil . ReadAll ( outputFile )
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
}
return & MediaUpload {
2021-10-22 19:14:34 +02:00
UploadResponse : uploadResp ,
Caption : caption ,
MentionedJIDs : mentionedJIDs ,
Thumbnail : portal . downloadThumbnail ( content , eventID ) ,
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 ) sendMatrixConnectionError ( sender * User , eventID id . EventID ) bool {
2019-08-30 20:32:29 +02:00
if ! sender . HasSession ( ) {
portal . log . Debugln ( "Ignoring event" , eventID , "from" , sender . MXID , "as user has no session" )
return true
2021-10-22 19:14:34 +02:00
} / * else if ! sender . IsConnected ( ) {
2019-08-30 20:37:12 +02:00
inRoom := ""
if portal . IsPrivateChat ( ) {
inRoom = " in your management room"
}
2019-08-30 21:04:57 +02:00
if sender . IsLoginInProgress ( ) {
2021-03-05 15:54:23 +01:00
portal . log . Debugln ( "Waiting for connection before handling event" , eventID , "from" , sender . MXID )
sender . Conn . WaitForLogin ( )
if sender . IsConnected ( ) {
return false
}
2019-08-30 21:04:57 +02:00
}
2021-03-05 15:54:23 +01:00
reconnect := fmt . Sprintf ( "Use `%s reconnect`%s to reconnect." , portal . bridge . Config . Bridge . CommandPrefix , inRoom )
portal . log . Debugln ( "Ignoring event" , eventID , "from" , sender . MXID , "as user is not connected" )
2020-05-09 01:08:23 +02:00
msg := format . RenderMarkdown ( "\u26a0 You are not connected to WhatsApp, so your message was not bridged. " + reconnect , true , false )
2020-05-08 21:32:22 +02:00
msg . MsgType = event . MsgNotice
2020-05-09 01:03:59 +02:00
_ , err := portal . sendMainIntentMessage ( msg )
2019-08-30 20:32:29 +02:00
if err != nil {
portal . log . Errorln ( "Failed to send bridging failure message:" , err )
}
return true
2021-10-22 19:14:34 +02:00
} * /
// FIXME implement
2019-08-30 20:32:29 +02:00
return false
}
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 )
2019-11-10 20:22:11 +01:00
if len ( member . Displayname ) == 0 {
2020-05-08 21:32:22 +02:00
member . Displayname = string ( sender . MXID )
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-10-28 12:57:15 +02: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-08-19 18:17:19 +02:00
func fallbackQuoteContent ( ) * waProto . Message {
blankString := ""
return & waProto . Message {
Conversation : & blankString ,
}
}
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 {
2020-05-08 21:32:22 +02:00
content . RemoveReplyFallback ( )
2021-10-22 19:14:34 +02:00
replyToMsg := portal . bridge . DB . Message . GetByMXID ( replyToID )
if replyToMsg != nil {
ctxInfo . StanzaId = & replyToMsg . JID
ctxInfo . Participant = proto . String ( replyToMsg . Sender . String ( ) )
2021-08-19 18:19:56 +02:00
// Using blank content here seems to work fine on all official WhatsApp apps.
// Getting the content from the phone would be possible, but it's complicated.
// https://github.com/mautrix/whatsapp/commit/b3312bc663772aa274cea90ffa773da2217bb5e0
2021-08-19 18:17:46 +02:00
ctxInfo . QuotedMessage = fallbackQuoteContent ( )
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-10-28 12:57:15 +02:00
if ! sender . HasSession ( ) {
2019-11-10 20:22:11 +01:00
if ! portal . HasRelaybot ( ) {
2021-10-28 12:57:15 +02:00
portal . log . Debugln ( "Ignoring message from" , sender . MXID , "in chat with no relaybot" )
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 ,
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"
}
2020-05-24 16:28:30 +02:00
resp , err := portal . sendMainIntentMessage ( event . MessageEventContent {
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-08-02 11:53:38 +02:00
if ! portal . HasRelaybot ( ) &&
2021-10-22 19:14:34 +02:00
( ( portal . IsPrivateChat ( ) && sender . JID . User != portal . Key . Receiver . User ) ||
2020-05-24 16:28:30 +02:00
portal . sendMatrixConnectionError ( sender , evt . ID ) ) {
2018-08-19 17:21:38 +02:00
return
}
2020-05-24 16:28:30 +02:00
portal . log . Debugfln ( "Received event %s" , evt . ID )
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 )
err := sender . Client . SendMessage ( portal . Key . JID , info . ID , msg )
2018-08-19 17:21:38 +02:00
if err != nil {
2021-10-22 19:14:34 +02:00
portal . log . Errorln ( "Error sending message: %v" , err )
portal . sendErrorMessage ( err . Error ( ) , true )
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 )
2020-05-25 10:17:47 +02:00
portal . sendDeliveryReceipt ( evt . ID )
2021-02-17 00:22:06 +01:00
dbMsg . MarkSent ( )
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-10-22 19:14:34 +02:00
if portal . IsPrivateChat ( ) && sender . JID . User != portal . Key . Receiver . User {
2019-05-16 00:59:36 +02:00
return
}
msg := portal . bridge . DB . Message . GetByMXID ( evt . Redacts )
2021-10-27 20:34:22 +02:00
if msg == nil {
portal . log . Debugfln ( "Ignoring redaction %s of unknown event by %s" , msg , sender . MXID )
return
} else if msg . Sender . User != sender . JID . User {
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 , sender . MXID , msg . Sender , sender . JID )
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 )
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 )
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 )
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
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 {
2020-06-25 22:58:35 +02:00
// TODO should we somehow deduplicate this call if this leave was sent by the bridge?
2021-10-22 19:14:34 +02:00
// FIXME reimplement
//resp, err := sender.Client.LeaveGroup(portal.Key.JID)
//if err != nil {
// portal.log.Errorfln("Failed to leave group as %s: %v", sender.MXID, err)
// return
//}
//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-10-22 19:14:34 +02:00
// FIXME reimplement
//resp, err := sender.Conn.RemoveMember(portal.Key.JID, []string{puppet.JID})
//if err != nil {
// portal.log.Errorfln("Failed to kick %s from group as %s: %v", puppet.JID, sender.MXID, err)
// return
//}
//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-10-22 19:14:34 +02:00
// FIXME reimplement
//resp, err := sender.Conn.AddMember(portal.Key.JID, []string{puppet.JID})
//if err != nil {
// portal.log.Errorfln("Failed to add %s to group as %s: %v", puppet.JID, sender.MXID, err)
// return
//}
//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-10-22 19:14:34 +02:00
// FIXME reimplement
//portal.Name = content.Name
//resp, err = sender.Conn.UpdateGroupSubject(content.Name, portal.Key.JID)
2021-02-09 22:41:14 +01:00
case * event . TopicEventContent :
if content . Topic == portal . Topic {
return
}
2021-10-22 19:14:34 +02:00
// FIXME reimplement
//portal.Topic = content.Topic
//resp, err = sender.Conn.UpdateGroupDescription(sender.JID, portal.Key.JID, content.Topic)
2021-02-09 22:41:14 +01:00
case * event . RoomAvatarEventContent :
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
}
}