2018-08-13 22:24:44 +02:00
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
2020-05-08 21:32:22 +02:00
// Copyright (C) 2020 Tulir Asokan
2018-08-13 00:00:23 +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
2018-08-13 22:24:44 +02:00
import (
2019-05-15 22:04:09 +02:00
"fmt"
2018-08-26 16:02:32 +02:00
"strings"
2019-01-11 20:17:31 +01:00
"maunium.net/go/maulogger/v2"
2020-07-05 17:57:03 +02:00
"maunium.net/go/mautrix"
2020-05-09 13:31:06 +02:00
"maunium.net/go/mautrix/appservice"
2020-05-08 21:32:22 +02:00
"maunium.net/go/mautrix/event"
2019-05-15 22:04:09 +02:00
"maunium.net/go/mautrix/format"
2020-05-08 21:32:22 +02:00
"maunium.net/go/mautrix/id"
2020-07-05 17:57:03 +02:00
"maunium.net/go/mautrix-whatsapp/database"
2018-08-13 22:24:44 +02:00
)
2018-08-13 00:00:23 +02:00
2018-08-18 21:57:08 +02:00
type MatrixHandler struct {
bridge * Bridge
as * appservice . AppService
log maulogger . Logger
cmd * CommandHandler
}
func NewMatrixHandler ( bridge * Bridge ) * MatrixHandler {
handler := & MatrixHandler {
bridge : bridge ,
2018-08-28 23:40:54 +02:00
as : bridge . AS ,
2018-08-18 21:57:08 +02:00
log : bridge . Log . Sub ( "Matrix" ) ,
cmd : NewCommandHandler ( bridge ) ,
}
2020-05-08 21:32:22 +02:00
bridge . EventProcessor . On ( event . EventMessage , handler . HandleMessage )
2020-05-09 01:03:59 +02:00
bridge . EventProcessor . On ( event . EventEncrypted , handler . HandleEncrypted )
2020-05-08 21:32:22 +02:00
bridge . EventProcessor . On ( event . EventSticker , handler . HandleMessage )
bridge . EventProcessor . On ( event . EventRedaction , handler . HandleRedaction )
bridge . EventProcessor . On ( event . StateMember , handler . HandleMembership )
bridge . EventProcessor . On ( event . StateRoomName , handler . HandleRoomMetadata )
bridge . EventProcessor . On ( event . StateRoomAvatar , handler . HandleRoomMetadata )
bridge . EventProcessor . On ( event . StateTopic , handler . HandleRoomMetadata )
2020-05-09 01:03:59 +02:00
bridge . EventProcessor . On ( event . StateEncryption , handler . HandleEncryption )
2018-08-18 21:57:08 +02:00
return handler
}
2020-05-09 01:03:59 +02:00
func ( mx * MatrixHandler ) HandleEncryption ( evt * event . Event ) {
2020-06-17 16:50:06 +02:00
defer mx . bridge . Metrics . TrackEvent ( evt . Type ) ( )
2020-05-09 01:03:59 +02:00
if evt . Content . AsEncryption ( ) . Algorithm != id . AlgorithmMegolmV1 {
return
}
portal := mx . bridge . GetPortalByMXID ( evt . RoomID )
if portal != nil && ! portal . Encrypted {
mx . log . Debugfln ( "%s enabled encryption in %s" , evt . Sender , evt . RoomID )
portal . Encrypted = true
portal . Update ( )
}
}
2020-07-05 17:57:03 +02:00
func ( mx * MatrixHandler ) joinAndCheckMembers ( evt * event . Event , intent * appservice . IntentAPI ) * mautrix . RespJoinedMembers {
2020-05-08 21:32:22 +02:00
resp , err := intent . JoinRoomByID ( evt . RoomID )
2018-08-16 14:59:18 +02:00
if err != nil {
2020-07-05 17:57:03 +02:00
mx . log . Debugfln ( "Failed to join room %s as %s with invite from %s: %v" , evt . RoomID , intent . UserID , evt . Sender , err )
return nil
2018-08-16 14:59:18 +02:00
}
2018-08-16 18:20:07 +02:00
members , err := intent . JoinedMembers ( resp . RoomID )
2018-08-16 14:59:18 +02:00
if err != nil {
2020-07-05 17:57:03 +02:00
mx . log . Debugfln ( "Failed to get members in room %s after accepting invite from %s as %s: %v" , resp . RoomID , evt . Sender , intent . UserID , err )
2020-06-25 16:01:40 +02:00
_ , _ = intent . LeaveRoom ( resp . RoomID )
2020-07-05 17:57:03 +02:00
return nil
2018-08-16 14:59:18 +02:00
}
if len ( members . Joined ) < 2 {
2020-07-05 17:57:03 +02:00
mx . log . Debugln ( "Leaving empty room" , resp . RoomID , "after accepting invite from" , evt . Sender , "as" , intent . UserID )
2020-06-25 16:01:40 +02:00
_ , _ = intent . LeaveRoom ( resp . RoomID )
2020-07-05 17:57:03 +02:00
return nil
}
return members
}
func ( mx * MatrixHandler ) HandleBotInvite ( evt * event . Event ) {
intent := mx . as . BotIntent ( )
user := mx . bridge . GetUserByMXID ( evt . Sender )
if user == nil {
return
}
members := mx . joinAndCheckMembers ( evt , intent )
if members == nil {
2018-08-16 14:59:18 +02:00
return
}
2018-08-16 23:11:28 +02:00
2018-08-26 16:08:37 +02:00
if ! user . Whitelisted {
2020-07-05 17:57:03 +02:00
_ , _ = intent . SendNotice ( evt . RoomID , "You are not whitelisted to use this bridge.\n" +
2018-08-26 16:08:37 +02:00
"If you're the owner of this bridge, see the bridge.permissions section in your config file." )
2020-07-05 17:57:03 +02:00
_ , _ = intent . LeaveRoom ( evt . RoomID )
2018-08-26 16:08:37 +02:00
return
}
2019-11-10 20:22:11 +01:00
if evt . RoomID == mx . bridge . Config . Bridge . Relaybot . ManagementRoom {
2020-06-25 16:01:40 +02:00
_ , _ = intent . SendNotice ( evt . RoomID , "This is the relaybot management room. Send `!wa help` to get a list of commands." )
2019-11-10 20:22:11 +01:00
mx . log . Debugln ( "Joined relaybot management room" , evt . RoomID , "after invite from" , evt . Sender )
return
}
2018-08-16 23:11:28 +02:00
hasPuppets := false
2018-08-16 14:59:18 +02:00
for mxid , _ := range members . Joined {
2018-08-16 18:20:07 +02:00
if mxid == intent . UserID || mxid == evt . Sender {
2018-08-16 14:59:18 +02:00
continue
2020-05-08 21:32:22 +02:00
} else if _ , ok := mx . bridge . ParsePuppetMXID ( mxid ) ; ok {
2018-08-16 23:11:28 +02:00
hasPuppets = true
2018-08-16 14:59:18 +02:00
continue
}
2020-07-05 17:57:03 +02:00
mx . log . Debugln ( "Leaving multi-user room" , evt . RoomID , "after accepting invite from" , evt . Sender )
_ , _ = intent . SendNotice ( evt . RoomID , "This bridge is user-specific, please don't invite me into rooms with other users." )
_ , _ = intent . LeaveRoom ( evt . RoomID )
2018-08-16 14:59:18 +02:00
return
}
2020-07-10 14:23:32 +02:00
if ! hasPuppets && ( len ( user . ManagementRoom ) == 0 || evt . Content . AsMember ( ) . IsDirect ) {
2020-07-05 17:57:03 +02:00
user . SetManagementRoom ( evt . RoomID )
_ , _ = intent . SendNotice ( user . ManagementRoom , "This room has been registered as your bridge management/status room. Send `help` to get a list of commands." )
mx . log . Debugln ( evt . RoomID , "registered as a management room with" , evt . Sender )
}
}
2020-07-10 14:23:32 +02:00
func ( mx * MatrixHandler ) handlePrivatePortal ( roomID id . RoomID , inviter * User , puppet * Puppet , key database . PortalKey ) {
portal := mx . bridge . GetPortalByJID ( key )
if len ( portal . MXID ) == 0 {
mx . createPrivatePortalFromInvite ( roomID , inviter , puppet , portal )
return
}
2020-07-05 17:57:03 +02:00
err := portal . MainIntent ( ) . EnsureInvited ( portal . MXID , inviter . MXID )
if err != nil {
mx . log . Warnfln ( "Failed to invite %s to existing private chat portal %s with %s: %v. Redirecting portal to new room..." , inviter . MXID , portal . MXID , puppet . JID , err )
2020-07-10 14:23:32 +02:00
mx . createPrivatePortalFromInvite ( roomID , inviter , puppet , portal )
2020-07-05 17:57:03 +02:00
return
}
intent := puppet . DefaultIntent ( )
_ , _ = intent . SendNotice ( roomID , "You already have a private chat portal with me at %s" )
mx . log . Debugln ( "Leaving private chat room" , roomID , "as" , puppet . MXID , "after accepting invite from" , inviter . MXID , "as we already have chat with the user" )
_ , _ = intent . LeaveRoom ( roomID )
}
2020-07-10 14:23:32 +02:00
func ( mx * MatrixHandler ) createPrivatePortalFromInvite ( roomID id . RoomID , inviter * User , puppet * Puppet , portal * Portal ) {
2020-07-05 17:57:03 +02:00
portal . MXID = roomID
portal . Topic = "WhatsApp private chat"
_ , _ = portal . MainIntent ( ) . SetRoomTopic ( portal . MXID , portal . Topic )
if portal . bridge . Config . Bridge . PrivateChatPortalMeta {
portal . Name = puppet . Displayname
portal . AvatarURL = puppet . AvatarURL
portal . Avatar = puppet . Avatar
_ , _ = portal . MainIntent ( ) . SetRoomName ( portal . MXID , portal . Name )
_ , _ = portal . MainIntent ( ) . SetRoomAvatar ( portal . MXID , portal . AvatarURL )
} else {
portal . Name = ""
}
portal . log . Infoln ( "Created private chat portal in %s after invite from" , roomID , inviter . MXID )
intent := puppet . DefaultIntent ( )
if mx . bridge . Config . Bridge . Encryption . Default {
_ , err := intent . InviteUser ( roomID , & mautrix . ReqInviteUser { UserID : mx . bridge . Bot . UserID } )
if err != nil {
portal . log . Warnln ( "Failed to invite bridge bot to enable e2be:" , err )
}
err = mx . bridge . Bot . EnsureJoined ( roomID )
if err != nil {
portal . log . Warnln ( "Failed to join as bridge bot to enable e2be:" , err )
}
_ , err = intent . SendStateEvent ( roomID , event . StateEncryption , "" , & event . EncryptionEventContent { Algorithm : id . AlgorithmMegolmV1 } )
if err != nil {
portal . log . Warnln ( "Failed to enable e2be:" , err )
}
mx . as . StateStore . SetMembership ( roomID , inviter . MXID , event . MembershipJoin )
mx . as . StateStore . SetMembership ( roomID , puppet . MXID , event . MembershipJoin )
mx . as . StateStore . SetMembership ( roomID , mx . bridge . Bot . UserID , event . MembershipJoin )
portal . Encrypted = true
}
portal . Update ( )
portal . UpdateBridgeInfo ( )
_ , _ = intent . SendNotice ( roomID , "Private chat portal created" )
err := portal . FillInitialHistory ( inviter )
if err != nil {
portal . log . Errorln ( "Failed to fill history:" , err )
}
inviter . addPortalToCommunity ( portal )
inviter . addPuppetToCommunity ( puppet )
}
func ( mx * MatrixHandler ) HandlePuppetInvite ( evt * event . Event , inviter * User , puppet * Puppet ) {
intent := puppet . DefaultIntent ( )
members := mx . joinAndCheckMembers ( evt , intent )
if members == nil {
return
}
var hasBridgeBot , hasOtherUsers bool
for mxid , _ := range members . Joined {
if mxid == intent . UserID || mxid == inviter . MXID {
continue
} else if mxid == mx . bridge . Bot . UserID {
hasBridgeBot = true
} else {
hasOtherUsers = true
}
}
if ! hasBridgeBot && ! hasOtherUsers {
key := database . NewPortalKey ( puppet . JID , inviter . JID )
2020-07-10 14:23:32 +02:00
mx . handlePrivatePortal ( evt . RoomID , inviter , puppet , key )
2020-07-05 17:57:03 +02:00
} else if ! hasBridgeBot {
mx . log . Debugln ( "Leaving multi-user room" , evt . RoomID , "as" , puppet . MXID , "after accepting invite from" , evt . Sender )
_ , _ = intent . SendNotice ( evt . RoomID , "Please invite the bridge bot first if you want to bridge to a WhatsApp group." )
_ , _ = intent . LeaveRoom ( evt . RoomID )
} else {
_ , _ = intent . SendNotice ( evt . RoomID , "This puppet will remain inactive until this room is bridged to a WhatsApp group." )
2018-08-16 14:59:18 +02:00
}
}
2020-05-08 21:32:22 +02:00
func ( mx * MatrixHandler ) HandleMembership ( evt * event . Event ) {
2020-05-12 21:25:55 +02:00
if _ , isPuppet := mx . bridge . ParsePuppetMXID ( evt . Sender ) ; evt . Sender == mx . bridge . Bot . UserID || isPuppet {
return
}
2020-06-17 16:50:06 +02:00
defer mx . bridge . Metrics . TrackEvent ( evt . Type ) ( )
2020-05-12 21:25:55 +02:00
2020-05-09 01:03:59 +02:00
if mx . bridge . Crypto != nil {
mx . bridge . Crypto . HandleMemberEvent ( evt )
}
2020-05-08 21:32:22 +02:00
content := evt . Content . AsMember ( )
if content . Membership == event . MembershipInvite && id . UserID ( evt . GetStateKey ( ) ) == mx . as . BotMXID ( ) {
2018-08-18 21:57:08 +02:00
mx . HandleBotInvite ( evt )
2020-06-25 22:33:11 +02:00
return
2018-08-16 23:11:28 +02:00
}
2019-05-16 19:14:32 +02:00
2020-07-05 17:57:03 +02:00
user := mx . bridge . GetUserByMXID ( evt . Sender )
if user == nil || ! user . Whitelisted || ! user . IsConnected ( ) {
2019-05-16 19:14:32 +02:00
return
}
2020-07-05 17:57:03 +02:00
portal := mx . bridge . GetPortalByMXID ( evt . RoomID )
if portal == nil {
puppet := mx . bridge . GetPuppetByMXID ( id . UserID ( evt . GetStateKey ( ) ) )
if content . Membership == event . MembershipInvite && puppet != nil {
mx . HandlePuppetInvite ( evt , user , puppet )
}
2019-05-16 19:14:32 +02:00
return
}
2020-06-25 22:58:35 +02:00
isSelf := id . UserID ( evt . GetStateKey ( ) ) == evt . Sender
2020-05-08 21:32:22 +02:00
if content . Membership == event . MembershipLeave {
2020-06-25 22:58:35 +02:00
if isSelf {
2020-05-08 21:32:22 +02:00
if evt . Unsigned . PrevContent != nil {
_ = evt . Unsigned . PrevContent . ParseRaw ( evt . Type )
prevContent , ok := evt . Unsigned . PrevContent . Parsed . ( * event . MemberEventContent )
if ok {
if portal . IsPrivateChat ( ) || prevContent . Membership == "join" {
portal . HandleMatrixLeave ( user )
}
}
2019-05-16 19:14:32 +02:00
}
} else {
portal . HandleMatrixKick ( user , evt )
}
2020-06-25 22:58:35 +02:00
} else if content . Membership == event . MembershipInvite && ! isSelf {
portal . HandleMatrixInvite ( user , evt )
2019-05-16 19:14:32 +02:00
}
2018-08-16 14:59:18 +02:00
}
2020-05-08 21:32:22 +02:00
func ( mx * MatrixHandler ) HandleRoomMetadata ( evt * event . Event ) {
2020-06-17 16:50:06 +02:00
defer mx . bridge . Metrics . TrackEvent ( evt . Type ) ( )
2020-05-09 01:03:59 +02:00
user := mx . bridge . GetUserByMXID ( evt . Sender )
2019-11-10 20:22:11 +01:00
if user == nil || ! user . Whitelisted || ! user . IsConnected ( ) {
2018-08-26 16:02:32 +02:00
return
}
2018-08-28 23:40:54 +02:00
portal := mx . bridge . GetPortalByMXID ( evt . RoomID )
2018-08-26 16:02:32 +02:00
if portal == nil || portal . IsPrivateChat ( ) {
return
}
var resp <- chan string
var err error
2020-05-08 21:32:22 +02:00
switch content := evt . Content . Parsed . ( type ) {
case * event . RoomNameEventContent :
resp , err = user . Conn . UpdateGroupSubject ( content . Name , portal . Key . JID )
case * event . TopicEventContent :
resp , err = user . Conn . UpdateGroupDescription ( portal . Key . JID , content . Topic )
case * event . RoomAvatarEventContent :
2018-08-26 16:02:32 +02:00
return
}
if err != nil {
mx . log . Errorln ( err )
} else {
out := <- resp
mx . log . Infoln ( out )
}
}
2020-05-09 01:03:59 +02:00
func ( mx * MatrixHandler ) shouldIgnoreEvent ( evt * event . Event ) bool {
2018-09-01 22:38:03 +02:00
if _ , isPuppet := mx . bridge . ParsePuppetMXID ( evt . Sender ) ; evt . Sender == mx . bridge . Bot . UserID || isPuppet {
2020-05-09 01:03:59 +02:00
return true
2018-09-01 22:38:03 +02:00
}
2019-05-24 01:33:26 +02:00
isCustomPuppet , ok := evt . Content . Raw [ "net.maunium.whatsapp.puppet" ] . ( bool )
if ok && isCustomPuppet && mx . bridge . GetPuppetByCustomMXID ( evt . Sender ) != nil {
2020-05-09 01:03:59 +02:00
return true
}
user := mx . bridge . GetUserByMXID ( evt . Sender )
if ! user . RelaybotWhitelisted {
return true
}
return false
}
func ( mx * MatrixHandler ) HandleEncrypted ( evt * event . Event ) {
2020-06-17 16:50:06 +02:00
defer mx . bridge . Metrics . TrackEvent ( evt . Type ) ( )
2020-05-09 01:03:59 +02:00
if mx . shouldIgnoreEvent ( evt ) || mx . bridge . Crypto == nil {
2019-05-24 01:33:26 +02:00
return
}
2018-09-01 22:38:03 +02:00
2020-05-09 01:03:59 +02:00
decrypted , err := mx . bridge . Crypto . Decrypt ( evt )
if err != nil {
2020-05-09 19:23:30 +02:00
mx . log . Warnfln ( "Failed to decrypt %s: %v" , evt . ID , err )
2020-09-17 21:01:17 +02:00
_ , _ = mx . bridge . Bot . SendNotice ( evt . RoomID , fmt . Sprintf (
"\u26a0 Your message was not bridged: %v. " +
"Try restarting your client if this error keeps happening." , err ) )
2020-05-09 01:03:59 +02:00
return
}
mx . bridge . EventProcessor . Dispatch ( decrypted )
}
2018-08-18 21:57:08 +02:00
2020-05-09 01:03:59 +02:00
func ( mx * MatrixHandler ) HandleMessage ( evt * event . Event ) {
2020-06-17 16:50:06 +02:00
defer mx . bridge . Metrics . TrackEvent ( evt . Type ) ( )
2020-05-09 01:03:59 +02:00
if mx . shouldIgnoreEvent ( evt ) {
2018-08-26 16:08:37 +02:00
return
}
2020-05-09 01:03:59 +02:00
user := mx . bridge . GetUserByMXID ( evt . Sender )
2020-05-08 21:32:22 +02:00
content := evt . Content . AsMessage ( )
if user . Whitelisted && content . MsgType == event . MsgText {
2018-08-18 21:57:08 +02:00
commandPrefix := mx . bridge . Config . Bridge . CommandPrefix
2020-05-08 21:32:22 +02:00
hasCommandPrefix := strings . HasPrefix ( content . Body , commandPrefix )
2018-08-18 21:57:08 +02:00
if hasCommandPrefix {
2020-05-08 21:32:22 +02:00
content . Body = strings . TrimLeft ( content . Body [ len ( commandPrefix ) : ] , " " )
2018-08-18 21:57:08 +02:00
}
2020-05-08 21:32:22 +02:00
if hasCommandPrefix || evt . RoomID == user . ManagementRoom {
mx . cmd . Handle ( evt . RoomID , user , content . Body )
2018-08-18 21:57:08 +02:00
return
}
}
2020-05-08 21:32:22 +02:00
portal := mx . bridge . GetPortalByMXID ( evt . RoomID )
2019-11-10 20:22:11 +01:00
if portal != nil && ( user . Whitelisted || portal . HasRelaybot ( ) ) {
2018-08-28 23:40:54 +02:00
portal . HandleMatrixMessage ( user , evt )
2018-08-18 21:57:08 +02:00
}
2018-08-13 00:00:23 +02:00
}
2019-05-16 00:59:36 +02:00
2020-05-08 21:32:22 +02:00
func ( mx * MatrixHandler ) HandleRedaction ( evt * event . Event ) {
2020-06-17 16:50:06 +02:00
defer mx . bridge . Metrics . TrackEvent ( evt . Type ) ( )
2019-05-16 00:59:36 +02:00
if _ , isPuppet := mx . bridge . ParsePuppetMXID ( evt . Sender ) ; evt . Sender == mx . bridge . Bot . UserID || isPuppet {
return
}
2020-05-09 01:03:59 +02:00
user := mx . bridge . GetUserByMXID ( evt . Sender )
2019-05-16 00:59:36 +02:00
if ! user . Whitelisted {
return
}
2019-08-24 21:39:12 +02:00
if ! user . HasSession ( ) {
2019-05-16 00:59:36 +02:00
return
2019-08-24 21:39:12 +02:00
} else if ! user . IsConnected ( ) {
2019-11-10 20:22:11 +01:00
msg := format . RenderMarkdown ( fmt . Sprintf ( "[%[1]s](https://matrix.to/#/%[1]s): \u26a0 " +
"You are not connected to WhatsApp, so your redaction was not bridged. " +
2020-05-08 21:32:22 +02:00
"Use `%[2]s reconnect` to reconnect." , user . MXID , mx . bridge . Config . Bridge . CommandPrefix ) , true , false )
msg . MsgType = event . MsgNotice
_ , _ = mx . bridge . Bot . SendMessageEvent ( evt . RoomID , event . EventMessage , msg )
2019-05-16 00:59:36 +02:00
return
}
2020-05-08 21:32:22 +02:00
portal := mx . bridge . GetPortalByMXID ( evt . RoomID )
2019-05-16 00:59:36 +02:00
if portal != nil {
portal . HandleMatrixRedaction ( user , evt )
}
}