Add provisioning API endpoints to list contacts and start chats

This commit is contained in:
Tulir Asokan 2022-02-17 15:14:53 +02:00
parent 1a1fd68812
commit 66d0817081
5 changed files with 109 additions and 21 deletions

View file

@ -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.`

2
go.mod
View file

@ -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

4
go.sum
View file

@ -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=

View file

@ -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
user.go
View file

@ -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
}