Browse Source

Add provisioning API endpoints to list contacts and start chats

Tulir Asokan 3 years ago
parent
commit
66d0817081
5 changed files with 109 additions and 21 deletions
  1. 5 17
      commands.go
  2. 1 1
      go.mod
  3. 2 2
      go.sum
  4. 83 1
      provisioning.go
  5. 18 0
      user.go

+ 5 - 17
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 <appstate/contacts/groups/space> [--create-portals] - Synchronize data from WhatsApp.`

+ 1 - 1
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

+ 2 - 2
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=

+ 83 - 1
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{}{

+ 18 - 0
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
+}