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
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"
2022-05-22 00:06:30 +02:00
"maunium.net/go/mautrix/bridge"
2022-05-22 15:15:54 +02:00
"maunium.net/go/mautrix/bridge/commands"
2022-08-15 15:36:28 +02:00
"maunium.net/go/mautrix/bridge/status"
2020-05-08 21:32:22 +02:00
"maunium.net/go/mautrix/event"
"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
)
2022-05-22 15:15:54 +02:00
type WrappedCommandEvent struct {
* commands . Event
Bridge * WABridge
User * User
Portal * Portal
2018-08-18 21:57:08 +02:00
}
2022-05-22 15:15:54 +02:00
func ( br * WABridge ) RegisterCommands ( ) {
proc := br . CommandProcessor . ( * commands . Processor )
proc . AddHandlers (
cmdSetRelay ,
cmdUnsetRelay ,
cmdInviteLink ,
cmdResolveLink ,
cmdJoin ,
cmdAccept ,
cmdCreate ,
cmdLogin ,
cmdLogout ,
cmdTogglePresence ,
cmdDeleteSession ,
cmdReconnect ,
cmdDisconnect ,
cmdPing ,
cmdDeletePortal ,
cmdDeleteAllPortals ,
cmdBackfill ,
cmdList ,
cmdSearch ,
cmdOpen ,
cmdPM ,
cmdSync ,
cmdDisappearingTimer ,
)
2018-08-18 21:57:08 +02:00
}
2022-05-22 15:15:54 +02:00
func wrapCommand ( handler func ( * WrappedCommandEvent ) ) func ( * commands . Event ) {
return func ( ce * commands . Event ) {
user := ce . User . ( * User )
var portal * Portal
if ce . Portal != nil {
portal = ce . Portal . ( * Portal )
2019-03-15 15:45:27 +01:00
}
2022-05-22 15:15:54 +02:00
br := ce . Bridge . Child . ( * WABridge )
handler ( & WrappedCommandEvent { ce , br , user , portal } )
2018-08-18 21:57:08 +02:00
}
}
2020-10-05 21:32:15 +02:00
2022-05-22 15:15:54 +02:00
var (
HelpSectionConnectionManagement = commands . HelpSection { Name : "Connection management" , Order : 11 }
HelpSectionCreatingPortals = commands . HelpSection { Name : "Creating portals" , Order : 15 }
HelpSectionPortalManagement = commands . HelpSection { Name : "Portal management" , Order : 20 }
HelpSectionInvites = commands . HelpSection { Name : "Group invites" , Order : 25 }
HelpSectionMiscellaneous = commands . HelpSection { Name : "Miscellaneous" , Order : 30 }
)
2018-08-18 21:57:08 +02:00
2022-05-22 15:15:54 +02:00
var cmdSetRelay = & commands . FullHandler {
Func : wrapCommand ( fnSetRelay ) ,
Name : "set-relay" ,
Help : commands . HelpMeta {
Section : HelpSectionPortalManagement ,
Description : "Relay messages in this room through your WhatsApp account." ,
} ,
RequiresPortal : true ,
RequiresLogin : true ,
}
2021-10-28 12:57:15 +02:00
2022-05-22 15:15:54 +02:00
func fnSetRelay ( ce * WrappedCommandEvent ) {
if ! ce . Bridge . Config . Bridge . Relay . Enabled {
2021-10-28 12:57:15 +02:00
ce . Reply ( "Relay mode is not enabled on this instance of the bridge" )
2022-05-22 15:15:54 +02:00
} else if ce . Bridge . Config . Bridge . Relay . AdminOnly && ! ce . User . Admin {
2021-10-28 12:57:15 +02:00
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
2022-05-13 01:56:40 +02:00
ce . Portal . Update ( nil )
2021-10-28 12:57:15 +02:00
ce . Reply ( "Messages from non-logged-in users in this room will now be bridged through your WhatsApp account" )
}
}
2022-05-22 15:15:54 +02:00
var cmdUnsetRelay = & commands . FullHandler {
Func : wrapCommand ( fnUnsetRelay ) ,
Name : "unset-relay" ,
Help : commands . HelpMeta {
Section : HelpSectionPortalManagement ,
Description : "Stop relaying messages in this room." ,
} ,
RequiresPortal : true ,
}
2021-10-28 12:57:15 +02:00
2022-05-22 15:15:54 +02:00
func fnUnsetRelay ( ce * WrappedCommandEvent ) {
if ! ce . Bridge . Config . Bridge . Relay . Enabled {
2021-10-28 12:57:15 +02:00
ce . Reply ( "Relay mode is not enabled on this instance of the bridge" )
2022-05-22 15:15:54 +02:00
} else if ce . Bridge . Config . Bridge . Relay . AdminOnly && ! ce . User . Admin {
2021-10-28 12:57:15 +02:00
ce . Reply ( "Only admins are allowed to enable relay mode on this instance of the bridge" )
} else {
ce . Portal . RelayUserID = ""
2022-05-13 01:56:40 +02:00
ce . Portal . Update ( nil )
2021-10-28 12:57:15 +02:00
ce . Reply ( "Messages from non-logged-in users will no longer be bridged in this room" )
2019-11-10 20:22:11 +01:00
}
}
2022-05-22 15:15:54 +02:00
var cmdInviteLink = & commands . FullHandler {
Func : wrapCommand ( fnInviteLink ) ,
Name : "invite-link" ,
Help : commands . HelpMeta {
Section : HelpSectionInvites ,
Description : "Get an invite link to the current group chat, optionally regenerating the link and revoking the old link." ,
Args : "[--reset]" ,
} ,
RequiresPortal : true ,
RequiresLogin : true ,
2020-06-03 19:32:53 +02:00
}
2022-05-22 15:15:54 +02:00
func fnInviteLink ( ce * WrappedCommandEvent ) {
2021-10-31 18:59:23 +01:00
reset := len ( ce . Args ) > 0 && strings . ToLower ( ce . Args [ 0 ] ) == "--reset"
2022-05-22 15:15:54 +02:00
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
}
2022-05-22 15:15:54 +02:00
var cmdResolveLink = & commands . FullHandler {
Func : wrapCommand ( fnResolveLink ) ,
Name : "resolve-link" ,
Help : commands . HelpMeta {
Section : HelpSectionInvites ,
Description : "Resolve a WhatsApp group invite or business message link." ,
2022-09-28 13:45:09 +02:00
Args : "<_group, contact, or message link_>" ,
2022-05-22 15:15:54 +02:00
} ,
RequiresLogin : true ,
}
2020-06-25 22:29:16 +02:00
2022-05-22 15:15:54 +02:00
func fnResolveLink ( ce * WrappedCommandEvent ) {
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 )
2022-09-28 13:45:09 +02:00
} else if strings . HasPrefix ( ce . Args [ 0 ] , whatsmeow . ContactQRLinkPrefix ) || strings . HasPrefix ( ce . Args [ 0 ] , whatsmeow . ContactQRLinkDirectPrefix ) {
target , err := ce . User . Client . ResolveContactQRLink ( ce . Args [ 0 ] )
if err != nil {
ce . Reply ( "Failed to get contact info: %v" , err )
return
}
if target . PushName != "" {
ce . Reply ( "That link points at %s (+%s)" , target . PushName , target . JID . User )
} else {
ce . Reply ( "That link points at +%s" , target . JID . User )
}
2021-11-27 10:30:41 +01:00
} 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
}
}
2022-05-22 15:15:54 +02:00
var cmdJoin = & commands . FullHandler {
Func : wrapCommand ( fnJoin ) ,
Name : "join" ,
Help : commands . HelpMeta {
Section : HelpSectionInvites ,
Description : "Join a group chat with an invite link." ,
Args : "<_invite link_>" ,
} ,
RequiresLogin : true ,
}
2021-10-31 18:59:23 +01:00
2022-05-22 15:15:54 +02:00
func fnJoin ( ce * WrappedCommandEvent ) {
2020-06-25 22:29:16 +02:00
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
}
2022-05-22 15:15:54 +02:00
ce . Log . Debugln ( "%s successfully joined group %s" , ce . User . MXID , jid )
2021-10-31 18:59:23 +01:00
ce . Reply ( "Successfully joined group `%s`, the portal should be created momentarily" , jid )
2020-06-25 21:40:34 +02:00
}
2022-05-22 00:06:30 +02:00
func tryDecryptEvent ( crypto bridge . Crypto , evt * event . Event ) ( json . RawMessage , error ) {
2022-03-14 12:15:52 +01:00
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
}
2022-05-22 15:15:54 +02:00
var cmdAccept = & commands . FullHandler {
Func : wrapCommand ( fnAccept ) ,
Name : "accept" ,
Help : commands . HelpMeta {
Section : HelpSectionInvites ,
Description : "Accept a group invite. This can only be used in reply to a group invite message." ,
} ,
RequiresLogin : true ,
RequiresPortal : true ,
}
func fnAccept ( ce * WrappedCommandEvent ) {
if len ( ce . ReplyTo ) == 0 {
2021-10-31 19:42:53 +01:00
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 {
2022-05-22 15:15:54 +02:00
ce . Log . Errorln ( "Failed to get event %s to handle !wa accept command: %v" , ce . ReplyTo , err )
2021-10-31 19:42:53 +01:00
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 {
2022-05-22 15:15:54 +02:00
ce . Log . Errorln ( "Failed to decrypt event %s to handle !wa accept command: %v" , ce . ReplyTo , err )
2022-03-14 12:15:52 +01:00
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
}
}
2022-05-22 15:15:54 +02:00
var cmdCreate = & commands . FullHandler {
Func : wrapCommand ( fnCreate ) ,
Name : "create" ,
Help : commands . HelpMeta {
Section : HelpSectionCreatingPortals ,
Description : "Create a WhatsApp group chat for the current Matrix room." ,
} ,
RequiresLogin : true ,
}
2020-07-10 14:23:32 +02:00
2022-05-22 15:15:54 +02:00
func fnCreate ( ce * WrappedCommandEvent ) {
2020-07-10 14:23:32 +02:00
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 ) {
2022-05-22 15:15:54 +02:00
ce . 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
}
2022-12-02 14:36:19 +01:00
var createEvent event . CreateEventContent
err = ce . Bot . StateEvent ( ce . RoomID , event . StateCreate , "" , & createEvent )
if err != nil && ! errors . Is ( err , mautrix . MNotFound ) {
ce . Reply ( "Failed to get room create event" )
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 {
2022-05-22 15:15:54 +02:00
jid , ok := ce . Bridge . ParsePuppetMXID ( userID )
2021-11-05 11:17:56 +01:00
if ! ok {
2022-05-22 15:15:54 +02:00
user := ce . Bridge . GetUserByMXID ( userID )
2021-11-05 11:17:56 +01:00
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 )
}
}
2022-12-02 14:36:19 +01:00
// TODO check m.space.parent to create rooms directly in communities
2020-07-10 14:23:32 +02:00
2022-05-22 15:15:54 +02:00
ce . Log . Infofln ( "Creating group for %s with name %s and participants %+v" , ce . RoomID , roomNameEvent . Name , participants )
2022-12-02 14:36:19 +01:00
resp , err := ce . User . Client . CreateGroup ( whatsmeow . ReqCreateGroup {
Name : roomNameEvent . Name ,
Participants : participants ,
GroupParent : types . GroupParent {
IsParent : createEvent . Type == event . RoomTypeSpace ,
} ,
} )
2021-11-05 11:17:56 +01:00
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
2022-12-02 14:36:19 +01:00
portal . IsParent = resp . IsParent
2021-11-05 11:17:56 +01:00
portal . Encrypted = encryptionEvent . Algorithm == id . AlgorithmMegolmV1
2022-05-22 15:15:54 +02:00
if ! portal . Encrypted && ce . Bridge . Config . Bridge . Encryption . Default {
2022-06-21 19:57:08 +02:00
_ , err = portal . MainIntent ( ) . SendStateEvent ( portal . MXID , event . StateEncryption , "" , portal . GetEncryptionEventContent ( ) )
2021-11-05 11:17:56 +01:00
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
}
2022-05-13 01:56:40 +02:00
portal . Update ( nil )
2021-11-05 11:17:56 +01:00
portal . UpdateBridgeInfo ( )
ce . Reply ( "Successfully created WhatsApp group %s" , portal . Key . JID )
2020-07-10 14:23:32 +02:00
}
2022-05-22 15:15:54 +02:00
var cmdLogin = & commands . FullHandler {
Func : wrapCommand ( fnLogin ) ,
Name : "login" ,
Help : commands . HelpMeta {
Section : commands . HelpSectionAuth ,
Description : "Link the bridge to your WhatsApp account as a web client." ,
} ,
2020-05-09 01:03:59 +02:00
}
2022-05-22 15:15:54 +02:00
func fnLogin ( ce * WrappedCommandEvent ) {
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
}
2022-05-22 15:15:54 +02:00
func ( user * User ) sendQR ( ce * WrappedCommandEvent , 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
}
2022-05-22 15:15:54 +02:00
func ( user * User ) uploadQR ( ce * WrappedCommandEvent , code string ) ( id . ContentURI , bool ) {
2021-10-22 19:14:34 +02:00
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
}
2022-05-22 15:15:54 +02:00
var cmdLogout = & commands . FullHandler {
Func : wrapCommand ( fnLogout ) ,
Name : "logout" ,
Help : commands . HelpMeta {
Section : commands . HelpSectionAuth ,
Description : "Unlink the bridge from your WhatsApp account." ,
} ,
}
2018-10-16 18:35:39 +02:00
2022-05-22 15:15:54 +02:00
func fnLogout ( ce * WrappedCommandEvent ) {
2018-08-18 21:57:08 +02:00
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
}
2022-05-22 15:15:54 +02:00
puppet := ce . Bridge . GetPuppetByJID ( ce . User . JID )
2020-05-21 18:49:01 +02:00
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-08-15 15:36:28 +02:00
ce . User . removeFromJIDMap ( status . BridgeState { StateEvent : status . 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." )
}
2022-05-22 15:15:54 +02:00
var cmdTogglePresence = & commands . FullHandler {
Func : wrapCommand ( fnTogglePresence ) ,
Name : "toggle-presence" ,
Help : commands . HelpMeta {
Section : HelpSectionConnectionManagement ,
Description : "Toggle bridging of presence or read receipts." ,
} ,
}
2020-07-10 13:53:18 +02:00
2022-05-22 15:15:54 +02:00
func fnTogglePresence ( ce * WrappedCommandEvent ) {
2020-07-10 13:53:18 +02:00
if ce . User . Session == nil {
ce . Reply ( "You're not logged in." )
return
}
2022-05-22 15:15:54 +02:00
customPuppet := ce . Bridge . GetPuppetByCustomMXID ( ce . User . MXID )
2020-07-10 13:53:18 +02:00
if customPuppet == nil {
ce . Reply ( "You're not logged in with your Matrix account." )
return
}
2022-05-22 15:15:54 +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" )
2021-04-19 18:25:40 +02:00
}
2022-05-22 15:15:54 +02:00
if ce . User . IsLoggedIn ( ) {
err := ce . User . Client . SendPresence ( newPresence )
if err != nil {
ce . User . log . Warnln ( "Failed to set presence:" , err )
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
}
2022-05-22 15:15:54 +02:00
var cmdDeleteSession = & commands . FullHandler {
Func : wrapCommand ( fnDeleteSession ) ,
Name : "delete-session" ,
Help : commands . HelpMeta {
Section : commands . HelpSectionAuth ,
Description : "Delete session information and disconnect from WhatsApp without sending a logout request." ,
} ,
}
2019-05-16 17:00:46 +02:00
2022-05-22 15:15:54 +02:00
func fnDeleteSession ( ce * WrappedCommandEvent ) {
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-08-15 15:36:28 +02:00
ce . User . removeFromJIDMap ( status . BridgeState { StateEvent : status . 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" )
}
2022-05-22 15:15:54 +02:00
var cmdReconnect = & commands . FullHandler {
Func : wrapCommand ( fnReconnect ) ,
Name : "reconnect" ,
Help : commands . HelpMeta {
Section : HelpSectionConnectionManagement ,
Description : "Reconnect to WhatsApp." ,
} ,
}
2019-05-15 22:04:09 +02:00
2022-05-22 15:15:54 +02:00
func fnReconnect ( ce * WrappedCommandEvent ) {
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 ( )
2022-08-15 15:36:28 +02:00
ce . User . BridgeState . Send ( status . BridgeState { StateEvent : status . StateTransientDisconnect , Error : WANotConnected } )
2021-10-31 13:11:15 +01:00
ce . User . Connect ( )
ce . Reply ( "Restarted connection to WhatsApp" )
}
2019-05-23 19:09:13 +02:00
}
2022-05-22 15:15:54 +02:00
var cmdDisconnect = & commands . FullHandler {
Func : wrapCommand ( fnDisconnect ) ,
Name : "disconnect" ,
Help : commands . HelpMeta {
Section : HelpSectionConnectionManagement ,
Description : "Disconnect from WhatsApp (without logging out)." ,
} ,
}
2019-05-15 22:04:09 +02:00
2022-05-22 15:15:54 +02:00
func fnDisconnect ( ce * WrappedCommandEvent ) {
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." )
2022-08-15 15:36:28 +02:00
ce . User . BridgeState . Send ( status . BridgeState { StateEvent : status . StateBadCredentials , Error : WANotConnected } )
2019-05-15 22:04:09 +02:00
}
2022-05-22 15:15:54 +02:00
var cmdPing = & commands . FullHandler {
Func : wrapCommand ( fnPing ) ,
Name : "ping" ,
Help : commands . HelpMeta {
Section : HelpSectionConnectionManagement ,
Description : "Check your connection to WhatsApp." ,
} ,
}
2019-08-24 21:39:12 +02:00
2022-05-22 15:15:54 +02:00
func fnPing ( ce * WrappedCommandEvent ) {
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
}
}
2021-10-28 13:27:59 +02:00
func canDeletePortal ( portal * Portal , userID id . UserID ) bool {
2022-05-22 15:15:54 +02:00
if len ( portal . MXID ) == 0 {
return false
}
2021-10-28 13:27:59 +02:00
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
}
2022-05-22 15:15:54 +02:00
var cmdDeletePortal = & commands . FullHandler {
Func : wrapCommand ( fnDeletePortal ) ,
Name : "delete-portal" ,
Help : commands . HelpMeta {
Section : HelpSectionPortalManagement ,
Description : "Delete the current portal. If the portal is used by other people, this is limited to bridge admins." ,
} ,
RequiresPortal : true ,
}
2019-05-21 20:06:27 +02:00
2022-05-22 15:15:54 +02:00
func fnDeletePortal ( ce * WrappedCommandEvent ) {
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
}
2022-05-22 15:15:54 +02:00
var cmdDeleteAllPortals = & commands . FullHandler {
Func : wrapCommand ( fnDeleteAllPortals ) ,
Name : "delete-all-portals" ,
Help : commands . HelpMeta {
Section : HelpSectionPortalManagement ,
Description : "Delete all portals." ,
} ,
}
2020-05-23 22:25:22 +02:00
2022-05-22 15:15:54 +02:00
func fnDeleteAllPortals ( ce * WrappedCommandEvent ) {
portals := ce . Bridge . GetAllPortals ( )
2021-10-28 13:27:59 +02:00
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
}
2022-05-22 15:15:54 +02:00
if len ( portalsToDelete ) == 0 {
ce . Reply ( "Didn't find any portals to delete" )
return
}
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 ,
} )
}
}
2022-05-22 15:15:54 +02:00
customPuppet := ce . 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-05-22 15:15:54 +02:00
var cmdBackfill = & commands . FullHandler {
Func : wrapCommand ( fnBackfill ) ,
Name : "backfill" ,
Help : commands . HelpMeta {
Section : HelpSectionPortalManagement ,
Description : "Backfill all messages the portal." ,
Args : "[_batch size_] [_batch delay_]" ,
} ,
RequiresPortal : true ,
}
2022-03-24 21:21:57 +01:00
2022-05-22 15:15:54 +02:00
func fnBackfill ( ce * WrappedCommandEvent ) {
2022-03-24 21:21:57 +01:00
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-12 19:54:38 +02:00
backfillMessages := ce . Portal . bridge . DB . Backfill . NewWithValues ( ce . User . MXID , database . BackfillImmediate , 0 , & ce . Portal . Key , nil , batchSize , - 1 , batchDelay )
2022-04-19 04:50:21 +02:00
backfillMessages . Insert ( )
2022-05-13 21:18:52 +02:00
ce . User . BackfillQueue . ReCheck ( )
2022-03-24 21:21:57 +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 )
}
2022-05-22 00:06:30 +02:00
func formatContacts ( bridge * WABridge , input map [ types . JID ] types . ContactInfo , query string ) ( result [ ] string ) {
2021-11-05 01:27:08 +01:00
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
2022-05-22 15:15:54 +02:00
var cmdList = & commands . FullHandler {
Func : wrapCommand ( fnList ) ,
Name : "list" ,
Help : commands . HelpMeta {
Section : HelpSectionMiscellaneous ,
Description : "Get a list of all contacts and groups." ,
Args : "<`contacts`|`groups`> [_page_] [_items per page_]" ,
} ,
RequiresLogin : true ,
}
func fnList ( ce * WrappedCommandEvent ) {
2020-05-23 22:17:43 +02:00
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
2022-05-22 15:15:54 +02:00
var cmdSearch = & commands . FullHandler {
Func : wrapCommand ( fnSearch ) ,
Name : "search" ,
Help : commands . HelpMeta {
Section : HelpSectionMiscellaneous ,
Description : "Search for contacts or groups." ,
Args : "<_query_>" ,
} ,
RequiresLogin : true ,
}
2021-11-05 01:27:08 +01:00
2022-05-22 15:15:54 +02:00
func fnSearch ( ce * WrappedCommandEvent ) {
2021-11-05 01:27:08 +01:00
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
}
2022-05-22 15:15:54 +02:00
var cmdOpen = & commands . FullHandler {
Func : wrapCommand ( fnOpen ) ,
Name : "open" ,
Help : commands . HelpMeta {
Section : HelpSectionCreatingPortals ,
Description : "Open a group chat portal." ,
Args : "<_group JID_>" ,
} ,
RequiresLogin : true ,
}
2018-12-07 14:42:57 +01:00
2022-05-22 15:15:54 +02:00
func fnOpen ( ce * WrappedCommandEvent ) {
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
}
2022-05-22 15:15:54 +02:00
ce . Log . Debugln ( "Importing" , jid , "for" , ce . User . MXID )
2021-11-01 10:28:52 +01:00
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
}
2022-05-22 15:15:54 +02:00
var cmdPM = & commands . FullHandler {
Func : wrapCommand ( fnPM ) ,
Name : "pm" ,
Help : commands . HelpMeta {
Section : HelpSectionCreatingPortals ,
Description : "Open a private chat with the given phone number." ,
Args : "<_international phone number_>" ,
} ,
RequiresLogin : true ,
}
2019-02-20 13:39:44 +01:00
2022-05-22 15:15:54 +02:00
func fnPM ( ce * WrappedCommandEvent ) {
2019-02-20 13:39:44 +01:00
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
2022-05-22 15:15:54 +02:00
var cmdSync = & commands . FullHandler {
Func : wrapCommand ( fnSync ) ,
Name : "sync" ,
Help : commands . HelpMeta {
Section : HelpSectionMiscellaneous ,
Description : "Synchronize data from WhatsApp." ,
2022-09-18 13:46:16 +02:00
Args : "<appstate/contacts/groups/space> [--contact-avatars] [--create-portals]" ,
2022-05-22 15:15:54 +02:00
} ,
RequiresLogin : true ,
}
2021-11-08 19:57:04 +01:00
2022-05-22 15:15:54 +02:00
func fnSync ( ce * WrappedCommandEvent ) {
2021-11-08 19:57:04 +01:00
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
2022-09-18 13:46:16 +02:00
if ! contacts && ! appState && ! space && ! groups {
ce . Reply ( "**Usage:** `sync <appstate/contacts/groups/space> [--contact-avatars] [--create-portals]`" )
return
}
2021-11-08 19:57:04 +01:00
createPortals := strings . Contains ( args , "--create-portals" )
2022-06-28 13:37:49 +02:00
contactAvatars := strings . Contains ( args , "--contact-avatars" )
if contactAvatars && ( ! contacts || appState ) {
ce . Reply ( "`--contact-avatars` can only be used with `sync contacts`" )
return
}
2021-11-08 19:57:04 +01:00
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 {
2022-06-28 13:37:49 +02:00
err := ce . User . ResyncContacts ( contactAvatars )
2021-11-08 19:57:04 +01:00
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 )
2022-12-02 14:36:19 +01:00
portal . addToPersonalSpace ( ce . User )
2021-12-29 20:40:58 +01:00
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" )
}
}
}
2022-05-22 15:15:54 +02:00
var cmdDisappearingTimer = & commands . FullHandler {
Func : wrapCommand ( fnDisappearingTimer ) ,
Name : "disappearing-timer" ,
Aliases : [ ] string { "disappear-timer" } ,
Help : commands . HelpMeta {
Section : HelpSectionPortalManagement ,
Description : "Set future messages in the room to disappear after the given time." ,
Args : "<off/1d/7d/90d>" ,
} ,
2022-06-30 16:25:43 +02:00
RequiresLogin : true ,
RequiresPortal : true ,
2022-05-22 15:15:54 +02:00
}
2022-05-15 13:01:23 +02:00
2022-05-22 15:15:54 +02:00
func fnDisappearingTimer ( ce * WrappedCommandEvent ) {
2022-06-30 16:25:43 +02:00
if len ( ce . Args ) == 0 {
ce . Reply ( "**Usage:** `disappearing-timer <off/1d/7d/90d>`" )
return
}
2022-05-15 13:01:23 +02:00
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
}
2022-05-17 02:08:20 +02:00
ce . Portal . Update ( nil )
2022-05-15 13:01:23 +02:00
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 ( "✅" )
}
}