From 518cb076ff32ff10a15a6ac797247c97c47ea2ba Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Jul 2020 15:23:32 +0300 Subject: [PATCH] Add command to create WhatsApp group --- commands.go | 74 +++++++++++++++++++++++++++++++++++++++- go.mod | 2 +- go.sum | 2 ++ matrix.go | 26 +++++++------- whatsapp-ext/group.go | 68 ++++++++++++++++++++++++++++++++++++ whatsapp-ext/whatsapp.go | 3 +- 6 files changed, 157 insertions(+), 18 deletions(-) create mode 100644 whatsapp-ext/group.go diff --git a/commands.go b/commands.go index 9913f2d..aa567b9 100644 --- a/commands.go +++ b/commands.go @@ -131,7 +131,7 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) { handler.CommandLogout(ce) case "toggle-presence": handler.CommandPresence(ce) - case "login-matrix", "sync", "list", "open", "pm", "invite-link", "join": + case "login-matrix", "sync", "list", "open", "pm", "invite-link", "join", "create": if !ce.User.HasSession() { ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.") return @@ -155,6 +155,8 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) { handler.CommandInviteLink(ce) case "join": handler.CommandJoin(ce) + case "create": + handler.CommandCreate(ce) } default: ce.Reply("Unknown Command") @@ -249,6 +251,75 @@ func (handler *CommandHandler) CommandJoin(ce *CommandEvent) { } } +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) + if err != nil { + 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) + if err != nil { + ce.Reply("Failed to get room encryption status") + return + } + + participants := []string{ce.User.JID} + for userID := range members.Joined { + jid, ok := handler.bridge.ParsePuppetMXID(userID) + if ok && jid != ce.User.JID { + participants = append(participants, jid) + } + } + + resp, err := ce.User.Conn.CreateGroup(roomNameEvent.Name, participants) + if err != nil { + ce.Reply("Failed to create group: %v", err) + return + } + portal := handler.bridge.GetPortalByJID(database.GroupPortalKey(resp.GroupID)) + 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 e2be:", err) + } + portal.Encrypted = true + } + + portal.Update() + portal.UpdateBridgeInfo() + + ce.Reply("Successfully created WhatsApp group %s", portal.Key.JID) + ce.User.addPortalToCommunity(portal) +} + const cmdSetPowerLevelHelp = `set-pl [user ID] - Change the power level in a portal room. Only for bridge admins.` func (handler *CommandHandler) CommandSetPowerLevel(ce *CommandEvent) { @@ -540,6 +611,7 @@ func (handler *CommandHandler) CommandHelp(ce *CommandEvent) { cmdPrefix + cmdPMHelp, cmdPrefix + cmdInviteLinkHelp, cmdPrefix + cmdJoinHelp, + cmdPrefix + cmdCreateHelp, cmdPrefix + cmdSetPowerLevelHelp, cmdPrefix + cmdDeletePortalHelp, cmdPrefix + cmdDeleteAllPortalsHelp, diff --git a/go.mod b/go.mod index db24d1c..4574b2a 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( gopkg.in/yaml.v2 v2.3.0 maunium.net/go/mauflag v1.0.0 maunium.net/go/maulogger/v2 v2.1.1 - maunium.net/go/mautrix v0.5.7 + maunium.net/go/mautrix v0.5.8 ) replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.3.4 diff --git a/go.sum b/go.sum index 2f77d69..37a4896 100644 --- a/go.sum +++ b/go.sum @@ -202,3 +202,5 @@ maunium.net/go/mautrix v0.5.6 h1:XCpyj3yeSOXpX+HMbF+3rdja97efMv/XchsOHylKdXY= maunium.net/go/mautrix v0.5.6/go.mod h1:FLbMANzwqlsX2Fgm7SDe+E4I3wSa4UxJRKqS5wGkCwA= maunium.net/go/mautrix v0.5.7 h1:tyRwllz3SZvMfD2YjaJPWopxmUCxZgQ2hl5/3/loHTE= maunium.net/go/mautrix v0.5.7/go.mod h1:FLbMANzwqlsX2Fgm7SDe+E4I3wSa4UxJRKqS5wGkCwA= +maunium.net/go/mautrix v0.5.8 h1:jOE3U8WYSIc4qbYvyVaDhOaQcB3sDPN5A2zQ93YixZ0= +maunium.net/go/mautrix v0.5.8/go.mod h1:Va/74MijqaS0DQ3aUqxmFO54/PMfr1LVsCOcGRHbYmo= diff --git a/matrix.go b/matrix.go index d8a5042..3aefd69 100644 --- a/matrix.go +++ b/matrix.go @@ -132,19 +132,25 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { return } - if !hasPuppets { - user := mx.bridge.GetUserByMXID(evt.Sender) + if !hasPuppets && (len(user.ManagementRoom) == 0 || evt.Content.AsMember().IsDirect) { user.SetManagementRoom(evt.RoomID) _, _ = intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.") mx.log.Debugln(evt.RoomID, "registered as a management room with", evt.Sender) } } -func (mx *MatrixHandler) handleExistingPrivatePortal(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) { +func (mx *MatrixHandler) handlePrivatePortal(roomID id.RoomID, inviter *User, puppet *Puppet, key database.PortalKey) { + portal := mx.bridge.GetPortalByJID(key) + + if len(portal.MXID) == 0 { + mx.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) + return + } + err := portal.MainIntent().EnsureInvited(portal.MXID, inviter.MXID) if err != nil { mx.log.Warnfln("Failed to invite %s to existing private chat portal %s with %s: %v. Redirecting portal to new room...", inviter.MXID, portal.MXID, puppet.JID, err) - mx.createPrivatePortalFromInvite(portal.Key, roomID, inviter, puppet, portal) + mx.createPrivatePortalFromInvite(roomID, inviter, puppet, portal) return } intent := puppet.DefaultIntent() @@ -153,10 +159,7 @@ func (mx *MatrixHandler) handleExistingPrivatePortal(roomID id.RoomID, inviter * _, _ = intent.LeaveRoom(roomID) } -func (mx *MatrixHandler) createPrivatePortalFromInvite(key database.PortalKey, roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) { - if portal == nil { - portal = mx.bridge.NewManualPortal(key) - } +func (mx *MatrixHandler) createPrivatePortalFromInvite(roomID id.RoomID, inviter *User, puppet *Puppet, portal *Portal) { portal.MXID = roomID portal.Topic = "WhatsApp private chat" _, _ = portal.MainIntent().SetRoomTopic(portal.MXID, portal.Topic) @@ -221,12 +224,7 @@ func (mx *MatrixHandler) HandlePuppetInvite(evt *event.Event, inviter *User, pup } if !hasBridgeBot && !hasOtherUsers { key := database.NewPortalKey(puppet.JID, inviter.JID) - existingPortal := mx.bridge.GetPortalByJID(key) - if existingPortal != nil && len(existingPortal.MXID) > 0 { - mx.handleExistingPrivatePortal(evt.RoomID, inviter, puppet, existingPortal) - } else { - mx.createPrivatePortalFromInvite(key, evt.RoomID, inviter, puppet, existingPortal) - } + mx.handlePrivatePortal(evt.RoomID, inviter, puppet, key) } else if !hasBridgeBot { mx.log.Debugln("Leaving multi-user room", evt.RoomID, "as", puppet.MXID, "after accepting invite from", evt.Sender) _, _ = intent.SendNotice(evt.RoomID, "Please invite the bridge bot first if you want to bridge to a WhatsApp group.") diff --git a/whatsapp-ext/group.go b/whatsapp-ext/group.go new file mode 100644 index 0000000..5992e39 --- /dev/null +++ b/whatsapp-ext/group.go @@ -0,0 +1,68 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2019 Tulir Asokan +// +// 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 . + +package whatsappExt + +import ( + "encoding/json" + "fmt" + + "maunium.net/go/mautrix-whatsapp/types" +) + +type CreateGroupResponse struct { + Status int `json:"status"` + GroupID types.WhatsAppID `json:"gid"` + Participants map[types.WhatsAppID]struct { + Code string `json:"code"` + } `json:"participants"` + + Source string `json:"-"` +} + +type actualCreateGroupResponse struct { + Status int `json:"status"` + GroupID types.WhatsAppID `json:"gid"` + Participants []map[types.WhatsAppID]struct { + Code string `json:"code"` + } `json:"participants"` +} + +func (ext *ExtendedConn) CreateGroup(subject string, participants []types.WhatsAppID) (*CreateGroupResponse, error) { + respChan, err := ext.Conn.CreateGroup(subject, participants) + if err != nil { + return nil, err + } + var resp CreateGroupResponse + var actualResp actualCreateGroupResponse + resp.Source = <-respChan + fmt.Println(">>>>>>", resp.Source) + err = json.Unmarshal([]byte(resp.Source), &actualResp) + if err != nil { + return nil, err + } + resp.Status = actualResp.Status + resp.GroupID = actualResp.GroupID + resp.Participants = make(map[types.WhatsAppID]struct { + Code string `json:"code"` + }) + for _, participantMap := range actualResp.Participants { + for jid, status := range participantMap { + resp.Participants[jid] = status + } + } + return &resp, nil +} diff --git a/whatsapp-ext/whatsapp.go b/whatsapp-ext/whatsapp.go index 9f0448e..a316c24 100644 --- a/whatsapp-ext/whatsapp.go +++ b/whatsapp-ext/whatsapp.go @@ -51,7 +51,6 @@ func (ext *ExtendedConn) AddHandler(handler whatsapp.Handler) { ext.handlers = append(ext.handlers, handler) } - func (ext *ExtendedConn) RemoveHandler(handler whatsapp.Handler) bool { ext.Conn.RemoveHandler(handler) for i, v := range ext.handlers { @@ -127,7 +126,7 @@ type ProfilePicInfo struct { URL string `json:"eurl"` Tag string `json:"tag"` - Status int16 `json:"status"` + Status int `json:"status"` } func (ppi *ProfilePicInfo) Download() (io.ReadCloser, error) {