2018-08-18 21:57:08 +02:00
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
2021-10-22 19:14:34 +02:00
// Copyright (C) 2021 Tulir Asokan
2018-08-18 21:57:08 +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 (
2021-02-18 22:36:14 +01:00
"context"
2022-03-14 12:15:52 +01:00
"encoding/json"
2020-10-24 15:51:23 +02:00
"errors"
2019-02-20 13:39:44 +01:00
"fmt"
2021-11-27 10:30:41 +01:00
"html"
2021-10-31 22:22:14 +01:00
"math"
"sort"
2020-05-09 01:03:59 +02:00
"strconv"
2019-05-24 01:33:26 +02:00
"strings"
2022-05-04 10:17:34 +02:00
"time"
2019-05-24 01:33:26 +02:00
2021-10-22 19:14:34 +02:00
"github.com/skip2/go-qrcode"
2022-03-14 12:15:52 +01:00
"github.com/tidwall/gjson"
2021-11-15 12:38:10 +01:00
2019-01-11 20:17:31 +01:00
"maunium.net/go/maulogger/v2"
2019-11-10 20:22:11 +01:00
2021-10-27 14:54:34 +02:00
"go.mau.fi/whatsmeow"
2021-11-22 16:36:05 +01:00
"go.mau.fi/whatsmeow/appstate"
2021-10-27 14:54:34 +02:00
"go.mau.fi/whatsmeow/types"
2019-11-10 20:22:11 +01: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-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"
2022-05-02 14:00:57 +02:00
"maunium.net/go/mautrix-whatsapp/database"
2018-08-18 21:57:08 +02:00
)
type CommandHandler struct {
bridge * Bridge
log maulogger . Logger
}
2018-10-16 19:16:13 +02:00
// NewCommandHandler creates a CommandHandler
2018-08-18 21:57:08 +02:00
func NewCommandHandler ( bridge * Bridge ) * CommandHandler {
return & CommandHandler {
bridge : bridge ,
log : bridge . Log . Sub ( "Command handler" ) ,
}
}
2018-10-16 19:16:13 +02:00
// CommandEvent stores all data which might be used to handle commands
2018-08-18 21:57:08 +02:00
type CommandEvent struct {
Bot * appservice . IntentAPI
Bridge * Bridge
2020-06-25 21:42:52 +02:00
Portal * Portal
2018-08-18 21:57:08 +02:00
Handler * CommandHandler
2020-05-08 21:32:22 +02:00
RoomID id . RoomID
2022-01-07 13:39:00 +01:00
EventID id . EventID
2018-08-18 21:57:08 +02:00
User * User
2019-11-10 20:22:11 +01:00
Command string
2018-08-18 21:57:08 +02:00
Args [ ] string
2021-10-31 19:42:53 +01:00
ReplyTo id . EventID
2018-08-18 21:57:08 +02:00
}
2018-10-16 19:16:13 +02:00
// Reply sends a reply to command as notice
2019-05-15 22:04:09 +02:00
func ( ce * CommandEvent ) Reply ( msg string , args ... interface { } ) {
2020-05-08 21:32:22 +02:00
content := format . RenderMarkdown ( fmt . Sprintf ( msg , args ... ) , true , false )
content . MsgType = event . MsgNotice
2020-06-25 21:42:52 +02:00
intent := ce . Bot
if ce . Portal != nil && ce . Portal . IsPrivateChat ( ) {
intent = ce . Portal . MainIntent ( )
}
_ , err := intent . SendMessageEvent ( ce . RoomID , event . EventMessage , content )
2018-08-18 21:57:08 +02:00
if err != nil {
2018-08-28 23:40:54 +02:00
ce . Handler . log . Warnfln ( "Failed to reply to command from %s: %v" , ce . User . MXID , err )
2018-08-18 21:57:08 +02:00
}
}
2022-05-15 13:01:23 +02:00
func ( ce * CommandEvent ) React ( key string ) {
intent := ce . Bot
if ce . Portal != nil && ce . Portal . IsPrivateChat ( ) {
intent = ce . Portal . MainIntent ( )
}
_ , err := intent . SendReaction ( ce . RoomID , ce . EventID , key )
if err != nil {
ce . Handler . log . Warnfln ( "Failed to react to command from %s: %v" , ce . User . MXID , err )
}
}
2018-10-16 19:16:13 +02:00
// Handle handles messages to the bridge
2022-01-07 13:39:00 +01:00
func ( handler * CommandHandler ) Handle ( roomID id . RoomID , eventID id . EventID , user * User , message string , replyTo id . EventID ) {
2020-05-24 18:03:57 +02:00
args := strings . Fields ( message )
2020-12-27 23:21:17 +01:00
if len ( args ) == 0 {
args = [ ] string { "unknown-command" }
}
2018-08-18 21:57:08 +02:00
ce := & CommandEvent {
2018-08-30 00:10:26 +02:00
Bot : handler . bridge . Bot ,
2018-08-18 21:57:08 +02:00
Bridge : handler . bridge ,
2020-06-25 21:42:52 +02:00
Portal : handler . bridge . GetPortalByMXID ( roomID ) ,
2018-08-18 21:57:08 +02:00
Handler : handler ,
RoomID : roomID ,
2022-01-07 13:39:00 +01:00
EventID : eventID ,
2018-08-18 21:57:08 +02:00
User : user ,
2019-11-10 20:22:11 +01:00
Command : strings . ToLower ( args [ 0 ] ) ,
2018-08-18 21:57:08 +02:00
Args : args [ 1 : ] ,
2021-10-31 19:42:53 +01:00
ReplyTo : replyTo ,
2018-08-18 21:57:08 +02:00
}
2019-05-23 19:09:13 +02:00
handler . log . Debugfln ( "%s sent '%s' in %s" , user . MXID , message , roomID )
2021-10-28 12:57:15 +02:00
handler . CommandMux ( ce )
2019-11-10 20:22:11 +01:00
}
func ( handler * CommandHandler ) CommandMux ( ce * CommandEvent ) {
switch ce . Command {
2018-08-18 21:57:08 +02:00
case "login" :
handler . CommandLogin ( ce )
2021-10-30 22:44:41 +02:00
case "ping-matrix" :
handler . CommandPingMatrix ( ce )
2019-05-24 01:33:26 +02:00
case "logout-matrix" :
handler . CommandLogoutMatrix ( ce )
2018-08-18 21:57:08 +02:00
case "help" :
handler . CommandHelp ( ce )
2020-06-03 19:32:53 +02:00
case "version" :
handler . CommandVersion ( ce )
2020-06-15 18:00:29 +02:00
case "reconnect" , "connect" :
2019-05-15 23:18:43 +02:00
handler . CommandReconnect ( ce )
2019-05-23 19:09:13 +02:00
case "disconnect" :
handler . CommandDisconnect ( ce )
2019-08-24 21:39:12 +02:00
case "ping" :
handler . CommandPing ( ce )
2019-05-16 17:00:46 +02:00
case "delete-session" :
handler . CommandDeleteSession ( ce )
2019-05-21 20:06:27 +02:00
case "delete-portal" :
handler . CommandDeletePortal ( ce )
2019-05-31 18:33:18 +02:00
case "delete-all-portals" :
handler . CommandDeleteAllPortals ( ce )
2020-10-05 21:32:15 +02:00
case "discard-megolm-session" , "discard-session" :
handler . CommandDiscardMegolmSession ( ce )
2019-08-24 21:39:12 +02:00
case "dev-test" :
handler . CommandDevTest ( ce )
2020-05-09 01:03:59 +02:00
case "set-pl" :
handler . CommandSetPowerLevel ( ce )
2020-05-21 19:50:54 +02:00
case "logout" :
handler . CommandLogout ( ce )
2020-07-10 15:26:55 +02:00
case "toggle" :
handler . CommandToggle ( ce )
2022-05-15 13:01:23 +02:00
case "set-relay" , "unset-relay" , "login-matrix" , "sync" , "list" , "search" , "open" , "pm" , "invite-link" , "resolve" ,
"resolve-link" , "join" , "create" , "accept" , "backfill" , "disappearing-timer" :
2019-08-24 23:25:29 +02:00
if ! ce . User . HasSession ( ) {
2019-05-15 23:18:43 +02:00
ce . Reply ( "You are not logged in. Use the `login` command to log into WhatsApp." )
return
2021-10-22 19:14:34 +02:00
} else if ! ce . User . IsLoggedIn ( ) {
2019-05-15 23:18:43 +02:00
ce . Reply ( "You are not connected to WhatsApp. Use the `reconnect` command to reconnect." )
2019-03-15 15:45:27 +01:00
return
}
2019-11-10 20:22:11 +01:00
switch ce . Command {
2021-10-28 12:57:15 +02:00
case "set-relay" :
handler . CommandSetRelay ( ce )
case "unset-relay" :
handler . CommandUnsetRelay ( ce )
2019-05-24 01:33:26 +02:00
case "login-matrix" :
handler . CommandLoginMatrix ( ce )
2021-11-08 19:57:04 +01:00
case "sync" :
handler . CommandSync ( ce )
2019-03-15 15:45:27 +01:00
case "list" :
handler . CommandList ( ce )
2021-11-05 01:27:08 +01:00
case "search" :
handler . CommandSearch ( ce )
2019-03-15 15:45:27 +01:00
case "open" :
handler . CommandOpen ( ce )
case "pm" :
handler . CommandPM ( ce )
2020-06-25 21:40:34 +02:00
case "invite-link" :
handler . CommandInviteLink ( ce )
2021-11-27 10:30:41 +01:00
case "resolve" , "resolve-link" :
handler . CommandResolveLink ( ce )
2020-06-25 22:29:16 +02:00
case "join" :
handler . CommandJoin ( ce )
2020-07-10 14:23:32 +02:00
case "create" :
handler . CommandCreate ( ce )
2021-10-31 19:42:53 +01:00
case "accept" :
handler . CommandAccept ( ce )
2022-03-24 21:21:57 +01:00
case "backfill" :
handler . CommandBackfill ( ce )
2022-05-15 13:01:23 +02:00
case "disappearing-timer" :
handler . CommandDisappearingTimer ( ce )
2019-03-15 15:45:27 +01:00
}
2018-10-16 19:15:38 +02:00
default :
2020-12-27 23:21:17 +01:00
ce . Reply ( "Unknown command, use the `help` command for help." )
2018-08-18 21:57:08 +02:00
}
}
2020-10-05 21:32:15 +02:00
func ( handler * CommandHandler ) CommandDiscardMegolmSession ( ce * CommandEvent ) {
if handler . bridge . Crypto == nil {
ce . Reply ( "This bridge instance doesn't have end-to-bridge encryption enabled" )
} else if ! ce . User . Admin {
ce . Reply ( "Only the bridge admin can reset Megolm sessions" )
} else {
handler . bridge . Crypto . ResetSession ( ce . RoomID )
ce . Reply ( "Successfully reset Megolm session in this room. New decryption keys will be shared the next time a message is sent from WhatsApp." )
}
}
2018-08-18 21:57:08 +02:00
2021-10-28 12:57:15 +02:00
const cmdSetRelayHelp = ` set-relay - Relay messages in this room through your WhatsApp account. `
func ( handler * CommandHandler ) CommandSetRelay ( ce * CommandEvent ) {
if ! handler . bridge . Config . Bridge . Relay . Enabled {
ce . Reply ( "Relay mode is not enabled on this instance of the bridge" )
} else if ce . Portal == nil {
ce . Reply ( "This is not a portal room" )
} else if handler . bridge . Config . Bridge . Relay . AdminOnly && ! ce . User . Admin {
ce . Reply ( "Only admins are allowed to enable relay mode on this instance of the bridge" )
2019-11-10 20:22:11 +01:00
} else {
2021-10-28 12:57:15 +02:00
ce . Portal . RelayUserID = ce . User . MXID
ce . Portal . Update ( )
ce . Reply ( "Messages from non-logged-in users in this room will now be bridged through your WhatsApp account" )
}
}
2021-10-30 21:41:18 +02:00
const cmdUnsetRelayHelp = ` unset-relay - Stop relaying messages in this room. `
2021-10-28 12:57:15 +02:00
func ( handler * CommandHandler ) CommandUnsetRelay ( ce * CommandEvent ) {
if ! handler . bridge . Config . Bridge . Relay . Enabled {
ce . Reply ( "Relay mode is not enabled on this instance of the bridge" )
} else if ce . Portal == nil {
ce . Reply ( "This is not a portal room" )
} else if handler . bridge . Config . Bridge . Relay . AdminOnly && ! ce . User . Admin {
ce . Reply ( "Only admins are allowed to enable relay mode on this instance of the bridge" )
} else {
ce . Portal . RelayUserID = ""
ce . Portal . Update ( )
ce . Reply ( "Messages from non-logged-in users will no longer be bridged in this room" )
2019-11-10 20:22:11 +01:00
}
}
2020-05-23 22:25:22 +02:00
func ( handler * CommandHandler ) CommandDevTest ( _ * CommandEvent ) {
2019-08-24 21:39:12 +02:00
}
2020-06-03 19:32:53 +02:00
const cmdVersionHelp = ` version - View the bridge version `
func ( handler * CommandHandler ) CommandVersion ( ce * CommandEvent ) {
2021-04-29 13:00:26 +02:00
linkifiedVersion := fmt . Sprintf ( "v%s" , Version )
2020-06-03 19:32:53 +02:00
if Tag == Version {
2021-04-29 13:00:26 +02:00
linkifiedVersion = fmt . Sprintf ( "[v%s](%s/releases/v%s)" , Version , URL , Tag )
2020-06-03 19:32:53 +02:00
} else if len ( Commit ) > 8 {
2021-04-29 13:00:26 +02:00
linkifiedVersion = strings . Replace ( linkifiedVersion , Commit [ : 8 ] , fmt . Sprintf ( "[%s](%s/commit/%s)" , Commit [ : 8 ] , URL , Commit ) , 1 )
2020-06-03 19:32:53 +02:00
}
2021-04-29 13:00:26 +02:00
ce . Reply ( fmt . Sprintf ( "[%s](%s) %s (%s)" , Name , URL , linkifiedVersion , BuildTime ) )
2020-06-03 19:32:53 +02:00
}
2021-10-31 18:59:23 +01:00
const cmdInviteLinkHelp = ` invite-link [--reset] - Get an invite link to the current group chat, optionally regenerating the link and revoking the old link. `
2020-06-25 21:40:34 +02:00
func ( handler * CommandHandler ) CommandInviteLink ( ce * CommandEvent ) {
2021-10-31 18:59:23 +01:00
reset := len ( ce . Args ) > 0 && strings . ToLower ( ce . Args [ 0 ] ) == "--reset"
2020-06-25 21:42:52 +02:00
if ce . Portal == nil {
2020-06-25 21:40:34 +02:00
ce . Reply ( "Not a portal room" )
2020-06-25 21:42:52 +02:00
} else if ce . Portal . IsPrivateChat ( ) {
2020-06-25 21:40:34 +02:00
ce . Reply ( "Can't get invite link to private chat" )
2021-10-31 14:14:26 +01:00
} else if ce . Portal . IsBroadcastList ( ) {
ce . Reply ( "Can't get invite link to broadcast list" )
2021-10-31 18:59:23 +01:00
} else if link , err := ce . User . Client . GetGroupInviteLink ( ce . Portal . Key . JID , reset ) ; err != nil {
2021-10-31 14:14:26 +01:00
ce . Reply ( "Failed to get invite link: %v" , err )
} else {
ce . Reply ( link )
2020-06-25 21:40:34 +02:00
}
2020-06-25 22:29:16 +02:00
}
2021-11-27 10:30:41 +01:00
const cmdResolveLinkHelp = ` resolve-link <group or message link> - Resolve a WhatsApp group invite or business message link. `
2020-06-25 22:29:16 +02:00
2021-11-27 10:30:41 +01:00
func ( handler * CommandHandler ) CommandResolveLink ( ce * CommandEvent ) {
2021-10-31 18:59:23 +01:00
if len ( ce . Args ) == 0 {
2021-11-27 10:30:41 +01:00
ce . Reply ( "**Usage:** `resolve-link <group or message link>`" )
2021-10-31 18:59:23 +01:00
return
}
2021-11-27 10:30:41 +01:00
if strings . HasPrefix ( ce . Args [ 0 ] , whatsmeow . InviteLinkPrefix ) {
group , err := ce . User . Client . GetGroupInfoFromLink ( ce . Args [ 0 ] )
if err != nil {
ce . Reply ( "Failed to get group info: %v" , err )
return
}
ce . Reply ( "That invite link points at %s (`%s`)" , group . Name , group . JID )
} else if strings . HasPrefix ( ce . Args [ 0 ] , whatsmeow . BusinessMessageLinkPrefix ) || strings . HasPrefix ( ce . Args [ 0 ] , whatsmeow . BusinessMessageLinkDirectPrefix ) {
target , err := ce . User . Client . ResolveBusinessMessageLink ( ce . Args [ 0 ] )
if err != nil {
ce . Reply ( "Failed to get business info: %v" , err )
return
}
message := ""
if len ( target . Message ) > 0 {
parts := strings . Split ( target . Message , "\n" )
for i , part := range parts {
parts [ i ] = "> " + html . EscapeString ( part )
}
message = fmt . Sprintf ( " The following prefilled message is attached:\n\n%s" , strings . Join ( parts , "\n" ) )
}
ce . Reply ( "That link points at %s (+%s).%s" , target . PushName , target . JID . User , message )
} else {
ce . Reply ( "That doesn't look like a group invite link nor a business message link." )
2021-10-31 18:59:23 +01:00
}
}
const cmdJoinHelp = ` join <invite link> - Join a group chat with an invite link. `
2020-06-25 22:29:16 +02:00
func ( handler * CommandHandler ) CommandJoin ( ce * CommandEvent ) {
if len ( ce . Args ) == 0 {
ce . Reply ( "**Usage:** `join <invite link>`" )
return
2021-11-27 10:30:41 +01:00
} else if ! strings . HasPrefix ( ce . Args [ 0 ] , whatsmeow . InviteLinkPrefix ) {
2020-06-25 22:29:16 +02:00
ce . Reply ( "That doesn't look like a WhatsApp invite link" )
return
}
2021-11-01 10:44:25 +01:00
jid , err := ce . User . Client . JoinGroupWithLink ( ce . Args [ 0 ] )
2021-10-31 18:59:23 +01:00
if err != nil {
ce . Reply ( "Failed to join group: %v" , err )
return
}
handler . log . Debugln ( "%s successfully joined group %s" , ce . User . MXID , jid )
ce . Reply ( "Successfully joined group `%s`, the portal should be created momentarily" , jid )
2020-06-25 21:40:34 +02:00
}
2022-03-14 12:15:52 +01:00
func tryDecryptEvent ( crypto Crypto , evt * event . Event ) ( json . RawMessage , error ) {
var data json . RawMessage
if evt . Type != event . EventEncrypted {
data = evt . Content . VeryRaw
} else {
err := evt . Content . ParseRaw ( evt . Type )
if err != nil && ! errors . Is ( err , event . ErrContentAlreadyParsed ) {
return nil , err
}
decrypted , err := crypto . Decrypt ( evt )
if err != nil {
return nil , err
}
data = decrypted . Content . VeryRaw
}
return data , nil
}
func parseInviteMeta ( data json . RawMessage ) ( * InviteMeta , error ) {
result := gjson . GetBytes ( data , escapedInviteMetaField )
if ! result . Exists ( ) || ! result . IsObject ( ) {
return nil , nil
}
var meta InviteMeta
err := json . Unmarshal ( [ ] byte ( result . Raw ) , & meta )
if err != nil {
return nil , nil
}
return & meta , nil
}
2021-10-31 19:42:53 +01:00
func ( handler * CommandHandler ) CommandAccept ( ce * CommandEvent ) {
if ce . Portal == nil || len ( ce . ReplyTo ) == 0 {
ce . Reply ( "You must reply to a group invite message when using this command." )
2021-10-31 19:47:30 +01:00
} else if evt , err := ce . Portal . MainIntent ( ) . GetEvent ( ce . RoomID , ce . ReplyTo ) ; err != nil {
2021-10-31 19:42:53 +01:00
handler . log . Errorln ( "Failed to get event %s to handle !wa accept command: %v" , ce . ReplyTo , err )
ce . Reply ( "Failed to get reply event" )
2022-03-14 12:15:52 +01:00
} else if rawContent , err := tryDecryptEvent ( ce . Bridge . Crypto , evt ) ; err != nil {
handler . log . Errorln ( "Failed to decrypt event %s to handle !wa accept command: %v" , ce . ReplyTo , err )
ce . Reply ( "Failed to decrypt reply event" )
} else if meta , err := parseInviteMeta ( rawContent ) ; err != nil || meta == nil {
2021-10-31 19:42:53 +01:00
ce . Reply ( "That doesn't look like a group invite message." )
2022-03-14 12:15:52 +01:00
} else if meta . Inviter . User == ce . User . JID . User {
2021-10-31 19:47:30 +01:00
ce . Reply ( "You can't accept your own invites" )
2022-03-14 12:15:52 +01:00
} else if err = ce . User . Client . JoinGroupWithInvite ( meta . JID , meta . Inviter , meta . Code , meta . Expiration ) ; err != nil {
2021-10-31 19:42:53 +01:00
ce . Reply ( "Failed to accept group invite: %v" , err )
2021-10-31 19:47:30 +01:00
} else {
ce . Reply ( "Successfully accepted the invite, the portal should be created momentarily" )
2021-10-31 19:42:53 +01:00
}
}
2020-07-10 14:23:32 +02:00
const cmdCreateHelp = ` create - Create a group chat. `
func ( handler * CommandHandler ) CommandCreate ( ce * CommandEvent ) {
if ce . Portal != nil {
ce . Reply ( "This is already a portal room" )
return
}
members , err := ce . Bot . JoinedMembers ( ce . RoomID )
if err != nil {
ce . Reply ( "Failed to get room members: %v" , err )
return
}
var roomNameEvent event . RoomNameEventContent
err = ce . Bot . StateEvent ( ce . RoomID , event . StateRoomName , "" , & roomNameEvent )
2020-10-24 15:51:23 +02:00
if err != nil && ! errors . Is ( err , mautrix . MNotFound ) {
2021-11-05 11:17:56 +01:00
handler . log . Errorln ( "Failed to get room name to create group:" , err )
2020-07-10 14:23:32 +02:00
ce . Reply ( "Failed to get room name" )
return
} else if len ( roomNameEvent . Name ) == 0 {
ce . Reply ( "Please set a name for the room first" )
return
}
var encryptionEvent event . EncryptionEventContent
err = ce . Bot . StateEvent ( ce . RoomID , event . StateEncryption , "" , & encryptionEvent )
2020-10-24 15:51:23 +02:00
if err != nil && ! errors . Is ( err , mautrix . MNotFound ) {
2020-07-10 14:23:32 +02:00
ce . Reply ( "Failed to get room encryption status" )
return
}
2021-11-05 11:17:56 +01:00
var participants [ ] types . JID
participantDedup := make ( map [ types . JID ] bool )
participantDedup [ ce . User . JID . ToNonAD ( ) ] = true
participantDedup [ types . EmptyJID ] = true
2020-07-10 14:23:32 +02:00
for userID := range members . Joined {
jid , ok := handler . bridge . ParsePuppetMXID ( userID )
2021-11-05 11:17:56 +01:00
if ! ok {
user := handler . bridge . GetUserByMXID ( userID )
if user != nil && ! user . JID . IsEmpty ( ) {
jid = user . JID . ToNonAD ( )
}
}
if ! participantDedup [ jid ] {
participantDedup [ jid ] = true
2020-07-10 14:23:32 +02:00
participants = append ( participants , jid )
}
}
2021-11-05 11:17:56 +01:00
handler . log . Infofln ( "Creating group for %s with name %s and participants %+v" , ce . RoomID , roomNameEvent . Name , participants )
resp , err := ce . User . Client . CreateGroup ( roomNameEvent . Name , participants )
if err != nil {
ce . Reply ( "Failed to create group: %v" , err )
return
}
portal := ce . User . GetPortalByJID ( resp . JID )
portal . roomCreateLock . Lock ( )
defer portal . roomCreateLock . Unlock ( )
if len ( portal . MXID ) != 0 {
portal . log . Warnln ( "Detected race condition in room creation" )
// TODO race condition, clean up the old room
}
portal . MXID = ce . RoomID
portal . Name = roomNameEvent . Name
portal . Encrypted = encryptionEvent . Algorithm == id . AlgorithmMegolmV1
if ! portal . Encrypted && handler . bridge . Config . Bridge . Encryption . Default {
_ , err = portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateEncryption , "" , & event . EncryptionEventContent { Algorithm : id . AlgorithmMegolmV1 } )
if err != nil {
portal . log . Warnln ( "Failed to enable encryption in room:" , err )
if errors . Is ( err , mautrix . MForbidden ) {
ce . Reply ( "I don't seem to have permission to enable encryption in this room." )
} else {
ce . Reply ( "Failed to enable encryption in room: %v" , err )
}
}
portal . Encrypted = true
}
portal . Update ( )
portal . UpdateBridgeInfo ( )
ce . Reply ( "Successfully created WhatsApp group %s" , portal . Key . JID )
2020-07-10 14:23:32 +02:00
}
2020-05-23 22:25:22 +02:00
const cmdSetPowerLevelHelp = ` set-pl [user ID] <power level> - Change the power level in a portal room. Only for bridge admins. `
2020-05-09 01:03:59 +02:00
func ( handler * CommandHandler ) CommandSetPowerLevel ( ce * CommandEvent ) {
2021-12-16 15:32:12 +01:00
if ! ce . User . Admin {
ce . Reply ( "Only bridge admins can use `set-pl`" )
return
} else if ce . Portal == nil {
2022-01-07 13:39:00 +01:00
ce . Reply ( "This is not a portal room" )
2020-05-09 01:03:59 +02:00
return
}
var level int
var userID id . UserID
var err error
if len ( ce . Args ) == 1 {
level , err = strconv . Atoi ( ce . Args [ 0 ] )
if err != nil {
ce . Reply ( "Invalid power level \"%s\"" , ce . Args [ 0 ] )
return
}
userID = ce . User . MXID
} else if len ( ce . Args ) == 2 {
userID = id . UserID ( ce . Args [ 0 ] )
_ , _ , err := userID . Parse ( )
if err != nil {
ce . Reply ( "Invalid user ID \"%s\"" , ce . Args [ 0 ] )
return
}
level , err = strconv . Atoi ( ce . Args [ 1 ] )
if err != nil {
ce . Reply ( "Invalid power level \"%s\"" , ce . Args [ 1 ] )
return
}
} else {
ce . Reply ( "**Usage:** `set-pl [user] <level>`" )
return
}
2020-06-25 21:42:52 +02:00
intent := ce . Portal . MainIntent ( )
2020-05-09 01:03:59 +02:00
_ , err = intent . SetPowerLevel ( ce . RoomID , userID , level )
if err != nil {
ce . Reply ( "Failed to set power levels: %v" , err )
}
}
2021-10-27 14:54:34 +02:00
const cmdLoginHelp = ` login - Link the bridge to your WhatsApp account as a web client `
2018-10-16 18:35:39 +02:00
2018-10-16 19:16:13 +02:00
// CommandLogin handles login command
2018-08-18 21:57:08 +02:00
func ( handler * CommandHandler ) CommandLogin ( ce * CommandEvent ) {
2021-10-22 19:14:34 +02:00
if ce . User . Session != nil {
2021-10-27 14:54:34 +02:00
if ce . User . IsConnected ( ) {
ce . Reply ( "You're already logged in" )
} else {
ce . Reply ( "You're already logged in. Perhaps you wanted to `reconnect`?" )
}
2021-10-22 19:14:34 +02:00
return
}
2021-10-27 14:54:34 +02:00
qrChan , err := ce . User . Login ( context . Background ( ) )
if err != nil {
ce . User . log . Errorf ( "Failed to log in:" , err )
ce . Reply ( "Failed to log in: %v" , err )
2019-08-30 19:57:08 +02:00
return
2018-08-18 21:57:08 +02:00
}
2021-10-22 19:14:34 +02:00
2021-10-27 14:54:34 +02:00
var qrEventID id . EventID
for item := range qrChan {
2021-12-09 14:54:01 +01:00
switch item . Event {
2021-12-09 14:57:26 +01:00
case whatsmeow . QRChannelSuccess . Event :
2021-10-27 14:54:34 +02:00
jid := ce . User . Client . Store . ID
ce . Reply ( "Successfully logged in as +%s (device #%d)" , jid . User , jid . Device )
2021-12-09 14:57:26 +01:00
case whatsmeow . QRChannelTimeout . Event :
2021-10-27 14:54:34 +02:00
ce . Reply ( "QR code timed out. Please restart the login." )
2021-12-09 14:57:26 +01:00
case whatsmeow . QRChannelErrUnexpectedEvent . Event :
2021-10-27 14:54:34 +02:00
ce . Reply ( "Failed to log in: unexpected connection event from server" )
2022-02-17 14:33:31 +01:00
case whatsmeow . QRChannelClientOutdated . Event :
ce . Reply ( "Failed to log in: outdated client. The bridge must be updated to continue." )
2021-12-09 14:57:26 +01:00
case whatsmeow . QRChannelScannedWithoutMultidevice . Event :
2021-10-28 20:22:34 +02:00
ce . Reply ( "Please enable the WhatsApp multidevice beta and scan the QR code again." )
2021-12-09 11:27:31 +01:00
case "error" :
ce . Reply ( "Failed to log in: %v" , item . Error )
case "code" :
2021-11-15 12:38:10 +01:00
qrEventID = ce . User . sendQR ( ce , item . Code , qrEventID )
2021-10-27 14:54:34 +02:00
}
2021-10-22 19:14:34 +02:00
}
2021-10-27 14:54:34 +02:00
_ , _ = ce . Bot . RedactEvent ( ce . RoomID , qrEventID )
2021-10-22 19:14:34 +02:00
}
2021-10-27 14:54:34 +02:00
func ( user * User ) sendQR ( ce * CommandEvent , code string , prevEvent id . EventID ) id . EventID {
2021-10-22 19:14:34 +02:00
url , ok := user . uploadQR ( ce , code )
if ! ok {
2021-10-27 14:54:34 +02:00
return prevEvent
2021-10-22 19:14:34 +02:00
}
2021-10-27 14:54:34 +02:00
content := event . MessageEventContent {
MsgType : event . MsgImage ,
Body : code ,
URL : url . CUString ( ) ,
2021-10-22 19:14:34 +02:00
}
2021-10-27 14:54:34 +02:00
if len ( prevEvent ) != 0 {
content . SetEdit ( prevEvent )
}
resp , err := ce . Bot . SendMessageEvent ( ce . RoomID , event . EventMessage , & content )
if err != nil {
user . log . Errorln ( "Failed to send edited QR code to user:" , err )
} else if len ( prevEvent ) == 0 {
prevEvent = resp . EventID
2021-10-22 19:14:34 +02:00
}
2021-10-27 14:54:34 +02:00
return prevEvent
2021-10-22 19:14:34 +02:00
}
func ( user * User ) uploadQR ( ce * CommandEvent , code string ) ( id . ContentURI , bool ) {
qrCode , err := qrcode . Encode ( code , qrcode . Low , 256 )
if err != nil {
user . log . Errorln ( "Failed to encode QR code:" , err )
ce . Reply ( "Failed to encode QR code: %v" , err )
return id . ContentURI { } , false
}
bot := user . bridge . AS . BotClient ( )
resp , err := bot . UploadBytes ( qrCode , "image/png" )
if err != nil {
user . log . Errorln ( "Failed to upload QR code:" , err )
ce . Reply ( "Failed to upload QR code: %v" , err )
return id . ContentURI { } , false
}
return resp . ContentURI , true
2018-08-18 21:57:08 +02:00
}
2021-10-27 14:54:34 +02:00
const cmdLogoutHelp = ` logout - Unlink the bridge from your WhatsApp account `
2018-10-16 18:35:39 +02:00
2018-10-16 19:16:13 +02:00
// CommandLogout handles !logout command
2018-08-18 21:57:08 +02:00
func ( handler * CommandHandler ) CommandLogout ( ce * CommandEvent ) {
if ce . User . Session == nil {
ce . Reply ( "You're not logged in." )
return
2021-10-22 19:14:34 +02:00
} else if ! ce . User . IsLoggedIn ( ) {
2020-05-21 19:50:54 +02:00
ce . Reply ( "You are not connected to WhatsApp. Use the `reconnect` command to reconnect, or `delete-session` to forget all login information." )
return
2018-08-18 21:57:08 +02:00
}
2020-05-21 18:49:01 +02:00
puppet := handler . bridge . GetPuppetByJID ( ce . User . JID )
if puppet . CustomMXID != "" {
err := puppet . SwitchCustomMXID ( "" , "" )
if err != nil {
ce . User . log . Warnln ( "Failed to logout-matrix while logging out of WhatsApp:" , err )
}
}
2021-10-27 14:54:34 +02:00
err := ce . User . Client . Logout ( )
if err != nil {
ce . User . log . Warnln ( "Error while logging out:" , err )
ce . Reply ( "Unknown error while logging out: %v" , err )
return
}
2021-10-29 20:38:30 +02:00
ce . User . Session = nil
2022-03-10 21:20:10 +01:00
ce . User . removeFromJIDMap ( BridgeState { StateEvent : StateLoggedOut } )
2021-02-18 22:36:14 +01:00
ce . User . DeleteConnection ( )
2021-10-22 19:14:34 +02:00
ce . User . DeleteSession ( )
2018-08-18 21:57:08 +02:00
ce . Reply ( "Logged out successfully." )
}
2021-04-19 18:25:40 +02:00
const cmdToggleHelp = ` toggle <presence|receipts|all> - Toggle bridging of presence or read receipts `
2020-07-10 13:53:18 +02:00
2020-07-10 15:26:55 +02:00
func ( handler * CommandHandler ) CommandToggle ( ce * CommandEvent ) {
2021-03-02 18:06:16 +01:00
if len ( ce . Args ) == 0 || ( ce . Args [ 0 ] != "presence" && ce . Args [ 0 ] != "receipts" && ce . Args [ 0 ] != "all" ) {
ce . Reply ( "**Usage:** `toggle <presence|receipts|all>`" )
2020-07-10 15:26:55 +02:00
return
}
2020-07-10 13:53:18 +02:00
if ce . User . Session == nil {
ce . Reply ( "You're not logged in." )
return
}
customPuppet := handler . bridge . GetPuppetByCustomMXID ( ce . User . MXID )
if customPuppet == nil {
ce . Reply ( "You're not logged in with your Matrix account." )
return
}
2021-03-02 18:06:16 +01:00
if ce . Args [ 0 ] == "presence" || ce . Args [ 0 ] == "all" {
2021-10-28 13:35:09 +02:00
customPuppet . EnablePresence = ! customPuppet . EnablePresence
var newPresence types . Presence
if customPuppet . EnablePresence {
newPresence = types . PresenceAvailable
ce . Reply ( "Enabled presence bridging" )
} else {
newPresence = types . PresenceUnavailable
ce . Reply ( "Disabled presence bridging" )
}
if ce . User . IsLoggedIn ( ) {
err := ce . User . Client . SendPresence ( newPresence )
if err != nil {
ce . User . log . Warnln ( "Failed to set presence:" , err )
}
}
2021-04-19 18:25:40 +02:00
}
if ce . Args [ 0 ] == "receipts" || ce . Args [ 0 ] == "all" {
2020-07-10 15:26:55 +02:00
customPuppet . EnableReceipts = ! customPuppet . EnableReceipts
if customPuppet . EnableReceipts {
ce . Reply ( "Enabled read receipt bridging" )
} else {
ce . Reply ( "Disabled read receipt bridging" )
2020-07-10 13:53:18 +02:00
}
}
2020-07-10 15:26:55 +02:00
customPuppet . Update ( )
2020-07-10 13:53:18 +02:00
}
2019-05-16 17:00:46 +02:00
const cmdDeleteSessionHelp = ` delete-session - Delete session information and disconnect from WhatsApp without sending a logout request `
func ( handler * CommandHandler ) CommandDeleteSession ( ce * CommandEvent ) {
2021-10-22 19:14:34 +02:00
if ce . User . Session == nil && ce . User . Client == nil {
2019-05-16 17:00:46 +02:00
ce . Reply ( "Nothing to purge: no session information stored and no active connection." )
return
}
2022-03-10 21:20:10 +01:00
ce . User . removeFromJIDMap ( BridgeState { StateEvent : StateLoggedOut } )
2021-02-18 22:36:14 +01:00
ce . User . DeleteConnection ( )
2021-10-22 19:14:34 +02:00
ce . User . DeleteSession ( )
2019-05-16 17:00:46 +02:00
ce . Reply ( "Session information purged" )
}
2019-05-15 22:04:09 +02:00
const cmdReconnectHelp = ` reconnect - Reconnect to WhatsApp `
func ( handler * CommandHandler ) CommandReconnect ( ce * CommandEvent ) {
2021-10-31 13:11:15 +01:00
if ce . User . Client == nil {
if ce . User . Session == nil {
ce . Reply ( "You're not logged into WhatsApp. Please log in first." )
} else {
ce . User . Connect ( )
ce . Reply ( "Started connecting to WhatsApp" )
}
} else {
ce . User . DeleteConnection ( )
ce . User . sendBridgeState ( BridgeState { StateEvent : StateTransientDisconnect , Error : WANotConnected } )
ce . User . Connect ( )
ce . Reply ( "Restarted connection to WhatsApp" )
}
2019-05-23 19:09:13 +02:00
}
2019-05-15 22:04:09 +02:00
const cmdDisconnectHelp = ` disconnect - Disconnect from WhatsApp (without logging out) `
func ( handler * CommandHandler ) CommandDisconnect ( ce * CommandEvent ) {
2021-10-22 19:14:34 +02:00
if ce . User . Client == nil {
2019-05-23 19:09:13 +02:00
ce . Reply ( "You don't have a WhatsApp connection." )
return
}
2021-10-22 19:14:34 +02:00
ce . User . DeleteConnection ( )
2019-05-15 22:04:09 +02:00
ce . Reply ( "Successfully disconnected. Use the `reconnect` command to reconnect." )
2021-10-29 21:03:00 +02:00
ce . User . sendBridgeState ( BridgeState { StateEvent : StateBadCredentials , Error : WANotConnected } )
2019-05-15 22:04:09 +02:00
}
2019-08-24 21:39:12 +02:00
const cmdPingHelp = ` ping - Check your connection to WhatsApp. `
func ( handler * CommandHandler ) CommandPing ( ce * CommandEvent ) {
2019-08-30 20:32:47 +02:00
if ce . User . Session == nil {
2021-11-01 14:32:08 +01:00
if ce . User . Client != nil {
ce . Reply ( "Connected to WhatsApp, but not logged in." )
} else {
ce . Reply ( "You're not logged into WhatsApp." )
}
2021-10-22 19:14:34 +02:00
} else if ce . User . Client == nil || ! ce . User . Client . IsConnected ( ) {
2021-11-01 14:32:08 +01:00
ce . Reply ( "You're logged in as +%s (device #%d), but you don't have a WhatsApp connection." , ce . User . JID . User , ce . User . JID . Device )
2020-06-25 21:42:52 +02:00
} else {
2021-11-01 14:32:08 +01:00
ce . Reply ( "Logged in as +%s (device #%d), connection to WhatsApp OK (probably)" , ce . User . JID . User , ce . User . JID . Device )
2022-05-04 10:17:34 +02:00
if ! ce . User . PhoneRecentlySeen ( false ) {
ce . Reply ( "Phone hasn't been seen in %s" , formatDisconnectTime ( time . Now ( ) . Sub ( ce . User . PhoneLastSeen ) ) )
}
2019-08-24 21:39:12 +02:00
}
}
2018-10-16 19:15:38 +02:00
const cmdHelpHelp = ` help - Prints this help `
2018-10-16 19:16:13 +02:00
// CommandHelp handles help command
2018-08-18 21:57:08 +02:00
func ( handler * CommandHandler ) CommandHelp ( ce * CommandEvent ) {
2018-12-07 23:31:15 +01:00
cmdPrefix := ""
2021-10-28 12:57:15 +02:00
if ce . User . ManagementRoom != ce . RoomID {
2018-12-07 23:31:15 +01:00
cmdPrefix = handler . bridge . Config . Bridge . CommandPrefix + " "
}
2019-02-20 13:39:44 +01:00
ce . Reply ( "* " + strings . Join ( [ ] string {
2018-10-16 19:15:38 +02:00
cmdPrefix + cmdHelpHelp ,
2021-04-29 13:00:26 +02:00
cmdPrefix + cmdVersionHelp ,
2018-10-16 19:15:38 +02:00
cmdPrefix + cmdLoginHelp ,
cmdPrefix + cmdLogoutHelp ,
2019-05-16 17:00:46 +02:00
cmdPrefix + cmdDeleteSessionHelp ,
2019-05-15 22:04:09 +02:00
cmdPrefix + cmdReconnectHelp ,
cmdPrefix + cmdDisconnectHelp ,
2019-08-24 21:39:12 +02:00
cmdPrefix + cmdPingHelp ,
2021-10-28 12:57:15 +02:00
cmdPrefix + cmdSetRelayHelp ,
cmdPrefix + cmdUnsetRelayHelp ,
2019-08-24 21:39:12 +02:00
cmdPrefix + cmdLoginMatrixHelp ,
2021-10-30 22:44:41 +02:00
cmdPrefix + cmdPingMatrixHelp ,
2019-08-24 21:39:12 +02:00
cmdPrefix + cmdLogoutMatrixHelp ,
2020-07-10 15:26:55 +02:00
cmdPrefix + cmdToggleHelp ,
2019-02-20 13:39:44 +01:00
cmdPrefix + cmdListHelp ,
2021-11-05 01:27:08 +01:00
cmdPrefix + cmdSearchHelp ,
2021-11-08 19:57:04 +01:00
cmdPrefix + cmdSyncHelp ,
2019-02-20 13:39:44 +01:00
cmdPrefix + cmdOpenHelp ,
cmdPrefix + cmdPMHelp ,
2020-06-25 21:40:34 +02:00
cmdPrefix + cmdInviteLinkHelp ,
2021-11-27 10:30:41 +01:00
cmdPrefix + cmdResolveLinkHelp ,
2020-06-25 22:29:16 +02:00
cmdPrefix + cmdJoinHelp ,
2020-07-10 14:23:32 +02:00
cmdPrefix + cmdCreateHelp ,
2022-05-15 13:01:23 +02:00
cmdPrefix + cmdDisappearingTimerHelp ,
2020-05-23 22:25:22 +02:00
cmdPrefix + cmdSetPowerLevelHelp ,
cmdPrefix + cmdDeletePortalHelp ,
cmdPrefix + cmdDeleteAllPortalsHelp ,
2022-03-24 21:21:57 +01:00
cmdPrefix + cmdBackfillHelp ,
2019-02-20 13:39:44 +01:00
} , "\n* " ) )
}
2021-10-28 13:27:59 +02:00
func canDeletePortal ( portal * Portal , userID id . UserID ) bool {
members , err := portal . MainIntent ( ) . JoinedMembers ( portal . MXID )
if err != nil {
portal . log . Errorfln ( "Failed to get joined members to check if portal can be deleted by %s: %v" , userID , err )
return false
}
for otherUser := range members . Joined {
_ , isPuppet := portal . bridge . ParsePuppetMXID ( otherUser )
if isPuppet || otherUser == portal . bridge . Bot . UserID || otherUser == userID {
continue
}
user := portal . bridge . GetUserByMXID ( otherUser )
if user != nil && user . Session != nil {
return false
}
}
return true
}
2020-05-23 22:25:22 +02:00
const cmdDeletePortalHelp = ` delete-portal - Delete the current portal. If the portal is used by other people, this is limited to bridge admins. `
2019-05-21 20:06:27 +02:00
2020-05-23 22:25:22 +02:00
func ( handler * CommandHandler ) CommandDeletePortal ( ce * CommandEvent ) {
2020-06-25 21:42:52 +02:00
if ce . Portal == nil {
2019-05-21 20:06:27 +02:00
ce . Reply ( "You must be in a portal room to use that command" )
return
}
2021-10-28 13:27:59 +02:00
if ! ce . User . Admin && ! canDeletePortal ( ce . Portal , ce . User . MXID ) {
ce . Reply ( "Only bridge admins can delete portals with other Matrix users" )
2021-10-22 19:14:34 +02:00
return
2020-05-23 22:25:22 +02:00
}
2020-06-25 21:42:52 +02:00
ce . Portal . log . Infoln ( ce . User . MXID , "requested deletion of portal." )
ce . Portal . Delete ( )
ce . Portal . Cleanup ( false )
2019-05-21 20:06:27 +02:00
}
2021-10-28 13:27:59 +02:00
const cmdDeleteAllPortalsHelp = ` delete-all-portals - Delete all portals. `
2020-05-23 22:25:22 +02:00
2019-05-31 18:33:18 +02:00
func ( handler * CommandHandler ) CommandDeleteAllPortals ( ce * CommandEvent ) {
2021-10-28 13:27:59 +02:00
portals := handler . bridge . GetAllPortals ( )
var portalsToDelete [ ] * Portal
if ce . User . Admin {
2021-10-30 20:54:35 +02:00
portalsToDelete = portals
2021-10-28 13:27:59 +02:00
} else {
portalsToDelete = portals [ : 0 ]
for _ , portal := range portals {
if canDeletePortal ( portal , ce . User . MXID ) {
portalsToDelete = append ( portalsToDelete , portal )
}
}
2021-10-26 21:13:31 +02:00
}
2019-05-31 18:33:18 +02:00
leave := func ( portal * Portal ) {
if len ( portal . MXID ) > 0 {
_ , _ = portal . MainIntent ( ) . KickUser ( portal . MXID , & mautrix . ReqKickUser {
Reason : "Deleting portal" ,
UserID : ce . User . MXID ,
} )
}
}
customPuppet := handler . bridge . GetPuppetByCustomMXID ( ce . User . MXID )
2019-05-31 19:51:16 +02:00
if customPuppet != nil && customPuppet . CustomIntent ( ) != nil {
2019-05-31 18:33:18 +02:00
intent := customPuppet . CustomIntent ( )
leave = func ( portal * Portal ) {
if len ( portal . MXID ) > 0 {
_ , _ = intent . LeaveRoom ( portal . MXID )
_ , _ = intent . ForgetRoom ( portal . MXID )
}
}
}
2021-10-28 13:27:59 +02:00
ce . Reply ( "Found %d portals, deleting..." , len ( portalsToDelete ) )
2019-05-31 18:33:18 +02:00
for _ , portal := range portalsToDelete {
portal . Delete ( )
leave ( portal )
}
2021-10-28 13:27:59 +02:00
ce . Reply ( "Finished deleting portal info. Now cleaning up rooms in background." )
2019-05-31 18:33:18 +02:00
go func ( ) {
for _ , portal := range portalsToDelete {
portal . Cleanup ( false )
}
ce . Reply ( "Finished background cleanup of deleted portal rooms." )
} ( )
}
2022-03-24 21:21:57 +01:00
const cmdBackfillHelp = ` backfill [batch size] [batch delay] - Backfill all messages the portal. `
func ( handler * CommandHandler ) CommandBackfill ( ce * CommandEvent ) {
if ce . Portal == nil {
ce . Reply ( "This is not a portal room" )
return
}
if ! ce . Bridge . Config . Bridge . HistorySync . Backfill {
2022-04-06 16:45:45 +02:00
ce . Reply ( "Backfill is not enabled for this bridge." )
2022-03-24 21:21:57 +01:00
return
}
batchSize := 100
batchDelay := 5
if len ( ce . Args ) >= 1 {
var err error
batchSize , err = strconv . Atoi ( ce . Args [ 0 ] )
if err != nil || batchSize < 1 {
ce . Reply ( "\"%s\" isn't a valid batch size" , ce . Args [ 0 ] )
return
}
}
if len ( ce . Args ) >= 2 {
var err error
batchDelay , err = strconv . Atoi ( ce . Args [ 0 ] )
if err != nil || batchSize < 0 {
ce . Reply ( "\"%s\" isn't a valid batch delay" , ce . Args [ 1 ] )
return
}
}
2022-05-10 22:28:30 +02:00
backfillMessages := ce . Portal . bridge . DB . Backfill . NewWithValues ( ce . User . MXID , database . BackfillImmediate , 0 , & ce . Portal . Key , nil , nil , batchSize , - 1 , batchDelay )
2022-04-19 04:50:21 +02:00
backfillMessages . Insert ( )
2022-03-24 21:21:57 +01:00
ce . User . BackfillQueue . ReCheckQueue <- true
}
2020-05-23 22:25:22 +02:00
const cmdListHelp = ` list <contacts|groups> [page] [items per page] - Get a list of all contacts and groups. `
2019-02-20 13:39:44 +01:00
2021-11-05 01:27:08 +01:00
func matchesQuery ( str string , query string ) bool {
if query == "" {
return true
}
return strings . Contains ( strings . ToLower ( str ) , query )
}
func formatContacts ( bridge * Bridge , input map [ types . JID ] types . ContactInfo , query string ) ( result [ ] string ) {
hasQuery := len ( query ) > 0
2021-10-31 22:22:14 +01:00
for jid , contact := range input {
if len ( contact . FullName ) == 0 {
continue
}
puppet := bridge . GetPuppetByJID ( jid )
pushName := contact . PushName
if len ( pushName ) == 0 {
pushName = contact . FullName
}
2021-11-05 01:27:08 +01:00
if ! hasQuery || matchesQuery ( pushName , query ) || matchesQuery ( contact . FullName , query ) || matchesQuery ( jid . User , query ) {
result = append ( result , fmt . Sprintf ( "* %s / [%s](https://matrix.to/#/%s) - `+%s`" , contact . FullName , pushName , puppet . MXID , jid . User ) )
}
2021-10-31 22:22:14 +01:00
}
sort . Sort ( sort . StringSlice ( result ) )
return
}
2021-11-05 01:27:08 +01:00
func formatGroups ( input [ ] * types . GroupInfo , query string ) ( result [ ] string ) {
hasQuery := len ( query ) > 0
2021-10-31 22:22:14 +01:00
for _ , group := range input {
2021-11-05 01:27:08 +01:00
if ! hasQuery || matchesQuery ( group . GroupName . Name , query ) || matchesQuery ( group . JID . User , query ) {
result = append ( result , fmt . Sprintf ( "* %s - `%s`" , group . GroupName . Name , group . JID . User ) )
}
2021-10-31 22:22:14 +01:00
}
sort . Sort ( sort . StringSlice ( result ) )
return
}
2020-05-23 22:17:43 +02:00
func ( handler * CommandHandler ) CommandList ( ce * CommandEvent ) {
if len ( ce . Args ) == 0 {
ce . Reply ( "**Usage:** `list <contacts|groups> [page] [items per page]`" )
return
}
mode := strings . ToLower ( ce . Args [ 0 ] )
if mode [ 0 ] != 'g' && mode [ 0 ] != 'c' {
ce . Reply ( "**Usage:** `list <contacts|groups> [page] [items per page]`" )
2020-05-23 23:11:56 +02:00
return
2020-05-23 22:17:43 +02:00
}
var err error
2020-05-23 22:52:05 +02:00
page := 1
2020-05-23 22:17:43 +02:00
max := 100
if len ( ce . Args ) > 1 {
page , err = strconv . Atoi ( ce . Args [ 1 ] )
if err != nil || page <= 0 {
ce . Reply ( "\"%s\" isn't a valid page number" , ce . Args [ 1 ] )
return
}
}
if len ( ce . Args ) > 2 {
max , err = strconv . Atoi ( ce . Args [ 2 ] )
if err != nil || max <= 0 {
ce . Reply ( "\"%s\" isn't a valid number of items per page" , ce . Args [ 2 ] )
return
} else if max > 400 {
ce . Reply ( "Warning: a high number of items per page may fail to send a reply" )
}
}
2021-10-31 22:22:14 +01:00
contacts := mode [ 0 ] == 'c'
typeName := "Groups"
var result [ ] string
if contacts {
typeName = "Contacts"
contactList , err := ce . User . Client . Store . Contacts . GetAllContacts ( )
if err != nil {
ce . Reply ( "Failed to get contacts: %s" , err )
return
}
2021-11-05 01:27:08 +01:00
result = formatContacts ( ce . User . bridge , contactList , "" )
2021-10-31 22:22:14 +01:00
} else {
groupList , err := ce . User . Client . GetJoinedGroups ( )
if err != nil {
ce . Reply ( "Failed to get groups: %s" , err )
return
}
2021-11-05 01:27:08 +01:00
result = formatGroups ( groupList , "" )
2021-10-31 22:22:14 +01:00
}
if len ( result ) == 0 {
ce . Reply ( "No %s found" , strings . ToLower ( typeName ) )
return
}
pages := int ( math . Ceil ( float64 ( len ( result ) ) / float64 ( max ) ) )
if ( page - 1 ) * max >= len ( result ) {
if pages == 1 {
ce . Reply ( "There is only 1 page of %s" , strings . ToLower ( typeName ) )
} else {
2021-11-01 10:28:52 +01:00
ce . Reply ( "There are %d pages of %s" , pages , strings . ToLower ( typeName ) )
2021-10-31 22:22:14 +01:00
}
return
}
lastIndex := page * max
if lastIndex > len ( result ) {
lastIndex = len ( result )
}
result = result [ ( page - 1 ) * max : lastIndex ]
ce . Reply ( "### %s (page %d of %d)\n\n%s" , typeName , page , pages , strings . Join ( result , "\n" ) )
2019-02-20 13:39:44 +01:00
}
2018-12-07 14:42:57 +01:00
2021-11-22 14:36:23 +01:00
const cmdSearchHelp = ` search <query> - Search for contacts or groups. `
2021-11-05 01:27:08 +01:00
func ( handler * CommandHandler ) CommandSearch ( ce * CommandEvent ) {
if len ( ce . Args ) == 0 {
2021-11-22 14:36:23 +01:00
ce . Reply ( "**Usage:** `search <query>`" )
2021-11-05 01:27:08 +01:00
return
}
contactList , err := ce . User . Client . Store . Contacts . GetAllContacts ( )
if err != nil {
ce . Reply ( "Failed to get contacts: %s" , err )
return
}
groupList , err := ce . User . Client . GetJoinedGroups ( )
if err != nil {
ce . Reply ( "Failed to get groups: %s" , err )
return
}
2021-11-22 14:36:23 +01:00
query := strings . ToLower ( strings . TrimSpace ( strings . Join ( ce . Args , " " ) ) )
formattedContacts := strings . Join ( formatContacts ( ce . User . bridge , contactList , query ) , "\n" )
formattedGroups := strings . Join ( formatGroups ( groupList , query ) , "\n" )
result := make ( [ ] string , 0 , 2 )
if len ( formattedContacts ) > 0 {
2021-11-30 14:27:15 +01:00
result = append ( result , "### Contacts\n\n" + formattedContacts )
2021-11-05 01:27:08 +01:00
}
2021-11-22 14:36:23 +01:00
if len ( formattedGroups ) > 0 {
2021-11-30 14:27:15 +01:00
result = append ( result , "### Groups\n\n" + formattedGroups )
2021-11-05 01:27:08 +01:00
}
if len ( result ) == 0 {
ce . Reply ( "No contacts or groups found" )
return
}
2021-11-22 14:36:23 +01:00
ce . Reply ( strings . Join ( result , "\n\n" ) )
2021-11-05 01:27:08 +01:00
}
2019-02-20 13:39:44 +01:00
const cmdOpenHelp = ` open <_group JID_> - Open a group chat portal. `
2018-12-07 14:42:57 +01:00
2019-02-20 13:39:44 +01:00
func ( handler * CommandHandler ) CommandOpen ( ce * CommandEvent ) {
2018-12-08 00:30:15 +01:00
if len ( ce . Args ) == 0 {
2019-02-20 13:39:44 +01:00
ce . Reply ( "**Usage:** `open <group JID>`" )
2019-03-15 15:45:27 +01:00
return
2019-02-20 13:39:44 +01:00
}
2021-11-01 10:28:52 +01:00
var jid types . JID
if strings . ContainsRune ( ce . Args [ 0 ] , '@' ) {
jid , _ = types . ParseJID ( ce . Args [ 0 ] )
} else {
jid = types . NewJID ( ce . Args [ 0 ] , types . GroupServer )
}
2022-02-06 13:40:39 +01:00
if jid . Server != types . GroupServer || ( ! strings . ContainsRune ( jid . User , '-' ) && len ( jid . User ) < 15 ) {
2021-11-01 10:28:52 +01:00
ce . Reply ( "That does not look like a group JID" )
return
}
info , err := ce . User . Client . GetGroupInfo ( jid )
if err != nil {
ce . Reply ( "Failed to get group info: %v" , err )
return
}
handler . log . Debugln ( "Importing" , jid , "for" , ce . User . MXID )
portal := ce . User . GetPortalByJID ( info . JID )
if len ( portal . MXID ) > 0 {
portal . UpdateMatrixRoom ( ce . User , info )
ce . Reply ( "Portal room synced." )
} else {
2022-03-25 07:15:52 +01:00
err = portal . CreateMatrixRoom ( ce . User , info , true , true )
2021-11-01 10:28:52 +01:00
if err != nil {
ce . Reply ( "Failed to create room: %v" , err )
} else {
ce . Reply ( "Portal room created." )
}
}
2019-02-20 13:39:44 +01:00
}
2021-10-22 19:14:34 +02:00
const cmdPMHelp = ` pm <_international phone number_> - Open a private chat with the given phone number. `
2019-02-20 13:39:44 +01:00
func ( handler * CommandHandler ) CommandPM ( ce * CommandEvent ) {
if len ( ce . Args ) == 0 {
2021-10-22 19:14:34 +02:00
ce . Reply ( "**Usage:** `pm <international phone number>`" )
2019-02-20 13:39:44 +01:00
return
}
2018-12-07 14:42:57 +01:00
user := ce . User
2019-02-20 13:39:44 +01:00
number := strings . Join ( ce . Args , "" )
2021-10-22 19:14:34 +02:00
resp , err := ce . User . Client . IsOnWhatsApp ( [ ] string { number } )
if err != nil {
ce . Reply ( "Failed to check if user is on WhatsApp: %v" , err )
return
} else if len ( resp ) == 0 {
ce . Reply ( "Didn't get a response to checking if the user is on WhatsApp" )
return
2019-02-20 13:39:44 +01:00
}
2021-10-22 19:14:34 +02:00
targetUser := resp [ 0 ]
if ! targetUser . IsIn {
ce . Reply ( "The server said +%s is not on WhatsApp" , targetUser . JID . User )
return
2019-02-20 13:39:44 +01:00
}
2018-12-07 14:42:57 +01:00
2022-02-17 14:14:53 +01:00
portal , puppet , justCreated , err := user . StartPM ( targetUser . JID , "manual PM command" )
2019-02-20 13:39:44 +01:00
if err != nil {
2019-05-15 22:04:09 +02:00
ce . Reply ( "Failed to create portal room: %v" , err )
2022-02-17 14:14:53 +01:00
} else if ! justCreated {
ce . Reply ( "You already have a private chat portal with +%s at [%s](https://matrix.to/#/%s)" , puppet . JID . User , puppet . Displayname , portal . MXID )
} else {
ce . Reply ( "Created portal room with +%s and invited you to it." , puppet . JID . User )
2018-12-07 14:42:57 +01:00
}
}
2019-05-24 01:33:26 +02:00
2021-12-30 05:51:09 +01:00
const cmdSyncHelp = ` sync <appstate/contacts/groups/space> [--create-portals] - Synchronize data from WhatsApp. `
2021-11-08 19:57:04 +01:00
func ( handler * CommandHandler ) CommandSync ( ce * CommandEvent ) {
if len ( ce . Args ) == 0 {
2021-12-29 20:40:58 +01:00
ce . Reply ( "**Usage:** `sync <appstate/contacts/groups/space> [--create-portals]`" )
2021-11-08 19:57:04 +01:00
return
}
args := strings . ToLower ( strings . Join ( ce . Args , " " ) )
contacts := strings . Contains ( args , "contacts" )
appState := strings . Contains ( args , "appstate" )
2021-12-29 20:40:58 +01:00
space := strings . Contains ( args , "space" )
groups := strings . Contains ( args , "groups" ) || space
2021-11-08 19:57:04 +01:00
createPortals := strings . Contains ( args , "--create-portals" )
if appState {
for _ , name := range appstate . AllPatchNames {
err := ce . User . Client . FetchAppState ( name , true , false )
2022-05-16 10:43:09 +02:00
if errors . Is ( err , appstate . ErrKeyNotFound ) {
ce . Reply ( "Key not found error syncing app state %s: %v\n\nKey requests are sent automatically, and the sync should happen in the background after your phone responds." , name , err )
return
} else if err != nil {
2021-11-09 22:29:47 +01:00
ce . Reply ( "Error syncing app state %s: %v" , name , err )
2021-11-08 19:57:04 +01:00
} else if name == appstate . WAPatchCriticalUnblockLow {
ce . Reply ( "Synced app state %s, contact sync running in background" , name )
} else {
ce . Reply ( "Synced app state %s" , name )
}
}
} else if contacts {
err := ce . User . ResyncContacts ( )
if err != nil {
ce . Reply ( "Error resyncing contacts: %v" , err )
} else {
ce . Reply ( "Resynced contacts" )
}
}
2021-12-29 20:40:58 +01:00
if space {
2021-12-29 20:52:29 +01:00
if ! ce . Bridge . Config . Bridge . PersonalFilteringSpaces {
ce . Reply ( "Personal filtering spaces are not enabled on this instance of the bridge" )
return
}
2021-12-29 20:40:58 +01:00
keys := ce . Bridge . DB . Portal . FindPrivateChatsNotInSpace ( ce . User . JID )
count := 0
for _ , key := range keys {
portal := ce . Bridge . GetPortalByJID ( key )
portal . addToSpace ( ce . User )
count ++
}
plural := "s"
if count == 1 {
plural = ""
}
ce . Reply ( "Added %d DM room%s to space" , count , plural )
}
2021-11-08 19:57:04 +01:00
if groups {
err := ce . User . ResyncGroups ( createPortals )
if err != nil {
ce . Reply ( "Error resyncing groups: %v" , err )
} else {
ce . Reply ( "Resynced groups" )
}
}
}
2021-09-13 14:34:39 +02:00
const cmdLoginMatrixHelp = ` login-matrix <_access token_> - Replace your WhatsApp account's Matrix puppet with your real Matrix account. `
2019-05-24 01:33:26 +02:00
func ( handler * CommandHandler ) CommandLoginMatrix ( ce * CommandEvent ) {
if len ( ce . Args ) == 0 {
ce . Reply ( "**Usage:** `login-matrix <access token>`" )
return
}
puppet := handler . bridge . GetPuppetByJID ( ce . User . JID )
err := puppet . SwitchCustomMXID ( ce . Args [ 0 ] , ce . User . MXID )
if err != nil {
ce . Reply ( "Failed to switch puppet: %v" , err )
return
}
ce . Reply ( "Successfully switched puppet" )
}
2021-10-30 22:44:41 +02:00
const cmdPingMatrixHelp = ` ping-matrix - Check if your double puppet is working correctly. `
func ( handler * CommandHandler ) CommandPingMatrix ( ce * CommandEvent ) {
puppet := handler . bridge . GetPuppetByCustomMXID ( ce . User . MXID )
if puppet == nil || puppet . CustomIntent ( ) == nil {
ce . Reply ( "You have not changed your WhatsApp account's Matrix puppet." )
return
}
resp , err := puppet . CustomIntent ( ) . Whoami ( )
if err != nil {
ce . Reply ( "Failed to validate Matrix login: %v" , err )
} else {
ce . Reply ( "Confirmed valid access token for %s / %s" , resp . UserID , resp . DeviceID )
}
}
2019-05-24 01:33:26 +02:00
const cmdLogoutMatrixHelp = ` logout-matrix - Switch your WhatsApp account's Matrix puppet back to the default one. `
func ( handler * CommandHandler ) CommandLogoutMatrix ( ce * CommandEvent ) {
2021-10-30 22:44:41 +02:00
puppet := handler . bridge . GetPuppetByCustomMXID ( ce . User . MXID )
if puppet == nil || puppet . CustomIntent ( ) == nil {
2019-05-31 22:02:00 +02:00
ce . Reply ( "You had not changed your WhatsApp account's Matrix puppet." )
return
}
err := puppet . SwitchCustomMXID ( "" , "" )
if err != nil {
ce . Reply ( "Failed to remove custom puppet: %v" , err )
return
}
ce . Reply ( "Successfully removed custom puppet" )
2019-05-24 01:33:26 +02:00
}
2022-05-15 13:01:23 +02:00
const cmdDisappearingTimerHelp = ` disappearing-timer <off/1d/7d/90d> - Set future messages in the room to disappear after the given time. `
func ( handler * CommandHandler ) CommandDisappearingTimer ( ce * CommandEvent ) {
duration , ok := whatsmeow . ParseDisappearingTimerString ( ce . Args [ 0 ] )
if ! ok {
ce . Reply ( "Invalid timer '%s'" , ce . Args [ 0 ] )
return
}
prevExpirationTime := ce . Portal . ExpirationTime
ce . Portal . ExpirationTime = uint32 ( duration . Seconds ( ) )
err := ce . User . Client . SetDisappearingTimer ( ce . Portal . Key . JID , duration )
if err != nil {
ce . Reply ( "Failed to set disappearing timer: %v" , err )
ce . Portal . ExpirationTime = prevExpirationTime
return
}
ce . Portal . Update ( )
if ! ce . Portal . IsPrivateChat ( ) && ! ce . Bridge . Config . Bridge . DisappearingMessagesInGroups {
ce . Reply ( "Disappearing timer changed successfully, but this bridge is not configured to disappear messages in group chats." )
} else {
ce . React ( "✅" )
}
}