diff --git a/commands.go b/commands.go index 595efcb..935e0c5 100644 --- a/commands.go +++ b/commands.go @@ -1048,26 +1048,14 @@ func (handler *CommandHandler) CommandPM(ce *CommandEvent) { return } - handler.log.Debugln("Importing", targetUser.JID, "for", user) - puppet := user.bridge.GetPuppetByJID(targetUser.JID) - puppet.SyncContact(user, true, "manual pm command") - portal := user.GetPortalByJID(puppet.JID) - if len(portal.MXID) > 0 { - ok := portal.ensureUserInvited(user) - if !ok { - portal.log.Warnfln("ensureUserInvited(%s) returned false, creating new portal", user.MXID) - portal.MXID = "" - } else { - ce.Reply("You already have a private chat portal with +%s at [%s](https://matrix.to/#/%s)", puppet.JID.User, puppet.Displayname, portal.MXID) - return - } - } - err = portal.CreateMatrixRoom(user, nil, false) + portal, puppet, justCreated, err := user.StartPM(targetUser.JID, "manual PM command") if err != nil { ce.Reply("Failed to create portal room: %v", err) - return + } 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) } - ce.Reply("Created portal room with +%s and invited you to it.", puppet.JID.User) } const cmdSyncHelp = `sync [--create-portals] - Synchronize data from WhatsApp.` diff --git a/go.mod b/go.mod index 07d7fe4..6856a31 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/prometheus/client_golang v1.11.1 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tidwall/gjson v1.14.0 - go.mau.fi/whatsmeow v0.0.0-20220217120518-0bf6c8fb0ce5 + go.mau.fi/whatsmeow v0.0.0-20220217121823-b7d4c5a8e8cc golang.org/x/image v0.0.0-20211028202545-6944b10bf410 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd google.golang.org/protobuf v1.27.1 diff --git a/go.sum b/go.sum index 608b2eb..3ec0410 100644 --- a/go.sum +++ b/go.sum @@ -120,8 +120,8 @@ github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= go.mau.fi/libsignal v0.0.0-20211109153248-a67163214910 h1:9FFhG0OmkuMau5UEaTgiUQ+7cSbtbOQ7hiWKdN8OI3I= go.mau.fi/libsignal v0.0.0-20211109153248-a67163214910/go.mod h1:AufGrvVh+00Nc07Jm4hTquh7yleZyn20tKJI2wCPAKg= -go.mau.fi/whatsmeow v0.0.0-20220217120518-0bf6c8fb0ce5 h1:hv2cBvttHOiRA/6JUt+yRMAX7CjjER6c+xO2WYqwtA0= -go.mau.fi/whatsmeow v0.0.0-20220217120518-0bf6c8fb0ce5/go.mod h1:NNI4Ah/B27mfQNChJMD1iSO8+HS+fQ4WqNuQ8Mh2/XI= +go.mau.fi/whatsmeow v0.0.0-20220217121823-b7d4c5a8e8cc h1:nmKmURePfVfpK+qLeAZvJIMbgpAKtfYcqeXwXAaFUls= +go.mau.fi/whatsmeow v0.0.0-20220217121823-b7d4c5a8e8cc/go.mod h1:NNI4Ah/B27mfQNChJMD1iSO8+HS+fQ4WqNuQ8Mh2/XI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= diff --git a/provisioning.go b/provisioning.go index 5b9e79f..c52cf58 100644 --- a/provisioning.go +++ b/provisioning.go @@ -1,5 +1,5 @@ // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2021 Tulir Asokan +// Copyright (C) 2022 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 @@ -29,7 +29,9 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/websocket" + "go.mau.fi/whatsmeow/appstate" + "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow" @@ -55,6 +57,8 @@ func (prov *ProvisioningAPI) Init() { r.HandleFunc("/disconnect", prov.Disconnect).Methods(http.MethodPost) r.HandleFunc("/reconnect", prov.Reconnect).Methods(http.MethodPost) r.HandleFunc("/sync/appstate/{name}", prov.SyncAppState).Methods(http.MethodPost) + r.HandleFunc("/contacts", prov.ListContacts).Methods(http.MethodGet) + r.HandleFunc("/pm/{number}", prov.StartPM).Methods(http.MethodPost) prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.asmux/ping", prov.BridgeStatePing).Methods(http.MethodPost) prov.bridge.AS.Router.HandleFunc("/_matrix/app/com.beeper.bridge_state", prov.BridgeStatePing).Methods(http.MethodPost) @@ -216,6 +220,84 @@ func (prov *ProvisioningAPI) SyncAppState(w http.ResponseWriter, r *http.Request } } +func (prov *ProvisioningAPI) ListContacts(w http.ResponseWriter, r *http.Request) { + if user := r.Context().Value("user").(*User); user.Session == nil { + jsonResponse(w, http.StatusBadRequest, Error{ + Error: "User is not logged into WhatsApp", + ErrCode: "no session", + }) + } else if contacts, err := user.Session.Contacts.GetAllContacts(); err != nil { + prov.log.Errorfln("Failed to fetch %s's contacts: %v", user.MXID, err) + jsonResponse(w, http.StatusInternalServerError, Error{ + Error: "Internal server error while fetching contact list", + ErrCode: "failed to get contacts", + }) + } else { + jsonResponse(w, http.StatusOK, contacts) + } +} + +type OtherUserInfo struct { + MXID id.UserID `json:"mxid"` + JID types.JID `json:"jid"` + Name string `json:"displayname"` + Avatar id.ContentURI `json:"avatar_url"` +} + +type PortalInfo struct { + RoomID id.RoomID `json:"room_id"` + OtherUser OtherUserInfo `json:"other_user"` + JustCreated bool `json:"just_created"` +} + +func (prov *ProvisioningAPI) StartPM(w http.ResponseWriter, r *http.Request) { + number, _ := mux.Vars(r)["number"] + if strings.HasSuffix(number, "@"+types.DefaultUserServer) { + jid, _ := types.ParseJID(number) + number = "+" + jid.User + } + if user := r.Context().Value("user").(*User); !user.IsLoggedIn() { + jsonResponse(w, http.StatusBadRequest, Error{ + Error: "User is not logged into WhatsApp", + ErrCode: "no session", + }) + } else if resp, err := user.Client.IsOnWhatsApp([]string{number}); err != nil { + jsonResponse(w, http.StatusInternalServerError, Error{ + Error: fmt.Sprintf("Failed to check if number is on WhatsApp: %v", err), + ErrCode: "error checking number", + }) + } else if len(resp) == 0 { + jsonResponse(w, http.StatusInternalServerError, Error{ + Error: "Didn't get a response to checking if the number is on WhatsApp", + ErrCode: "error checking number", + }) + } else if !resp[0].IsIn { + jsonResponse(w, http.StatusNotFound, Error{ + Error: fmt.Sprintf("The server said +%s is not on WhatsApp", resp[0].JID.User), + ErrCode: "not on whatsapp", + }) + } else if portal, puppet, justCreated, err := user.StartPM(resp[0].JID, "provisioning API PM"); err != nil { + jsonResponse(w, http.StatusInternalServerError, Error{ + Error: fmt.Sprintf("Failed to create portal: %v", err), + }) + } else { + status := http.StatusOK + if justCreated { + status = http.StatusCreated + } + jsonResponse(w, status, PortalInfo{ + RoomID: portal.MXID, + OtherUser: OtherUserInfo{ + JID: puppet.JID, + MXID: puppet.MXID, + Name: puppet.Displayname, + Avatar: puppet.AvatarURL, + }, + JustCreated: justCreated, + }) + } +} + func (prov *ProvisioningAPI) Ping(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("user").(*User) wa := map[string]interface{}{ diff --git a/user.go b/user.go index 80331cd..7342ddf 100644 --- a/user.go +++ b/user.go @@ -1035,3 +1035,21 @@ func (user *User) handlePictureUpdate(evt *events.Picture) { } } } + +func (user *User) StartPM(jid types.JID, reason string) (*Portal, *Puppet, bool, error) { + user.log.Debugln("Starting PM with", jid, "from", reason) + puppet := user.bridge.GetPuppetByJID(jid) + puppet.SyncContact(user, true, reason) + portal := user.GetPortalByJID(puppet.JID) + if len(portal.MXID) > 0 { + ok := portal.ensureUserInvited(user) + if !ok { + portal.log.Warnfln("ensureUserInvited(%s) returned false, creating new portal", user.MXID) + portal.MXID = "" + } else { + return portal, puppet, false, nil + } + } + err := portal.CreateMatrixRoom(user, nil, false) + return portal, puppet, true, err +}