2018-08-16 14:59:18 +02:00
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
2020-05-08 21:32:22 +02:00
// Copyright (C) 2020 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"
2018-09-01 22:38:03 +02:00
"encoding/gob"
2018-08-24 18:46:14 +02:00
"encoding/hex"
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
"math/rand"
"mime"
"net/http"
2020-06-23 15:36:05 +02:00
"os"
"os/exec"
"path/filepath"
2019-05-31 21:30:57 +02:00
"reflect"
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"
2021-05-12 12:39:24 +02:00
"sync/atomic"
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"
2018-08-24 18:46:14 +02:00
"github.com/Rhymen/go-whatsapp"
2018-08-25 23:26:24 +02:00
waProto "github.com/Rhymen/go-whatsapp/binary/proto"
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"
2019-11-10 20:22:11 +01:00
"maunium.net/go/mautrix/format"
2020-05-08 21:32:22 +02:00
"maunium.net/go/mautrix/id"
2020-06-08 19:51:24 +02:00
"maunium.net/go/mautrix/pushrules"
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-02-17 00:21:30 +01:00
func ( bridge * Bridge ) GetAllPortalsByJID ( jid whatsapp . JID ) [ ] * Portal {
2019-06-01 19:03:29 +02:00
return bridge . dbPortalsToPortals ( bridge . DB . Portal . GetAllByJID ( jid ) )
}
func ( bridge * Bridge ) dbPortalsToPortals ( dbPortals [ ] * database . Portal ) [ ] * Portal {
2018-08-28 23:40:54 +02:00
bridge . portalsLock . Lock ( )
defer bridge . portalsLock . Unlock ( )
2018-08-16 14:59:18 +02:00
output := make ( [ ] * Portal , len ( dbPortals ) )
for index , dbPortal := range dbPortals {
2019-06-13 20:30:38 +02:00
if dbPortal == nil {
continue
}
2018-08-28 23:40:54 +02:00
portal , ok := bridge . portalsByJID [ dbPortal . Key ]
2018-08-16 14:59:18 +02:00
if ! ok {
2019-05-28 20:31:25 +02:00
portal = bridge . loadDBPortal ( dbPortal , nil )
2018-08-16 14:59:18 +02:00
}
output [ index ] = portal
}
return output
}
2019-05-28 20:31:25 +02:00
func ( bridge * Bridge ) loadDBPortal ( dbPortal * database . Portal , key * database . PortalKey ) * Portal {
if dbPortal == nil {
if key == nil {
return nil
}
dbPortal = bridge . DB . Portal . New ( )
dbPortal . Key = * key
dbPortal . Insert ( )
}
portal := bridge . NewPortal ( dbPortal )
bridge . portalsByJID [ portal . Key ] = portal
if len ( portal . MXID ) > 0 {
bridge . portalsByMXID [ portal . MXID ] = portal
}
return portal
}
func ( portal * Portal ) GetUsers ( ) [ ] * User {
return nil
}
2020-07-05 17:57:03 +02:00
func ( bridge * Bridge ) NewManualPortal ( key database . PortalKey ) * Portal {
portal := & Portal {
Portal : bridge . DB . Portal . New ( ) ,
bridge : bridge ,
log : bridge . Log . Sub ( fmt . Sprintf ( "Portal/%s" , key ) ) ,
2021-02-17 00:21:30 +01:00
recentlyHandled : [ recentlyHandledLength ] whatsapp . MessageID { } ,
2020-07-05 17:57:03 +02:00
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
2021-02-17 00:21:30 +01:00
recentlyHandled : [ recentlyHandledLength ] whatsapp . MessageID { } ,
2019-05-21 22:44:14 +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 {
2019-05-22 22:05:58 +02:00
chat string
2019-05-22 15:46:18 +02:00
source * User
data interface { }
timestamp uint64
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
2018-08-30 23:13:08 +02:00
2021-02-17 00:21:30 +01:00
recentlyHandled [ recentlyHandledLength ] whatsapp . MessageID
2018-08-30 23:13:08 +02:00
recentlyHandledLock sync . Mutex
recentlyHandledIndex uint8
2018-09-01 22:38:03 +02:00
2020-11-18 12:29:47 +01:00
backfillLock sync . Mutex
2019-05-30 16:22:03 +02:00
backfilling bool
2019-05-21 22:44:14 +02:00
lastMessageTs uint64
2019-05-30 16:48:22 +02:00
privateChatBackfillInvitePuppet func ( )
2019-05-21 22:44:14 +02:00
messages chan PortalMessage
2019-11-10 20:22:11 +01:00
isPrivate * bool
2021-02-21 13:18:15 +01:00
isBroadcast * bool
2019-11-10 20:22:11 +01:00
hasRelaybot * bool
2018-08-30 23:13:08 +02:00
}
2019-05-22 15:46:18 +02:00
const MaxMessageAgeToCreatePortal = 5 * 60 // 5 minutes
2021-04-29 10:57:05 +02:00
func ( portal * Portal ) syncDoublePuppetDetailsAfterCreate ( source * User ) {
doublePuppet := portal . bridge . GetPuppetByCustomMXID ( source . MXID )
if doublePuppet == nil {
return
}
2021-06-19 01:22:01 +02:00
source . Conn . Store . ChatsLock . RLock ( )
2021-04-29 10:57:05 +02:00
chat , ok := source . Conn . Store . Chats [ portal . Key . JID ]
2021-06-19 01:22:01 +02:00
source . Conn . Store . ChatsLock . RUnlock ( )
2021-04-29 10:57:05 +02:00
if ! ok {
portal . log . Debugln ( "Not syncing chat mute/tags with %s: chat info not found" , source . MXID )
return
}
source . syncChatDoublePuppetDetails ( doublePuppet , Chat {
2021-06-01 12:32:14 +02:00
Chat : chat ,
Portal : portal ,
2021-04-29 10:57:05 +02:00
} , true )
}
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 {
if msg . timestamp + MaxMessageAgeToCreatePortal < uint64 ( time . Now ( ) . Unix ( ) ) {
2021-03-19 20:14:01 +01:00
portal . log . Debugln ( "Not creating portal room for incoming message: message is too old" )
continue
} else if ! portal . shouldCreateRoom ( msg ) {
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
}
2021-04-29 10:57:05 +02:00
portal . syncDoublePuppetDetailsAfterCreate ( msg . source )
2020-11-16 13:28:08 +01:00
}
2020-11-18 12:29:47 +01:00
portal . backfillLock . Lock ( )
2021-02-09 22:41:14 +01:00
portal . handleMessage ( msg , false )
2020-11-18 12:29:47 +01:00
portal . backfillLock . Unlock ( )
2020-11-16 13:28:08 +01:00
}
}
2021-03-19 20:14:01 +01:00
func ( portal * Portal ) shouldCreateRoom ( msg PortalMessage ) bool {
stubMsg , ok := msg . data . ( whatsapp . StubMessage )
if ok {
// This could be more specific: if someone else was added, we might not care,
// but if the local user was added, we definitely care.
return stubMsg . Type == waProto . WebMessageInfo_GROUP_PARTICIPANT_ADD || stubMsg . Type == waProto . WebMessageInfo_GROUP_PARTICIPANT_INVITE
}
return true
}
2021-02-09 22:41:14 +01:00
func ( portal * Portal ) handleMessage ( msg PortalMessage , isBackfill bool ) {
2019-05-30 16:00:36 +02:00
if len ( portal . MXID ) == 0 {
2019-05-31 21:30:57 +02:00
portal . log . Warnln ( "handleMessage called even though portal.MXID is empty" )
2019-05-30 16:00:36 +02:00
return
}
2021-06-25 14:33:37 +02:00
var triedToHandle bool
var trackMessageCallback func ( )
dataType := reflect . TypeOf ( msg . data )
if ! isBackfill {
trackMessageCallback = portal . bridge . Metrics . TrackWhatsAppMessage ( msg . timestamp , dataType . Name ( ) )
}
2019-05-21 22:44:14 +02:00
switch data := msg . data . ( type ) {
case whatsapp . TextMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleTextMessage ( msg . source , data )
2019-05-21 22:44:14 +02:00
case whatsapp . ImageMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleMediaMessage ( msg . source , mediaMessage {
2020-06-20 21:24:27 +02:00
base : base { data . Download , data . Info , data . ContextInfo , data . Type } ,
thumbnail : data . Thumbnail ,
caption : data . Caption ,
} )
2019-10-04 20:01:53 +02:00
case whatsapp . StickerMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleMediaMessage ( msg . source , mediaMessage {
2020-06-20 21:24:27 +02:00
base : base { data . Download , data . Info , data . ContextInfo , data . Type } ,
sendAsSticker : true ,
} )
2019-05-21 22:44:14 +02:00
case whatsapp . VideoMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleMediaMessage ( msg . source , mediaMessage {
2020-06-20 21:24:27 +02:00
base : base { data . Download , data . Info , data . ContextInfo , data . Type } ,
thumbnail : data . Thumbnail ,
caption : data . Caption ,
2021-06-22 19:03:22 +02:00
length : data . Length * 1000 ,
2020-06-20 21:24:27 +02:00
} )
2019-05-21 22:44:14 +02:00
case whatsapp . AudioMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleMediaMessage ( msg . source , mediaMessage {
2020-06-20 21:24:27 +02:00
base : base { data . Download , data . Info , data . ContextInfo , data . Type } ,
2021-06-22 19:03:22 +02:00
length : data . Length * 1000 ,
2020-06-20 21:24:27 +02:00
} )
2019-05-21 22:44:14 +02:00
case whatsapp . DocumentMessage :
2020-11-02 16:18:18 +01:00
fileName := data . FileName
if len ( fileName ) == 0 {
fileName = data . Title
}
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleMediaMessage ( msg . source , mediaMessage {
2020-06-20 21:24:27 +02:00
base : base { data . Download , data . Info , data . ContextInfo , data . Type } ,
thumbnail : data . Thumbnail ,
2020-11-02 16:18:18 +01:00
fileName : fileName ,
2020-06-20 21:24:27 +02:00
} )
2020-06-10 13:06:36 +02:00
case whatsapp . ContactMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleContactMessage ( msg . source , data )
2020-06-10 13:58:57 +02:00
case whatsapp . LocationMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleLocationMessage ( msg . source , data )
2021-02-09 22:41:14 +01:00
case whatsapp . StubMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleStubMessage ( msg . source , data , isBackfill )
2021-02-17 00:21:30 +01:00
case whatsapp . MessageRevocation :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleMessageRevoke ( msg . source , data )
2019-05-30 16:00:36 +02:00
case FakeMessage :
2021-06-25 14:33:37 +02:00
triedToHandle = portal . HandleFakeMessage ( msg . source , data )
2019-05-31 21:30:57 +02:00
default :
2021-06-25 14:33:37 +02:00
portal . log . Warnln ( "Unknown message type:" , dataType )
}
if triedToHandle && trackMessageCallback != nil {
trackMessageCallback ( )
2019-05-21 22:44:14 +02:00
}
}
2021-02-17 00:21:30 +01:00
func ( portal * Portal ) isRecentlyHandled ( id whatsapp . MessageID ) bool {
2018-08-30 23:13:08 +02:00
start := portal . recentlyHandledIndex
2018-09-01 23:01:22 +02:00
for i := start ; i != start ; i = ( i - 1 ) % recentlyHandledLength {
2018-08-30 23:13:08 +02:00
if portal . recentlyHandled [ i ] == id {
return true
}
}
return false
}
2021-02-17 00:21:30 +01:00
func ( portal * Portal ) isDuplicate ( id whatsapp . MessageID ) bool {
2018-08-30 23:13:08 +02:00
msg := portal . bridge . DB . Message . GetByJID ( portal . Key , id )
if msg != nil {
return true
}
return false
}
2018-09-01 22:38:03 +02:00
func init ( ) {
gob . Register ( & waProto . Message { } )
}
2021-02-17 00:22:06 +01:00
func ( portal * Portal ) markHandled ( source * User , message * waProto . WebMessageInfo , mxid id . EventID , isSent bool ) * database . Message {
2018-08-30 23:13:08 +02:00
msg := portal . bridge . DB . Message . New ( )
msg . Chat = portal . Key
2018-09-01 22:38:03 +02:00
msg . JID = message . GetKey ( ) . GetId ( )
2018-08-30 23:13:08 +02:00
msg . MXID = mxid
2021-04-19 21:14:32 +02:00
msg . Timestamp = int64 ( message . GetMessageTimestamp ( ) )
2018-09-01 22:38:03 +02:00
if message . GetKey ( ) . GetFromMe ( ) {
msg . Sender = source . JID
} else if portal . IsPrivateChat ( ) {
msg . Sender = portal . Key . JID
} else {
msg . Sender = message . GetKey ( ) . GetParticipant ( )
if len ( msg . Sender ) == 0 {
msg . Sender = message . GetParticipant ( )
}
}
2021-02-17 00:22:06 +01:00
msg . Sent = isSent
2018-08-30 23:13:08 +02:00
msg . Insert ( )
portal . recentlyHandledLock . Lock ( )
index := portal . recentlyHandledIndex
2018-09-01 23:01:22 +02:00
portal . recentlyHandledIndex = ( portal . recentlyHandledIndex + 1 ) % recentlyHandledLength
2018-08-30 23:13:08 +02:00
portal . recentlyHandledLock . Unlock ( )
2018-09-01 22:38:03 +02:00
portal . recentlyHandled [ index ] = msg . JID
2021-02-17 00:22:06 +01:00
return msg
2018-08-30 23:13:08 +02:00
}
2020-06-10 13:58:57 +02:00
func ( portal * Portal ) getMessageIntent ( user * User , info whatsapp . MessageInfo ) * appservice . IntentAPI {
if info . FromMe {
return portal . bridge . GetPuppetByJID ( user . JID ) . IntentFor ( portal )
} else if portal . IsPrivateChat ( ) {
return portal . MainIntent ( )
} else if len ( info . SenderJid ) == 0 {
if len ( info . Source . GetParticipant ( ) ) != 0 {
info . SenderJid = info . Source . GetParticipant ( )
} else {
return nil
}
}
2021-06-15 11:34:55 +02:00
puppet := portal . bridge . GetPuppetByJID ( info . SenderJid )
puppet . SyncContactIfNecessary ( user )
return puppet . IntentFor ( portal )
2020-06-10 13:58:57 +02:00
}
2021-06-10 15:29:20 +02:00
func ( portal * Portal ) startHandling ( source * User , info whatsapp . MessageInfo , msgType string ) * appservice . IntentAPI {
2020-05-28 19:35:43 +02:00
// TODO these should all be trace logs
2021-05-12 12:39:24 +02:00
if portal . lastMessageTs == 0 {
portal . log . Debugln ( "Fetching last message from database to get its timestamp" )
lastMessage := portal . bridge . DB . Message . GetLastInChat ( portal . Key )
if lastMessage != nil {
atomic . CompareAndSwapUint64 ( & portal . lastMessageTs , 0 , uint64 ( lastMessage . Timestamp ) )
}
}
2021-06-21 12:52:59 +02:00
// If there are messages slightly older than the last message, it's possible the order is just wrong,
// so don't short-circuit and check the database for duplicates.
const timestampIgnoreFuzziness = 5 * 60
if portal . lastMessageTs > info . Timestamp + timestampIgnoreFuzziness {
portal . log . Debugfln ( "Not handling %s (%s): message is >5 minutes older (%d) than last bridge message (%d)" , info . Id , msgType , info . Timestamp , portal . lastMessageTs )
2020-05-28 19:35:43 +02:00
} else if portal . isRecentlyHandled ( info . Id ) {
2021-06-10 15:29:20 +02:00
portal . log . Debugfln ( "Not handling %s (%s): message was recently handled" , info . Id , msgType )
2020-05-28 19:35:43 +02:00
} else if portal . isDuplicate ( info . Id ) {
2021-06-10 15:29:20 +02:00
portal . log . Debugfln ( "Not handling %s (%s): message is duplicate" , info . Id , msgType )
2020-05-28 19:35:43 +02:00
} else {
portal . lastMessageTs = info . Timestamp
2020-11-17 01:16:32 +01:00
intent := portal . getMessageIntent ( source , info )
if intent != nil {
2021-06-10 15:29:20 +02:00
portal . log . Debugfln ( "Starting handling of %s (%s, ts: %d)" , info . Id , msgType , info . Timestamp )
2020-11-17 01:16:32 +01:00
} else {
2021-06-10 15:29:20 +02:00
portal . log . Debugfln ( "Not handling %s (%s): sender is not known" , info . Id , msgType )
2020-11-17 01:16:32 +01:00
}
return intent
2018-08-30 23:13:08 +02:00
}
2020-06-10 13:58:57 +02:00
return nil
2018-08-30 23:13:08 +02:00
}
2020-05-08 21:32:22 +02:00
func ( portal * Portal ) finishHandling ( source * User , message * waProto . WebMessageInfo , mxid id . EventID ) {
2021-02-17 00:22:06 +01:00
portal . markHandled ( source , message , mxid , true )
2020-06-05 16:54:09 +02:00
portal . sendDeliveryReceipt ( mxid )
2019-05-28 13:12:35 +02:00
portal . log . Debugln ( "Handled message" , message . GetKey ( ) . GetId ( ) , "->" , mxid )
2018-08-16 14:59:18 +02:00
}
2018-08-18 21:57:08 +02:00
2021-02-21 13:45:33 +01:00
func ( portal * Portal ) kickExtraUsers ( participantMap map [ whatsapp . JID ] bool ) {
members , err := portal . MainIntent ( ) . JoinedMembers ( portal . MXID )
if err != nil {
portal . log . Warnln ( "Failed to get member list:" , err )
} else {
for member := range members . Joined {
jid , ok := portal . bridge . ParsePuppetMXID ( member )
if ok {
_ , shouldBePresent := participantMap [ jid ]
if ! shouldBePresent {
2021-02-21 14:00:04 +01:00
_ , err = portal . MainIntent ( ) . KickUser ( portal . MXID , & mautrix . ReqKickUser {
2021-02-21 13:45:33 +01:00
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-03-19 19:55:08 +01:00
func ( portal * Portal ) SyncBroadcastRecipients ( source * User , metadata * whatsapp . BroadcastListInfo ) {
2021-02-21 13:45:33 +01:00
participantMap := make ( map [ whatsapp . JID ] bool )
for _ , recipient := range metadata . Recipients {
participantMap [ recipient . JID ] = true
puppet := portal . bridge . GetPuppetByJID ( recipient . JID )
2021-03-19 19:55:08 +01:00
puppet . SyncContactIfNecessary ( source )
2021-02-21 13:45:33 +01:00
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 )
}
2021-03-19 19:55:08 +01:00
func ( portal * Portal ) SyncParticipants ( source * User , metadata * whatsapp . 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-02-21 13:45:33 +01:00
participantMap := make ( map [ whatsapp . 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
2018-08-28 23:40:54 +02:00
user := portal . bridge . GetUserByJID ( participant . JID )
2019-11-10 20:22:11 +01:00
portal . userMXIDAction ( user , portal . ensureMXIDInvited )
2018-08-28 23:40:54 +02:00
2019-05-24 01:33:26 +02:00
puppet := portal . bridge . GetPuppetByJID ( participant . JID )
2021-03-19 19:55:08 +01:00
puppet . SyncContactIfNecessary ( source )
err = puppet . IntentFor ( portal ) . EnsureJoined ( portal . MXID )
2019-05-24 01:33:26 +02:00
if err != nil {
portal . log . Warnfln ( "Failed to make puppet of %s join %s: %v" , participant . JID , portal . MXID , err )
}
2018-08-26 15:11:48 +02:00
expectedLevel := 0
if participant . IsSuperAdmin {
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-02-17 00:21:30 +01:00
func ( portal * Portal ) UpdateAvatar ( user * User , avatar * whatsapp . ProfilePicInfo , updateInfo bool ) bool {
2021-02-09 22:52:12 +01:00
if avatar == nil || ( avatar . Status == 0 && avatar . Tag != "remove" && len ( avatar . URL ) == 0 ) {
2018-08-26 00:55:21 +02:00
var err error
2018-08-28 23:40:54 +02:00
avatar , err = user . Conn . GetProfilePicThumb ( portal . Key . JID )
2018-08-26 00:55:21 +02:00
if err != nil {
portal . log . Errorln ( err )
return false
}
2018-08-23 00:12:26 +02:00
}
2018-08-26 00:55:21 +02:00
2021-02-09 22:41:14 +01:00
if avatar . Status == 404 {
avatar . Tag = "remove"
avatar . Status = 0
2018-12-05 10:30:07 +01:00
}
2021-02-09 22:41:14 +01:00
if avatar . Status != 0 || portal . Avatar == avatar . Tag {
2018-08-23 00:12:26 +02:00
return false
}
2021-02-09 22:41:14 +01:00
if avatar . Tag == "remove" {
portal . AvatarURL = id . ContentURI { }
} else {
data , err := avatar . DownloadBytes ( )
if err != nil {
portal . log . Warnln ( "Failed to download avatar:" , err )
return false
}
2018-08-23 00:12:26 +02:00
2021-02-09 22:41:14 +01:00
mimeType := http . DetectContentType ( data )
resp , err := portal . MainIntent ( ) . UploadBytes ( data , mimeType )
if err != nil {
portal . log . Warnln ( "Failed to upload avatar:" , err )
return false
}
portal . AvatarURL = resp . ContentURI
2018-08-23 00:12:26 +02:00
}
2019-05-22 22:27:58 +02:00
if len ( portal . MXID ) > 0 {
2021-02-09 22:41:14 +01:00
_ , 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
}
portal . Avatar = avatar . Tag
2020-06-15 13:56:52 +02:00
if updateInfo {
portal . UpdateBridgeInfo ( )
}
2018-08-23 00:12:26 +02:00
return true
}
2021-02-17 00:21:30 +01:00
func ( portal * Portal ) UpdateName ( name string , setBy whatsapp . JID , intent * appservice . IntentAPI , 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
if intent == nil {
intent = portal . MainIntent ( )
if len ( setBy ) > 0 {
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 )
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-02-17 00:21:30 +01:00
func ( portal * Portal ) UpdateTopic ( topic string , setBy whatsapp . JID , intent * appservice . IntentAPI , 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
if intent == nil {
intent = portal . MainIntent ( )
if len ( setBy ) > 0 {
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 )
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-02-21 13:18:15 +01:00
update = portal . UpdateName ( StatusBroadcastName , "" , nil , false ) || update
update = portal . UpdateTopic ( StatusBroadcastTopic , "" , nil , false ) || update
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-02-21 14:00:04 +01:00
broadcastMetadata , err := user . Conn . GetBroadcastMetadata ( portal . Key . JID )
if err == nil && broadcastMetadata . Status == 200 {
2021-03-19 19:55:08 +01:00
portal . SyncBroadcastRecipients ( user , broadcastMetadata )
2021-02-21 14:00:04 +01:00
update = portal . UpdateName ( broadcastMetadata . Name , "" , nil , false ) || update
} else {
2021-06-19 01:22:01 +02:00
user . Conn . Store . ContactsLock . RLock ( )
2021-02-21 14:00:04 +01:00
contact , _ := user . Conn . Store . Contacts [ portal . Key . JID ]
2021-06-19 01:22:01 +02:00
user . Conn . Store . ContactsLock . RUnlock ( )
2021-02-21 14:00:04 +01:00
update = portal . UpdateName ( contact . Name , "" , nil , false ) || update
}
2021-02-21 13:18:15 +01:00
update = portal . UpdateTopic ( BroadcastTopic , "" , nil , false ) || update
2019-03-13 23:38:11 +01:00
return update
}
2018-08-28 23:40:54 +02:00
metadata , err := user . Conn . GetGroupMetaData ( portal . Key . JID )
2018-08-23 00:12:26 +02:00
if err != nil {
portal . log . Errorln ( err )
return false
}
2018-12-05 09:20:39 +01:00
if metadata . Status != 0 {
// 401: access denied
// 404: group does (no longer) exist
// 500: ??? happens with status@broadcast
// TODO: update the room, e.g. change priority level
// to send messages to moderator
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-02-09 22:41:14 +01:00
update = portal . UpdateName ( metadata . Name , metadata . NameSetBy , nil , false ) || update
update = portal . UpdateTopic ( metadata . Topic , metadata . TopicSetBy , nil , false ) || update
2020-10-12 12:59:14 +02:00
portal . RestrictMessageSending ( metadata . Announce )
2018-08-23 00:12:26 +02:00
return update
}
2020-05-08 21:32:22 +02:00
func ( portal * Portal ) userMXIDAction ( user * User , fn func ( mxid id . UserID ) ) {
2019-11-10 20:22:11 +01:00
if user == nil {
return
}
if user == portal . bridge . Relaybot {
for _ , mxid := range portal . bridge . Config . Bridge . Relaybot . InviteUsers {
fn ( mxid )
}
} else {
fn ( user . MXID )
}
}
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
}
func ( portal * Portal ) ensureUserInvited ( user * User ) {
2021-08-18 15:24:13 +02:00
if user . IsRelaybot {
portal . userMXIDAction ( user , portal . ensureMXIDInvited )
return
}
2019-11-10 20:22:11 +01:00
2021-08-18 15:24:13 +02:00
inviteContent := event . Content {
Parsed : & event . MemberEventContent {
Membership : event . MembershipInvite ,
IsDirect : portal . IsPrivateChat ( ) ,
} ,
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 )
} else if err != nil {
portal . log . Warnfln ( "Failed to invite %s: %v" , user . MXID , err )
}
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 )
}
2019-05-30 16:22:03 +02:00
}
}
2021-06-01 14:28:15 +02:00
func ( portal * Portal ) Sync ( user * User , contact whatsapp . Contact ) 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
2019-11-10 20:22:11 +01:00
if user . IsRelaybot {
yes := true
portal . hasRelaybot = & yes
}
2018-08-19 17:21:38 +02:00
if len ( portal . MXID ) == 0 {
2019-05-31 18:33:18 +02:00
if ! portal . IsPrivateChat ( ) {
portal . Name = contact . Name
}
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 == "" {
2020-06-15 13:56:52 +02:00
update = portal . UpdateAvatar ( user , nil , 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-02-09 22:41:14 +01:00
func ( portal * Portal ) ChangeAdminStatus ( jids [ ] string , setAdmin 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 setAdmin {
newLevel = 50
}
changed := false
for _ , jid := range jids {
2018-08-28 23:40:54 +02:00
puppet := portal . bridge . GetPuppetByJID ( jid )
2018-08-26 15:11:48 +02:00
changed = levels . EnsureUserLevel ( puppet . MXID , newLevel ) || changed
2018-08-26 15:19:50 +02:00
2018-08-28 23:40:54 +02:00
user := portal . bridge . GetUserByJID ( jid )
if user != nil {
changed = levels . EnsureUserLevel ( user . MXID , newLevel ) || changed
2018-08-26 15:19:50 +02:00
}
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-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-04-19 21:14:32 +02:00
func ( portal * Portal ) BackfillHistory ( user * User , lastMessageTime int64 ) error {
2019-05-22 15:46:18 +02:00
if ! portal . bridge . Config . Bridge . RecoverHistory {
return nil
}
2019-05-30 16:25:56 +02:00
endBackfill := portal . beginBackfill ( )
defer endBackfill ( )
2019-05-22 15:46:18 +02:00
lastMessage := portal . bridge . DB . Message . GetLastInChat ( portal . Key )
if lastMessage == nil {
return nil
}
2019-05-28 20:30:39 +02:00
if lastMessage . Timestamp >= lastMessageTime {
2019-05-22 22:05:58 +02:00
portal . log . Debugln ( "Not backfilling: no new messages" )
return nil
}
2019-05-22 15:46:18 +02:00
lastMessageID := lastMessage . JID
2019-05-28 20:30:39 +02:00
lastMessageFromMe := lastMessage . Sender == user . JID
2019-05-22 15:46:18 +02:00
portal . log . Infoln ( "Backfilling history since" , lastMessageID , "for" , user . MXID )
for len ( lastMessageID ) > 0 {
2021-01-23 17:09:58 +01:00
portal . log . Debugln ( "Fetching 50 messages of history after" , lastMessageID )
2019-05-28 20:30:39 +02:00
resp , err := user . Conn . LoadMessagesAfter ( portal . Key . JID , lastMessageID , lastMessageFromMe , 50 )
2021-02-17 00:22:06 +01:00
if err == whatsapp . ErrServerRespondedWith404 {
portal . log . Warnln ( "Got 404 response trying to fetch messages to backfill. Fetching latest messages as fallback." )
resp , err = user . Conn . LoadMessagesBefore ( portal . Key . JID , "" , true , 50 )
}
2019-05-22 15:46:18 +02:00
if err != nil {
return err
}
2019-05-22 21:05:28 +02:00
messages , ok := resp . Content . ( [ ] interface { } )
2019-05-28 20:30:39 +02:00
if ! ok || len ( messages ) == 0 {
2021-01-23 17:09:58 +01:00
portal . log . Debugfln ( "Didn't get more messages to backfill (resp.Content is %T)" , resp . Content )
2019-05-22 21:05:28 +02:00
break
}
portal . handleHistory ( user , messages )
lastMessageProto , ok := messages [ len ( messages ) - 1 ] . ( * waProto . WebMessageInfo )
if ok {
lastMessageID = lastMessageProto . GetKey ( ) . GetId ( )
2019-05-28 20:30:39 +02:00
lastMessageFromMe = lastMessageProto . GetKey ( ) . GetFromMe ( )
2019-05-22 15:46:18 +02:00
}
}
portal . log . Infoln ( "Backfilling finished" )
return nil
}
2019-05-30 16:25:56 +02:00
func ( portal * Portal ) beginBackfill ( ) func ( ) {
2020-11-18 12:29:47 +01:00
portal . backfillLock . Lock ( )
2019-05-30 16:22:03 +02:00
portal . backfilling = true
2019-05-30 16:48:22 +02:00
var privateChatPuppetInvited bool
2019-05-30 16:22:03 +02:00
var privateChatPuppet * Puppet
2020-07-05 18:24:48 +02:00
if portal . IsPrivateChat ( ) && portal . bridge . Config . Bridge . InviteOwnPuppetForBackfilling && portal . Key . JID != portal . Key . Receiver {
2019-05-30 16:22:03 +02:00
privateChatPuppet = portal . bridge . GetPuppetByJID ( portal . Key . Receiver )
2019-05-30 16:48:22 +02:00
portal . privateChatBackfillInvitePuppet = func ( ) {
if privateChatPuppetInvited {
return
}
privateChatPuppetInvited = true
_ , _ = portal . MainIntent ( ) . InviteUser ( portal . MXID , & mautrix . ReqInviteUser { UserID : privateChatPuppet . MXID } )
_ = privateChatPuppet . DefaultIntent ( ) . EnsureJoined ( portal . MXID )
}
2019-05-30 16:22:03 +02:00
}
2019-05-30 16:25:56 +02:00
return func ( ) {
portal . backfilling = false
2019-05-30 16:48:22 +02:00
portal . privateChatBackfillInvitePuppet = nil
2020-11-18 12:29:47 +01:00
portal . backfillLock . Unlock ( )
2019-05-30 16:48:22 +02:00
if privateChatPuppet != nil && privateChatPuppetInvited {
2019-05-30 16:25:56 +02:00
_ , _ = privateChatPuppet . DefaultIntent ( ) . LeaveRoom ( portal . MXID )
}
}
}
2020-06-08 19:51:24 +02:00
func ( portal * Portal ) disableNotifications ( user * User ) {
if ! portal . bridge . Config . Bridge . HistoryDisableNotifs {
return
}
puppet := portal . bridge . GetPuppetByCustomMXID ( user . MXID )
if puppet == nil || puppet . customIntent == nil {
return
}
portal . log . Debugfln ( "Disabling notifications for %s for backfilling" , user . MXID )
ruleID := fmt . Sprintf ( "net.maunium.silence_while_backfilling.%s" , portal . MXID )
err := puppet . customIntent . PutPushRule ( "global" , pushrules . OverrideRule , ruleID , & mautrix . ReqPutPushRule {
Actions : [ ] pushrules . PushActionType { pushrules . ActionDontNotify } ,
Conditions : [ ] pushrules . PushCondition { {
Kind : pushrules . KindEventMatch ,
Key : "room_id" ,
Pattern : string ( portal . MXID ) ,
} } ,
} )
if err != nil {
portal . log . Warnfln ( "Failed to disable notifications for %s while backfilling: %v" , user . MXID , err )
}
}
func ( portal * Portal ) enableNotifications ( user * User ) {
if ! portal . bridge . Config . Bridge . HistoryDisableNotifs {
return
}
puppet := portal . bridge . GetPuppetByCustomMXID ( user . MXID )
if puppet == nil || puppet . customIntent == nil {
return
}
portal . log . Debugfln ( "Re-enabling notifications for %s after backfilling" , user . MXID )
ruleID := fmt . Sprintf ( "net.maunium.silence_while_backfilling.%s" , portal . MXID )
err := puppet . customIntent . DeletePushRule ( "global" , pushrules . OverrideRule , ruleID )
if err != nil {
portal . log . Warnfln ( "Failed to re-enable notifications for %s after backfilling: %v" , user . MXID , err )
}
}
2019-05-30 16:25:56 +02:00
func ( portal * Portal ) FillInitialHistory ( user * User ) error {
if portal . bridge . Config . Bridge . InitialHistoryFill == 0 {
return nil
}
endBackfill := portal . beginBackfill ( )
defer endBackfill ( )
2019-05-30 16:48:22 +02:00
if portal . privateChatBackfillInvitePuppet != nil {
portal . privateChatBackfillInvitePuppet ( )
}
2019-05-30 16:25:56 +02:00
2019-05-22 21:05:28 +02:00
n := portal . bridge . Config . Bridge . InitialHistoryFill
portal . log . Infoln ( "Filling initial history, maximum" , n , "messages" )
var messages [ ] interface { }
before := ""
2019-05-28 20:30:39 +02:00
fromMe := true
2021-04-01 11:31:39 +02:00
chunkNum := 0
2019-05-22 21:05:28 +02:00
for n > 0 {
2021-04-01 11:31:39 +02:00
chunkNum += 1
2019-05-28 20:48:37 +02:00
count := 50
2019-05-22 21:05:28 +02:00
if n < count {
count = n
}
portal . log . Debugfln ( "Fetching chunk %d (%d messages / %d cap) before message %s" , chunkNum , count , n , before )
2019-05-28 20:30:39 +02:00
resp , err := user . Conn . LoadMessagesBefore ( portal . Key . JID , before , fromMe , count )
2019-05-22 21:05:28 +02:00
if err != nil {
return err
}
chunk , ok := resp . Content . ( [ ] interface { } )
2019-05-31 21:30:57 +02:00
if ! ok || len ( chunk ) == 0 {
2019-05-22 21:05:28 +02:00
portal . log . Infoln ( "Chunk empty, starting handling of loaded messages" )
break
}
2019-05-31 21:30:57 +02:00
messages = append ( chunk , messages ... )
2019-05-22 21:05:28 +02:00
portal . log . Debugfln ( "Fetched chunk and received %d messages" , len ( chunk ) )
n -= len ( chunk )
2019-05-28 20:30:39 +02:00
key := chunk [ 0 ] . ( * waProto . WebMessageInfo ) . GetKey ( )
before = key . GetId ( )
fromMe = key . GetFromMe ( )
2019-05-22 21:05:28 +02:00
if len ( before ) == 0 {
portal . log . Infoln ( "No message ID for first message, starting handling of loaded messages" )
break
}
2019-05-21 22:44:14 +02:00
}
2020-06-08 19:51:24 +02:00
portal . disableNotifications ( user )
2019-05-22 21:05:28 +02:00
portal . handleHistory ( user , messages )
2020-06-08 19:51:24 +02:00
portal . enableNotifications ( user )
2019-05-30 16:48:22 +02:00
portal . log . Infoln ( "Initial history fill complete" )
2019-05-22 21:05:28 +02:00
return nil
2019-05-22 15:46:18 +02:00
}
2019-05-22 21:05:28 +02:00
func ( portal * Portal ) handleHistory ( user * User , messages [ ] interface { } ) {
portal . log . Infoln ( "Handling" , len ( messages ) , "messages of history" )
2019-05-21 22:44:14 +02:00
for _ , rawMessage := range messages {
message , ok := rawMessage . ( * waProto . WebMessageInfo )
if ! ok {
portal . log . Warnln ( "Unexpected non-WebMessageInfo item in history response:" , rawMessage )
continue
}
2019-05-22 16:39:27 +02:00
data := whatsapp . ParseProtoMessage ( message )
2021-01-23 17:09:58 +01:00
if data == nil || data == whatsapp . ErrMessageTypeNotImplemented {
2020-06-23 18:03:20 +02:00
st := message . GetMessageStubType ( )
// Ignore some types that are known to fail
if st == waProto . WebMessageInfo_CALL_MISSED_VOICE || st == waProto . WebMessageInfo_CALL_MISSED_VIDEO ||
st == waProto . WebMessageInfo_CALL_MISSED_GROUP_VOICE || st == waProto . WebMessageInfo_CALL_MISSED_GROUP_VIDEO {
continue
}
2019-08-25 18:41:46 +02:00
portal . log . Warnln ( "Message" , message . GetKey ( ) . GetId ( ) , "failed to parse during backfilling" )
continue
}
2019-05-30 16:48:22 +02:00
if portal . privateChatBackfillInvitePuppet != nil && message . GetKey ( ) . GetFromMe ( ) && portal . IsPrivateChat ( ) {
portal . privateChatBackfillInvitePuppet ( )
}
2021-02-09 22:41:14 +01:00
portal . handleMessage ( PortalMessage { portal . Key . JID , user , data , message . GetMessageTimestamp ( ) } , true )
2019-05-21 22:44:14 +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 {
ID : portal . Key . JID ,
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 )
}
}
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-02-17 00:21:30 +01:00
var metadata * whatsapp . GroupInfo
2021-02-21 13:45:33 +01:00
var broadcastMetadata * whatsapp . 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-03-19 19:55:08 +01:00
puppet . SyncContactIfNecessary ( user )
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 ( ) {
var err error
broadcastMetadata , err = user . Conn . GetBroadcastMetadata ( portal . Key . JID )
2021-02-21 14:00:04 +01:00
if err == nil && broadcastMetadata . Status == 200 {
2021-02-21 13:45:33 +01:00
portal . Name = broadcastMetadata . Name
2021-02-21 13:18:15 +01:00
} else {
2021-06-19 01:22:01 +02:00
user . Conn . Store . ContactsLock . RLock ( )
2021-02-21 13:45:33 +01:00
contact , _ := user . Conn . Store . Contacts [ portal . Key . JID ]
2021-06-19 01:22:01 +02:00
user . Conn . Store . ContactsLock . RUnlock ( )
2021-02-21 13:45:33 +01:00
portal . Name = contact . Name
}
if len ( portal . Name ) == 0 {
2021-02-21 13:18:15 +01:00
portal . Name = UnnamedBroadcastName
}
portal . Topic = BroadcastTopic
2019-03-13 23:38:11 +01:00
} else {
2019-05-22 22:27:58 +02:00
var err error
metadata , err = user . Conn . GetGroupMetaData ( portal . Key . JID )
2019-03-13 23:38:11 +01:00
if err == nil && metadata . Status == 0 {
portal . Name = metadata . Name
portal . Topic = metadata . Topic
}
2020-06-15 13:56:52 +02:00
portal . UpdateAvatar ( user , nil , 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
if user . IsRelaybot {
invite = portal . bridge . Config . Bridge . Relaybot . InviteUsers
}
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 )
2019-05-22 22:27:58 +02:00
if metadata != nil {
2021-03-19 19:55:08 +01:00
portal . SyncParticipants ( user , metadata )
2020-10-12 12:59:14 +02:00
if metadata . Announce {
portal . RestrictMessageSending ( metadata . Announce )
}
2019-05-22 22:27:58 +02:00
}
2021-02-21 13:45:33 +01:00
if broadcastMetadata != nil {
2021-03-19 19:55:08 +01:00
portal . SyncBroadcastRecipients ( user , broadcastMetadata )
2021-02-21 13:45:33 +01:00
}
2021-02-26 15:09:24 +01:00
inCommunity := user . addPortalToCommunity ( portal )
2021-03-08 00:51:37 +01:00
if portal . IsPrivateChat ( ) && ! user . IsRelaybot {
2020-01-07 20:25:41 +01:00
puppet := user . bridge . GetPuppetByJID ( portal . Key . JID )
user . addPuppetToCommunity ( puppet )
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
user . CreateUserPortal ( database . PortalKeyWithMeta { PortalKey : portal . Key , InCommunity : inCommunity } )
2019-05-22 15:46:18 +02:00
err = portal . FillInitialHistory ( user )
2019-05-21 22:44:14 +02:00
if err != nil {
portal . log . Errorln ( "Failed to fill history:" , err )
}
2018-08-18 21:57:08 +02:00
return nil
}
func ( portal * Portal ) IsPrivateChat ( ) bool {
2018-09-01 22:38:03 +02:00
if portal . isPrivate == nil {
2021-02-17 00:21:30 +01:00
val := strings . HasSuffix ( portal . Key . JID , whatsapp . NewUserSuffix )
2018-09-01 22:38:03 +02:00
portal . isPrivate = & val
}
return * portal . isPrivate
2018-08-18 21:57:08 +02:00
}
2021-02-21 13:45:33 +01:00
func ( portal * Portal ) IsBroadcastList ( ) bool {
2021-02-21 13:18:15 +01:00
if portal . isBroadcast == nil {
val := strings . HasSuffix ( portal . Key . JID , whatsapp . BroadcastSuffix )
portal . isBroadcast = & val
}
return * portal . isBroadcast
}
2021-02-21 13:45:33 +01:00
func ( portal * Portal ) IsStatusBroadcastList ( ) bool {
2021-02-21 13:18:15 +01:00
return portal . Key . JID == "status@broadcast"
}
2019-11-10 20:22:11 +01:00
func ( portal * Portal ) HasRelaybot ( ) bool {
if portal . bridge . Relaybot == nil {
return false
} else if portal . hasRelaybot == nil {
2019-11-11 21:41:58 +01:00
val := portal . bridge . Relaybot . IsInPortal ( portal . Key )
2019-11-10 20:22:11 +01:00
portal . hasRelaybot = & val
}
return * portal . hasRelaybot
}
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
}
2020-05-08 21:32:22 +02:00
func ( portal * Portal ) SetReply ( content * event . MessageEventContent , info whatsapp . ContextInfo ) {
2018-08-23 23:52:06 +02:00
if len ( info . QuotedMessageID ) == 0 {
return
}
2018-08-28 23:40:54 +02:00
message := portal . bridge . DB . Message . GetByJID ( portal . Key , info . QuotedMessageID )
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
}
2020-06-30 15:26:13 +02:00
if evt . Type == event . EventEncrypted {
_ = evt . Content . ParseRaw ( evt . Type )
decryptedEvt , err := portal . bridge . Crypto . Decrypt ( evt )
if err != nil {
portal . log . Warnln ( "Failed to decrypt reply target:" , err )
} else {
evt = decryptedEvt
}
}
_ = evt . Content . ParseRaw ( evt . Type )
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-06-25 14:33:37 +02:00
func ( portal * Portal ) HandleMessageRevoke ( user * User , message whatsapp . MessageRevocation ) bool {
2019-05-16 00:59:36 +02:00
msg := portal . bridge . DB . Message . GetByJID ( portal . Key , message . Id )
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
}
2019-05-24 01:33:26 +02:00
var intent * appservice . IntentAPI
2019-05-16 00:59:36 +02:00
if message . FromMe {
if portal . IsPrivateChat ( ) {
2019-05-24 01:33:26 +02:00
intent = portal . bridge . GetPuppetByJID ( user . JID ) . CustomIntent ( )
2019-05-16 00:59:36 +02:00
} else {
2019-05-24 01:33:26 +02:00
intent = portal . bridge . GetPuppetByJID ( user . JID ) . IntentFor ( portal )
2019-05-16 00:59:36 +02:00
}
} else if len ( message . Participant ) > 0 {
2019-05-24 01:33:26 +02:00
intent = portal . bridge . GetPuppetByJID ( message . Participant ) . IntentFor ( portal )
}
if intent == nil {
intent = portal . MainIntent ( )
2019-05-16 00:59:36 +02:00
}
_ , err := intent . 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-06-25 14:33:37 +02:00
func ( portal * Portal ) HandleFakeMessage ( _ * User , message FakeMessage ) bool {
2019-05-30 16:00:36 +02:00
if portal . isRecentlyHandled ( message . ID ) {
2021-06-25 14:33:37 +02:00
return false
2019-05-30 16:00:36 +02:00
}
2020-05-21 19:14:43 +02:00
content := event . MessageEventContent {
MsgType : event . MsgNotice ,
Body : message . Text ,
}
if message . Alert {
content . MsgType = event . MsgText
}
_ , err := portal . sendMainIntentMessage ( content )
2019-05-30 16:00:36 +02:00
if err != nil {
portal . log . Errorfln ( "Failed to handle fake message %s: %v" , message . ID , err )
2021-06-25 14:33:37 +02:00
return true
2019-05-30 16:00:36 +02:00
}
portal . recentlyHandledLock . Lock ( )
index := portal . recentlyHandledIndex
portal . recentlyHandledIndex = ( portal . recentlyHandledIndex + 1 ) % recentlyHandledLength
portal . recentlyHandledLock . Unlock ( )
portal . recentlyHandled [ index ] = message . ID
2021-06-25 14:33:37 +02:00
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 )
}
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 ,
}
}
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 ( )
2020-05-09 01:03:59 +02:00
encrypted , err := portal . bridge . Crypto . Encrypt ( portal . MXID , eventType , wrappedContent )
2021-02-25 16:22:29 +01:00
portal . encryptLock . Unlock ( )
2020-05-09 01:03:59 +02:00
if err != nil {
2020-10-05 21:38:34 +02:00
return nil , fmt . Errorf ( "failed to encrypt event: %w" , err )
2020-05-09 01:03:59 +02:00
}
eventType = event . EventEncrypted
wrappedContent . Parsed = encrypted
}
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-06-25 14:33:37 +02:00
func ( portal * Portal ) HandleTextMessage ( source * User , message whatsapp . TextMessage ) bool {
2021-06-10 15:29:20 +02:00
intent := portal . startHandling ( source , message . Info , "text" )
2018-08-23 00:12:26 +02:00
if intent == nil {
2021-06-25 14:33:37 +02:00
return false
2018-08-19 17:21:38 +02:00
}
2018-08-23 00:12:26 +02:00
2020-05-08 21:32:22 +02:00
content := & event . MessageEventContent {
2018-08-24 18:46:14 +02:00
Body : message . Text ,
2020-05-08 21:32:22 +02:00
MsgType : event . MsgText ,
2018-08-24 18:46:14 +02:00
}
2018-08-24 21:05:38 +02:00
2020-07-31 13:30:58 +02:00
portal . bridge . Formatter . ParseWhatsApp ( content , message . ContextInfo . MentionedJID )
2019-11-10 20:22:11 +01:00
portal . SetReply ( content , message . ContextInfo )
2018-08-24 18:46:14 +02:00
2020-05-09 01:03:59 +02:00
resp , err := portal . sendMessage ( intent , event . EventMessage , content , int64 ( message . Info . Timestamp * 1000 ) )
2018-08-23 00:12:26 +02:00
if err != nil {
portal . log . Errorfln ( "Failed to handle message %s: %v" , message . Info . Id , err )
2021-06-25 14:33:37 +02:00
} else {
portal . finishHandling ( source , message . Info . Source , resp . EventID )
2018-08-23 00:12:26 +02:00
}
2021-06-25 14:33:37 +02:00
return true
2018-08-19 17:21:38 +02:00
}
2021-06-25 14:33:37 +02:00
func ( portal * Portal ) HandleStubMessage ( source * User , message whatsapp . StubMessage , isBackfill bool ) bool {
2021-02-21 13:45:33 +01:00
if portal . bridge . Config . Bridge . ChatMetaSync && ( ! portal . IsBroadcastList ( ) || isBackfill ) {
2021-02-09 22:41:14 +01:00
// Chat meta sync is enabled, so we use chat update commands and full-syncs instead of message history
2021-02-21 13:45:33 +01:00
// However, broadcast lists don't have update commands, so we handle these if it's not a backfill
2021-06-25 14:33:37 +02:00
return false
2021-02-09 22:41:14 +01:00
}
2021-06-10 15:29:20 +02:00
intent := portal . startHandling ( source , message . Info , fmt . Sprintf ( "stub %s" , message . Type . String ( ) ) )
2021-02-09 22:41:14 +01:00
if intent == nil {
2021-06-25 14:33:37 +02:00
return false
2021-02-09 22:41:14 +01:00
}
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" )
2021-02-21 13:45:33 +01:00
case waProto . WebMessageInfo_GROUP_PARTICIPANT_ADD , waProto . WebMessageInfo_GROUP_PARTICIPANT_INVITE , waProto . WebMessageInfo_BROADCAST_ADD :
2021-06-15 11:34:55 +02:00
eventID = portal . HandleWhatsAppInvite ( source , senderJID , intent , message . Params )
2021-02-21 13:45:33 +01:00
case waProto . WebMessageInfo_GROUP_PARTICIPANT_REMOVE , waProto . WebMessageInfo_GROUP_PARTICIPANT_LEAVE , waProto . WebMessageInfo_BROADCAST_REMOVE :
2021-02-10 19:27:14 +01:00
portal . HandleWhatsAppKick ( source , senderJID , message . Params )
2021-02-09 22:41:14 +01:00
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 :
2021-06-25 14:33:37 +02:00
return false
2021-02-09 22:41:14 +01:00
}
if len ( eventID ) == 0 {
eventID = id . EventID ( fmt . Sprintf ( "net.maunium.whatsapp.fake::%s" , message . Info . Id ) )
}
2021-02-17 00:22:06 +01:00
portal . markHandled ( source , message . Info . Source , eventID , true )
2021-06-25 14:33:37 +02:00
return true
2021-02-09 22:41:14 +01:00
}
2021-06-25 14:33:37 +02:00
func ( portal * Portal ) HandleLocationMessage ( source * User , message whatsapp . LocationMessage ) bool {
2021-06-10 15:29:20 +02:00
intent := portal . startHandling ( source , message . Info , "location" )
2020-06-10 13:58:57 +02:00
if intent == nil {
2021-06-25 14:33:37 +02:00
return false
2020-06-10 13:58:57 +02:00
}
url := message . Url
if len ( url ) == 0 {
url = fmt . Sprintf ( "https://maps.google.com/?q=%.5f,%.5f" , message . DegreesLatitude , message . DegreesLongitude )
}
name := message . Name
if len ( name ) == 0 {
latChar := 'N'
if message . DegreesLatitude < 0 {
latChar = 'S'
}
longChar := 'E'
if message . DegreesLongitude < 0 {
longChar = 'W'
}
name = fmt . Sprintf ( "%.4f° %c %.4f° %c" , math . Abs ( message . DegreesLatitude ) , latChar , math . Abs ( message . DegreesLongitude ) , longChar )
}
content := & event . MessageEventContent {
MsgType : event . MsgLocation ,
Body : fmt . Sprintf ( "Location: %s\n%s\n%s" , name , message . Address , url ) ,
Format : event . FormatHTML ,
FormattedBody : fmt . Sprintf ( "Location: <a href='%s'>%s</a><br>%s" , url , name , message . Address ) ,
GeoURI : fmt . Sprintf ( "geo:%.5f,%.5f" , message . DegreesLatitude , message . DegreesLongitude ) ,
}
if len ( message . JpegThumbnail ) > 0 {
thumbnailMime := http . DetectContentType ( message . JpegThumbnail )
uploadedThumbnail , _ := intent . UploadBytes ( message . JpegThumbnail , thumbnailMime )
if uploadedThumbnail != nil {
cfg , _ , _ := image . DecodeConfig ( bytes . NewReader ( message . JpegThumbnail ) )
content . Info = & event . FileInfo {
ThumbnailInfo : & event . FileInfo {
Size : len ( message . JpegThumbnail ) ,
Width : cfg . Width ,
Height : cfg . Height ,
MimeType : thumbnailMime ,
} ,
ThumbnailURL : uploadedThumbnail . ContentURI . CUString ( ) ,
}
}
}
portal . SetReply ( content , message . ContextInfo )
resp , err := portal . sendMessage ( intent , event . EventMessage , content , int64 ( message . Info . Timestamp * 1000 ) )
if err != nil {
portal . log . Errorfln ( "Failed to handle message %s: %v" , message . Info . Id , err )
2021-06-25 14:33:37 +02:00
} else {
portal . finishHandling ( source , message . Info . Source , resp . EventID )
2020-06-10 13:06:36 +02:00
}
2021-06-25 14:33:37 +02:00
return true
2020-06-10 13:58:57 +02:00
}
2020-06-10 13:06:36 +02:00
2021-06-25 14:33:37 +02:00
func ( portal * Portal ) HandleContactMessage ( source * User , message whatsapp . ContactMessage ) bool {
2021-06-10 15:29:20 +02:00
intent := portal . startHandling ( source , message . Info , "contact" )
2020-06-10 13:06:36 +02:00
if intent == nil {
2021-06-25 14:33:37 +02:00
return false
2020-06-10 13:06:36 +02:00
}
fileName := fmt . Sprintf ( "%s.vcf" , message . DisplayName )
2020-06-10 14:26:14 +02:00
data := [ ] byte ( message . Vcard )
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 {
portal . log . Errorfln ( "Failed to upload vcard of %s: %v" , message . DisplayName , err )
2021-06-25 14:33:37 +02:00
return true
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 ,
2020-06-10 13:06:36 +02:00
Size : len ( message . Vcard ) ,
} ,
}
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
portal . SetReply ( content , message . ContextInfo )
resp , err := portal . sendMessage ( intent , event . EventMessage , content , int64 ( message . Info . Timestamp * 1000 ) )
if err != nil {
portal . log . Errorfln ( "Failed to handle message %s: %v" , message . Info . Id , err )
2021-06-25 14:33:37 +02:00
} else {
portal . finishHandling ( source , message . Info . Source , resp . EventID )
2020-06-10 13:06:36 +02:00
}
2021-06-25 14:33:37 +02:00
return true
2020-06-10 13:06:36 +02:00
}
2020-07-21 15:08:54 +02:00
func ( portal * Portal ) sendMediaBridgeFailure ( source * User , intent * appservice . IntentAPI , info whatsapp . MessageInfo , bridgeErr error ) {
portal . log . Errorfln ( "Failed to bridge media for %s: %v" , info . Id , bridgeErr )
2020-05-24 16:46:19 +02:00
resp , err := portal . sendMessage ( intent , event . EventMessage , & event . MessageEventContent {
MsgType : event . MsgNotice ,
Body : "Failed to bridge media" ,
} , int64 ( info . Timestamp * 1000 ) )
if err != nil {
portal . log . Errorfln ( "Failed to send media download error message for %s: %v" , info . Id , err )
} else {
portal . finishHandling ( source , info . Source , resp . EventID )
}
}
2020-06-10 14:26:14 +02:00
func ( portal * Portal ) encryptFile ( data [ ] byte , mimeType string ) ( [ ] byte , string , * event . EncryptedFileInfo ) {
if ! portal . Encrypted {
return data , mimeType , nil
}
file := & event . EncryptedFileInfo {
EncryptedFile : * attachment . NewEncryptedFile ( ) ,
URL : "" ,
}
return file . Encrypt ( data ) , "application/octet-stream" , file
2020-06-20 21:24:27 +02:00
}
2020-06-10 14:26:14 +02:00
2020-06-25 22:58:35 +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 {
_ , _ = targetIntent . LeaveRoom ( portal . MXID )
}
}
} else {
_ , err := targetIntent . LeaveRoom ( portal . MXID )
if err != nil {
portal . log . Warnfln ( "Failed to leave portal as %s: %v" , target , err )
_ , _ = portal . MainIntent ( ) . KickUser ( portal . MXID , & mautrix . ReqKickUser { UserID : target } )
}
}
}
2021-02-10 19:27:14 +01:00
func ( portal * Portal ) HandleWhatsAppKick ( source * User , senderJID string , jids [ ] string ) {
2020-06-25 22:58:35 +02:00
sender := portal . bridge . GetPuppetByJID ( senderJID )
senderIntent := sender . IntentFor ( portal )
for _ , jid := range jids {
2021-02-10 19:27:14 +01:00
if source != nil && source . JID == jid {
portal . log . Debugln ( "Ignoring self-kick by" , source . MXID )
continue
}
2020-06-25 22:58:35 +02:00
puppet := portal . bridge . GetPuppetByJID ( jid )
portal . removeUser ( puppet . JID == sender . JID , senderIntent , puppet . MXID , puppet . DefaultIntent ( ) )
2021-02-21 13:45:33 +01:00
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 )
2020-06-25 22:58:35 +02:00
}
}
}
}
2021-06-15 11:34:55 +02:00
func ( portal * Portal ) HandleWhatsAppInvite ( source * User , senderJID string , intent * appservice . IntentAPI , jids [ ] string ) ( evtID id . EventID ) {
2021-02-09 22:41:14 +01:00
if intent == nil {
intent = portal . MainIntent ( )
if senderJID != "unknown" {
sender := portal . bridge . GetPuppetByJID ( senderJID )
intent = sender . IntentFor ( portal )
}
2020-06-25 23:05:51 +02:00
}
for _ , jid := range jids {
puppet := portal . bridge . GetPuppetByJID ( jid )
2021-06-15 11:34:55 +02:00
puppet . SyncContactIfNecessary ( source )
2021-03-05 10:29:08 +01:00
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 )
2020-06-25 23:05:51 +02:00
if err != nil {
2021-02-09 22:41:14 +01:00
portal . log . Warnfln ( "Failed to invite %s as %s: %v" , puppet . MXID , intent . UserID , err )
2021-02-10 19:27:14 +01:00
_ = portal . MainIntent ( ) . EnsureInvited ( portal . MXID , puppet . MXID )
2021-03-05 10:29:08 +01:00
} else {
evtID = resp . EventID
2020-06-25 23:05:51 +02:00
}
err = puppet . DefaultIntent ( ) . EnsureJoined ( portal . MXID )
if err != nil {
portal . log . Errorfln ( "Failed to ensure %s is joined: %v" , puppet . MXID , err )
}
}
2021-03-05 10:29:08 +01:00
return
2020-06-25 23:05:51 +02:00
}
2020-06-20 21:24:27 +02:00
type base struct {
download func ( ) ( [ ] byte , error )
info whatsapp . MessageInfo
context whatsapp . ContextInfo
mimeType string
2020-06-10 14:26:14 +02:00
}
2020-06-20 21:24:27 +02:00
type mediaMessage struct {
base
thumbnail [ ] byte
caption string
fileName string
length uint32
sendAsSticker bool
}
2021-06-25 14:33:37 +02:00
func ( portal * Portal ) HandleMediaMessage ( source * User , msg mediaMessage ) bool {
2021-06-10 15:29:20 +02:00
intent := portal . startHandling ( source , msg . info , fmt . Sprintf ( "media %s" , msg . mimeType ) )
2018-08-23 00:12:26 +02:00
if intent == nil {
2021-06-25 14:33:37 +02:00
return false
2018-08-19 17:21:38 +02:00
}
2018-08-23 00:12:26 +02:00
2020-06-20 21:24:27 +02:00
data , err := msg . download ( )
2021-08-06 00:33:17 +02:00
if errors . Is ( err , whatsapp . ErrMediaDownloadFailedWith404 ) || errors . Is ( err , whatsapp . ErrMediaDownloadFailedWith410 ) {
2020-06-20 21:24:27 +02:00
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 )
2020-05-24 16:46:19 +02:00
if err != nil {
2020-10-05 21:38:34 +02:00
portal . sendMediaBridgeFailure ( source , intent , msg . info , fmt . Errorf ( "failed to load media info: %w" , err ) )
2021-06-25 14:33:37 +02:00
return true
2020-05-24 16:46:19 +02:00
}
2020-06-20 21:24:27 +02:00
data , err = msg . download ( )
2020-05-24 16:46:19 +02:00
}
2021-08-06 00:33:17 +02:00
if errors . Is ( err , whatsapp . ErrNoURLPresent ) {
2020-06-20 21:24:27 +02:00
portal . log . Debugfln ( "No URL present error for media message %s, ignoring..." , msg . info . Id )
2021-06-25 14:33:37 +02:00
return true
2021-08-06 00:33:17 +02:00
} else if errors . Is ( err , whatsapp . ErrInvalidMediaHMAC ) || errors . Is ( err , whatsapp . ErrFileLengthMismatch ) {
portal . log . Warnfln ( "Got error '%v' while downloading media in %s, but official WhatsApp clients don't seem to care, so ignoring that error and bridging file anyway" , err , msg . info . Id )
2019-12-31 19:17:03 +01:00
} else if err != nil {
2020-06-20 21:24:27 +02:00
portal . sendMediaBridgeFailure ( source , intent , msg . info , err )
2021-06-25 14:33:37 +02:00
return true
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
2020-06-20 21:24:27 +02:00
if strings . HasPrefix ( msg . mimeType , "image/" ) {
2020-06-10 14:26:14 +02:00
cfg , _ , _ := image . DecodeConfig ( bytes . NewReader ( data ) )
width , height = cfg . Width , cfg . Height
}
2020-06-20 21:24:27 +02:00
data , uploadMimeType , file := portal . encryptFile ( data , msg . mimeType )
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 ) {
portal . sendMediaBridgeFailure ( source , intent , msg . info , errors . New ( "homeserver rejected too large file" ) )
2021-03-04 19:46:08 +01:00
} else if httpErr , ok := err . ( mautrix . HTTPError ) ; ok && httpErr . IsStatus ( 413 ) {
2020-09-24 14:25:36 +02:00
portal . sendMediaBridgeFailure ( source , intent , msg . info , errors . New ( "proxy rejected too large file" ) )
2020-07-21 15:08:54 +02:00
} else {
2020-10-05 21:38:34 +02:00
portal . sendMediaBridgeFailure ( source , intent , msg . info , fmt . Errorf ( "failed to upload media: %w" , err ) )
2020-07-21 15:08:54 +02:00
}
2021-06-25 14:33:37 +02:00
return true
2018-08-19 17:21:38 +02:00
}
2018-08-27 22:15:05 +02:00
2020-06-20 21:24:27 +02:00
if msg . fileName == "" {
2020-11-02 16:18:18 +01:00
mimeClass := strings . Split ( msg . mimeType , "/" ) [ 0 ]
switch mimeClass {
case "application" :
msg . fileName = "file"
default :
msg . fileName = mimeClass
}
2020-06-20 17:26:45 +02:00
2020-06-20 21:24:27 +02:00
exts , _ := mime . ExtensionsByType ( msg . mimeType )
2020-06-20 17:26:45 +02:00
if exts != nil && len ( exts ) > 0 {
2020-06-20 21:24:27 +02:00
msg . fileName += exts [ 0 ]
2020-06-20 17:26:45 +02:00
}
2018-08-23 23:52:06 +02:00
}
2020-05-08 21:32:22 +02:00
content := & event . MessageEventContent {
2020-06-20 21:24:27 +02:00
Body : msg . fileName ,
2020-06-10 14:26:14 +02:00
File : file ,
2020-05-08 21:32:22 +02:00
Info : & event . FileInfo {
2018-08-23 23:52:06 +02:00
Size : len ( data ) ,
2020-06-20 21:24:27 +02:00
MimeType : msg . mimeType ,
2020-06-10 14:26:14 +02:00
Width : width ,
Height : height ,
2020-06-20 21:24:27 +02:00
Duration : int ( msg . length ) ,
2018-08-23 23:52:06 +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 ( )
}
2020-06-20 21:24:27 +02:00
portal . SetReply ( content , msg . context )
2018-08-23 23:52:06 +02:00
2020-06-20 21:24:27 +02:00
if msg . thumbnail != nil && portal . bridge . Config . Bridge . WhatsappThumbnail {
thumbnailMime := http . DetectContentType ( msg . thumbnail )
thumbnailCfg , _ , _ := image . DecodeConfig ( bytes . NewReader ( msg . thumbnail ) )
thumbnailSize := len ( msg . thumbnail )
thumbnail , thumbnailUploadMime , thumbnailFile := portal . encryptFile ( msg . thumbnail , thumbnailMime )
2020-06-10 14:26:14 +02:00
uploadedThumbnail , err := intent . UploadBytes ( thumbnail , thumbnailUploadMime )
if err != nil {
2020-06-20 21:24:27 +02:00
portal . log . Warnfln ( "Failed to upload thumbnail for %s: %v" , msg . 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 ,
}
}
}
2020-06-20 21:24:27 +02:00
switch strings . ToLower ( strings . Split ( msg . mimeType , "/" ) [ 0 ] ) {
2018-08-23 23:52:06 +02:00
case "image" :
2020-06-20 21:24:27 +02:00
if ! msg . sendAsSticker {
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-06-20 21:24:27 +02:00
ts := int64 ( msg . info . Timestamp * 1000 )
2020-05-08 21:32:22 +02:00
eventType := event . EventMessage
2020-06-20 21:24:27 +02:00
if msg . sendAsSticker {
2020-05-08 21:32:22 +02:00
eventType = event . EventSticker
2019-10-04 20:01:53 +02:00
}
2020-05-09 01:03:59 +02:00
resp , err := portal . sendMessage ( intent , eventType , content , ts )
2018-08-23 00:12:26 +02:00
if err != nil {
2020-06-20 21:24:27 +02:00
portal . log . Errorfln ( "Failed to handle message %s: %v" , msg . info . Id , err )
2021-06-25 14:33:37 +02:00
return true
2018-08-23 00:12:26 +02:00
}
2018-08-27 22:15:05 +02:00
2020-06-20 21:24:27 +02:00
if len ( msg . caption ) > 0 {
2020-05-08 21:32:22 +02:00
captionContent := & event . MessageEventContent {
2020-06-20 21:24:27 +02:00
Body : msg . caption ,
2020-05-08 21:32:22 +02:00
MsgType : event . MsgNotice ,
2018-08-27 22:15:05 +02:00
}
2020-07-31 13:30:58 +02:00
portal . bridge . Formatter . ParseWhatsApp ( captionContent , msg . context . MentionedJID )
2018-08-27 22:15:05 +02:00
2021-06-22 11:35:38 +02:00
resp , err = portal . sendMessage ( intent , event . EventMessage , captionContent , ts )
2018-08-27 22:15:05 +02:00
if err != nil {
2020-06-20 21:24:27 +02:00
portal . log . Warnfln ( "Failed to handle caption of message %s: %v" , msg . info . Id , err )
2018-08-27 22:15:05 +02:00
}
}
2020-06-20 21:24:27 +02:00
portal . finishHandling ( source , msg . info . Source , resp . EventID )
2021-06-25 14:33:37 +02:00
return true
2018-08-19 17:21:38 +02:00
}
2018-08-25 23:26:24 +02:00
func makeMessageID ( ) * string {
2018-08-24 18:46:14 +02:00
b := make ( [ ] byte , 10 )
rand . Read ( b )
2018-08-25 23:26:24 +02:00
str := strings . ToUpper ( hex . EncodeToString ( b ) )
return & str
2018-08-24 18:46:14 +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
}
2020-05-20 15:43:55 +02:00
func ( portal * Portal ) preprocessMatrixMedia ( sender * User , relaybotFormatted bool , content * event . MessageEventContent , eventID id . EventID , mediaType whatsapp . MediaType ) * MediaUpload {
2019-11-10 20:22:11 +01:00
var caption string
2021-02-17 00:21:30 +01:00
var mentionedJIDs [ ] whatsapp . JID
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
}
}
2020-06-23 15:36:05 +02:00
if mediaType == whatsapp . MediaVideo && content . GetInfo ( ) . MimeType == "image/gif" {
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-06 20:11:37 +02:00
if mediaType == whatsapp . MediaImage && content . GetInfo ( ) . MimeType == "image/webp" {
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"
}
2020-05-08 21:32:22 +02:00
url , mediaKey , fileEncSHA256 , fileSHA256 , fileLength , err := sender . Conn . Upload ( bytes . NewReader ( 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 {
Caption : caption ,
2020-07-31 13:30:58 +02:00
MentionedJIDs : mentionedJIDs ,
2018-08-25 23:26:24 +02:00
URL : url ,
MediaKey : mediaKey ,
FileEncSHA256 : fileEncSHA256 ,
FileSHA256 : fileSHA256 ,
FileLength : fileLength ,
2020-05-20 15:43:55 +02:00
Thumbnail : portal . downloadThumbnail ( content , eventID ) ,
2018-08-25 23:26:24 +02:00
}
}
type MediaUpload struct {
Caption string
2021-02-17 00:21:30 +01:00
MentionedJIDs [ ] whatsapp . JID
2018-08-25 23:26:24 +02:00
URL string
MediaKey [ ] byte
FileEncSHA256 [ ] byte
FileSHA256 [ ] byte
FileLength uint64
Thumbnail [ ] byte
}
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
} 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
}
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
}
2020-05-08 21:32:22 +02:00
data , err := portal . bridge . Config . Bridge . Relaybot . 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 ,
}
}
2020-05-25 22:11:00 +02:00
func ( portal * Portal ) convertMatrixMessage ( sender * User , evt * event . Event ) ( * waProto . WebMessageInfo , * User ) {
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
2018-08-25 23:26:24 +02:00
ts := uint64 ( evt . Timestamp / 1000 )
2021-06-01 12:32:14 +02:00
status := waProto . WebMessageInfo_PENDING
2021-06-22 19:33:30 +02:00
trueVal := true
2018-08-25 23:26:24 +02:00
info := & waProto . WebMessageInfo {
Key : & waProto . MessageKey {
2021-06-22 19:33:30 +02:00
FromMe : & trueVal ,
2018-08-25 23:26:24 +02:00
Id : makeMessageID ( ) ,
2018-08-28 23:40:54 +02:00
RemoteJid : & portal . Key . JID ,
2018-08-25 23:26:24 +02:00
} ,
2021-06-01 12:32:14 +02:00
MessageTimestamp : & ts ,
MessageC2STimestamp : & ts ,
Message : & waProto . Message { } ,
Status : & status ,
2018-08-25 23:26:24 +02:00
}
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 ( )
2018-08-25 23:26:24 +02:00
msg := portal . bridge . DB . Message . GetByMXID ( replyToID )
2021-08-19 18:17:19 +02:00
if msg != nil {
2018-09-01 22:38:03 +02:00
ctxInfo . StanzaId = & msg . JID
ctxInfo . Participant = & msg . Sender
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
if sender . NeedsRelaybot ( portal ) {
if ! portal . HasRelaybot ( ) {
2019-11-13 08:40:41 +01:00
if sender . HasSession ( ) {
2019-11-13 08:36:43 +01:00
portal . log . Debugln ( "Database says" , sender . MXID , "not in chat and no relaybot, but trying to send anyway" )
} else {
portal . log . Debugln ( "Ignoring message from" , sender . MXID , "in chat with no relaybot" )
2020-05-25 22:11:00 +02:00
return nil , sender
2019-11-13 08:36:43 +01:00
}
} else {
2020-05-08 21:32:22 +02:00
relaybotFormatted = portal . addRelaybotFormat ( sender , content )
2019-11-13 08:36:43 +01:00
sender = portal . bridge . Relaybot
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 {
info . Message . ExtendedTextMessage = & waProto . ExtendedTextMessage {
Text : & text ,
ContextInfo : ctxInfo ,
}
} else {
info . Message . Conversation = & text
}
2020-05-08 21:32:22 +02:00
case event . MsgImage :
media := portal . preprocessMatrixMedia ( sender , relaybotFormatted , content , evt . ID , whatsapp . 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
2018-08-25 23:26:24 +02:00
info . Message . ImageMessage = & waProto . ImageMessage {
2020-07-31 13:30:58 +02:00
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 ,
FileLength : & media . FileLength ,
}
2020-05-08 21:32:22 +02:00
case event . MsgVideo :
2020-06-23 15:36:05 +02:00
gifPlayback := content . GetInfo ( ) . MimeType == "image/gif"
2020-05-08 21:32:22 +02:00
media := portal . preprocessMatrixMedia ( sender , relaybotFormatted , content , evt . ID , whatsapp . 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
2018-08-25 23:26:24 +02:00
info . Message . VideoMessage = & waProto . VideoMessage {
2020-07-31 13:30:58 +02:00
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 ,
FileLength : & media . FileLength ,
}
2020-05-08 21:32:22 +02:00
case event . MsgAudio :
media := portal . preprocessMatrixMedia ( sender , relaybotFormatted , content , evt . ID , whatsapp . 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 )
2018-08-25 23:26:24 +02:00
info . Message . AudioMessage = & waProto . AudioMessage {
2020-07-31 13:30:58 +02:00
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 ,
FileLength : & media . FileLength ,
}
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-06-22 19:33:30 +02:00
info . Message . AudioMessage . Ptt = & trueVal
// hacky hack to add the codecs param that whatsapp seems to require
mimeWithCodec := addCodecToMime ( content . GetInfo ( ) . MimeType , "opus" )
info . Message . AudioMessage . Mimetype = & mimeWithCodec
}
2020-05-08 21:32:22 +02:00
case event . MsgFile :
media := portal . preprocessMatrixMedia ( sender , relaybotFormatted , content , evt . ID , whatsapp . 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
}
2018-08-25 23:26:24 +02:00
info . Message . DocumentMessage = & waProto . DocumentMessage {
2020-07-31 13:30:58 +02:00
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 ,
FileLength : & media . FileLength ,
}
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
}
info . Message . LocationMessage = & waProto . LocationMessage {
DegreesLatitude : & lat ,
DegreesLongitude : & long ,
Comment : & content . Body ,
ContextInfo : ctxInfo ,
}
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
}
2020-05-25 22:11:00 +02:00
return info , sender
2020-05-24 16:28:30 +02:00
}
func ( portal * Portal ) wasMessageSent ( sender * User , id string ) bool {
_ , err := sender . Conn . LoadMessagesAfter ( portal . Key . JID , id , true , 0 )
if err != nil {
if err != whatsapp . ErrServerRespondedWith404 {
portal . log . Warnfln ( "Failed to check if message was bridged without response: %v" , err )
}
return false
}
return true
}
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 )
}
}
}
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 ( ) &&
( ( portal . IsPrivateChat ( ) && sender . JID != portal . Key . Receiver ) ||
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 )
2020-05-25 22:11:00 +02:00
info , sender := portal . convertMatrixMessage ( sender , evt )
if info == nil {
return
}
2021-02-17 00:22:06 +01:00
dbMsg := portal . markHandled ( sender , info , evt . ID , false )
portal . sendRaw ( sender , evt , info , dbMsg )
2020-11-04 13:58:24 +01:00
}
2020-05-24 14:33:26 +02:00
2021-02-17 00:22:06 +01:00
func ( portal * Portal ) sendRaw ( sender * User , evt * event . Event , info * waProto . WebMessageInfo , dbMsg * database . Message ) {
2021-06-01 12:32:14 +02:00
portal . log . Debugln ( "Sending event" , evt . ID , "to WhatsApp" , info . Key . GetId ( ) )
2020-05-24 14:33:26 +02:00
errChan := make ( chan error , 1 )
go sender . Conn . SendRaw ( info , errChan )
2020-05-24 16:28:30 +02:00
var err error
var errorEventID id . EventID
2020-05-24 14:33:26 +02:00
select {
case err = <- errChan :
case <- time . After ( time . Duration ( portal . bridge . Config . Bridge . ConnectionTimeout ) * time . Second ) :
2020-05-24 16:28:30 +02:00
if portal . bridge . Config . Bridge . FetchMessageOnTimeout && portal . wasMessageSent ( sender , info . Key . GetId ( ) ) {
portal . log . Debugln ( "Matrix event %s was bridged, but response didn't arrive within timeout" )
2020-05-25 10:17:47 +02:00
portal . sendDeliveryReceipt ( evt . ID )
2020-05-24 16:28:30 +02:00
} else {
portal . log . Warnfln ( "Response when bridging Matrix event %s is taking long to arrive" , evt . ID )
2021-06-15 11:05:11 +02:00
errorEventID = portal . sendErrorMessage ( "message sending timed out" , false )
2020-05-24 14:33:26 +02:00
}
err = <- errChan
}
2018-08-19 17:21:38 +02:00
if err != nil {
2021-06-15 11:05:11 +02:00
var statusErr whatsapp . StatusResponse
errors . As ( err , & statusErr )
var confirmed bool
var errMsg string
switch statusErr . Status {
case 400 :
portal . log . Errorfln ( "400 response handling Matrix event %s: %+v" , evt . ID , statusErr . Extra )
errMsg = "WhatsApp rejected the message (status code 400)."
if info . Message . ImageMessage != nil || info . Message . VideoMessage != nil || info . Message . AudioMessage != nil || info . Message . DocumentMessage != nil {
errMsg += " The attachment type you sent may be unsupported."
}
confirmed = true
case 599 :
errMsg = "WhatsApp rate-limited the message (status code 599)."
default :
portal . log . Errorfln ( "Error handling Matrix event %s: %v" , evt . ID , err )
errMsg = err . Error ( )
}
portal . sendErrorMessage ( errMsg , confirmed )
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
}
2020-05-24 16:28:30 +02:00
if errorEventID != "" {
_ , err = portal . MainIntent ( ) . RedactEvent ( portal . MXID , errorEventID )
2019-08-30 21:04:57 +02:00
if err != nil {
2020-05-24 16:28:30 +02:00
portal . log . Warnfln ( "Failed to redact timeout warning message %s: %v" , errorEventID , err )
2019-08-30 21:04:57 +02:00
}
2018-08-19 17:21:38 +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 ) {
2019-05-16 00:59:36 +02:00
if portal . IsPrivateChat ( ) && sender . JID != portal . Key . Receiver {
return
}
msg := portal . bridge . DB . Message . GetByMXID ( evt . Redacts )
2019-06-18 07:24:17 +02:00
if msg == nil || msg . Sender != sender . JID {
2019-05-16 00:59:36 +02:00
return
}
ts := uint64 ( evt . Timestamp / 1000 )
status := waProto . WebMessageInfo_PENDING
protoMsgType := waProto . ProtocolMessage_REVOKE
fromMe := true
info := & waProto . WebMessageInfo {
Key : & waProto . MessageKey {
FromMe : & fromMe ,
Id : makeMessageID ( ) ,
RemoteJid : & portal . Key . JID ,
} ,
MessageTimestamp : & ts ,
Message : & waProto . Message {
ProtocolMessage : & waProto . ProtocolMessage {
Type : & protoMsgType ,
Key : & waProto . MessageKey {
FromMe : & fromMe ,
Id : & msg . JID ,
RemoteJid : & portal . Key . JID ,
} ,
} ,
} ,
Status : & status ,
}
2020-05-24 14:33:26 +02:00
errChan := make ( chan error , 1 )
go sender . Conn . SendRaw ( info , errChan )
var err error
select {
case err = <- errChan :
case <- time . After ( time . Duration ( portal . bridge . Config . Bridge . ConnectionTimeout ) * time . Second ) :
portal . log . Warnfln ( "Response when bridging Matrix redaction %s is taking long to arrive" , evt . ID )
err = <- errChan
}
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 {
2020-05-24 14:33:26 +02:00
portal . log . Debugln ( "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?
2020-06-25 22:33:11 +02:00
resp , err := sender . Conn . 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 {
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 )
}
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 {
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
}
2021-03-05 10:29:08 +01:00
portal . log . Infofln ( "Add %s response: %s" , puppet . JID , <- resp )
2020-06-25 22:58:35 +02:00
}
}
2021-02-09 22:41:14 +01:00
func ( portal * Portal ) HandleMatrixMeta ( sender * User , evt * event . Event ) {
var resp <- chan string
var err error
switch content := evt . Content . Parsed . ( type ) {
case * event . RoomNameEventContent :
if content . Name == portal . Name {
return
}
portal . Name = content . Name
resp , err = sender . Conn . UpdateGroupSubject ( content . Name , portal . Key . JID )
case * event . TopicEventContent :
if content . Topic == portal . Topic {
return
}
portal . Topic = content . Topic
2021-02-18 22:36:14 +01:00
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 {
out := <- resp
portal . log . Debugln ( "Successfully updated metadata:" , out )
}
}