legacyprovision: add backwards-compatible login and SNC endpoints

This commit is contained in:
Tulir Asokan 2024-09-11 15:33:41 +03:00
parent d95934cbc0
commit 1ef02aa54e
6 changed files with 300 additions and 11 deletions

View file

@ -0,0 +1,271 @@
package main
import (
"context"
"errors"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/hlog"
"go.mau.fi/util/exhttp"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/matrix"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/pkg/connector"
"maunium.net/go/mautrix-whatsapp/pkg/waid"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
Subprotocols: []string{"net.maunium.whatsapp.login"},
}
func legacyProvAuth(r *http.Request) string {
if !strings.HasSuffix(r.URL.Path, "/v1/login") {
return ""
}
authParts := strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ",")
for _, part := range authParts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "net.maunium.whatsapp.auth-") {
return strings.TrimPrefix(part, "net.maunium.whatsapp.auth-")
}
}
return ""
}
type OtherUserInfo struct {
MXID id.UserID `json:"mxid"`
JID types.JID `json:"jid"`
Name string `json:"displayname"`
Avatar id.ContentURIString `json:"avatar_url"`
}
type PortalInfo struct {
RoomID id.RoomID `json:"room_id"`
OtherUser *OtherUserInfo `json:"other_user,omitempty"`
GroupInfo *types.GroupInfo `json:"group_info,omitempty"`
JustCreated bool `json:"just_created"`
}
type Error struct {
Success bool `json:"success"`
Error string `json:"error"`
ErrCode string `json:"errcode"`
}
type Response struct {
Success bool `json:"success"`
Status string `json:"status"`
}
func respondWebsocketWithError(conn *websocket.Conn, err error, message string) {
var mautrixRespErr mautrix.RespError
var bv2RespErr bridgev2.RespError
if errors.As(err, &bv2RespErr) {
mautrixRespErr = mautrix.RespError(bv2RespErr)
} else if !errors.As(err, &mautrixRespErr) {
mautrixRespErr = mautrix.RespError{
Err: message,
ErrCode: "M_UNKNOWN",
StatusCode: http.StatusInternalServerError,
}
}
_ = conn.WriteJSON(&mautrixRespErr)
}
func legacyProvLogin(w http.ResponseWriter, r *http.Request) {
log := hlog.FromRequest(r)
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Err(err).Msg("Failed to upgrade connection to websocket")
return
}
defer func() {
err := conn.Close()
if err != nil {
log.Debug().Err(err).Msg("Error closing websocket")
}
}()
go func() {
// Read everything so SetCloseHandler() works
for {
_, _, err = conn.ReadMessage()
if err != nil {
break
}
}
}()
ctx, cancel := context.WithCancel(context.Background())
conn.SetCloseHandler(func(code int, text string) error {
log.Debug().Int("close_code", code).Msg("Login websocket closed, cancelling login")
cancel()
return nil
})
//if userTimezone := r.URL.Query().Get("tz"); userTimezone != "" {
// log.Debug().Str("timezone", userTimezone).Msg("Updating user timezone")
// user.Timezone = userTimezone
// err = user.Update(r.Context())
// if err != nil {
// log.Err(err).Msg("Failed to save user after updating timezone")
// }
//} else {
// log.Debug().Msg("No timezone provided in request")
//}
user := m.Matrix.Provisioning.GetUser(r)
loginFlowID := connector.LoginFlowIDQR
phoneNum := r.URL.Query().Get("phone_number")
if phoneNum != "" {
loginFlowID = connector.LoginFlowIDPhone
}
login, err := c.CreateLogin(ctx, user, loginFlowID)
if err != nil {
log.Err(err).Msg("Failed to create login")
respondWebsocketWithError(conn, err, "Failed to create login")
return
}
waLogin := login.(*connector.WALogin)
step, err := waLogin.Start(ctx)
if err != nil {
log.Err(err).Msg("Failed to start login")
respondWebsocketWithError(conn, err, "Failed to start login")
return
}
if phoneNum != "" {
if step.StepID != connector.LoginStepIDPhoneNumber {
respondWebsocketWithError(conn, errors.New("unexpected step"), "Unexpected step while starting phone number login")
return
}
step, err = waLogin.SubmitUserInput(ctx, map[string]string{"phone": phoneNum})
if err != nil {
log.Err(err).Msg("Failed to submit phone number")
respondWebsocketWithError(conn, err, "Failed to start phone code login")
return
} else if step.StepID != connector.LoginStepIDCode {
respondWebsocketWithError(conn, errors.New("unexpected step"), "Unexpected step after submitting phone number")
return
}
_ = conn.WriteJSON(map[string]any{
"pairing_code": step.DisplayAndWaitParams.Data,
"timeout": 180,
})
} else if step.StepID != connector.LoginStepIDQR {
respondWebsocketWithError(conn, errors.New("unexpected step"), "Unexpected step while starting QR login")
return
} else {
_ = conn.WriteJSON(map[string]any{
"qr_code": step.DisplayAndWaitParams.Data,
"timeout": 60,
})
}
for {
step, err = waLogin.Wait(ctx)
if err != nil {
log.Err(err).Msg("Failed to wait for login")
respondWebsocketWithError(conn, err, "Failed to wait for login")
} else if step.StepID == connector.LoginStepIDQR {
_ = conn.WriteJSON(map[string]any{
"qr_code": step.DisplayAndWaitParams.Data,
"timeout": 20,
})
continue
} else if step.StepID != connector.LoginStepIDComplete {
respondWebsocketWithError(conn, errors.New("unexpected step"), "Unexpected step while waiting for login")
} else {
// TODO delete old logins
_ = conn.WriteJSON(map[string]any{
"success": true,
"jid": waid.ParseUserLoginID(step.CompleteParams.UserLoginID, step.CompleteParams.UserLogin.Metadata.(*connector.UserLoginMetadata).WADeviceID).String(),
"platform": step.CompleteParams.UserLogin.Client.(*connector.WhatsAppClient).Device.Platform,
"phone": step.CompleteParams.UserLogin.RemoteProfile.Phone,
})
}
break
}
}
func legacyProvLogout(w http.ResponseWriter, r *http.Request) {
user := m.Matrix.Provisioning.GetUser(r)
allLogins := user.GetUserLogins()
if len(allLogins) == 0 {
exhttp.WriteJSONResponse(w, http.StatusOK, Error{
Error: "You're not logged in",
ErrCode: "not logged in",
})
return
}
for _, login := range allLogins {
login.Logout(r.Context())
}
exhttp.WriteJSONResponse(w, http.StatusOK, Response{true, "Logged out successfully"})
}
func legacyProvContacts(w http.ResponseWriter, r *http.Request) {
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if userLogin == nil {
return
}
if contacts, err := userLogin.Client.(*connector.WhatsAppClient).Device.Contacts.GetAllContacts(); err != nil {
hlog.FromRequest(r).Err(err).Msg("Failed to fetch all contacts")
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, Error{
Error: "Internal server error while fetching contact list",
ErrCode: "failed to get contacts",
})
} else {
augmentedContacts := map[types.JID]any{}
for jid, contact := range contacts {
var avatarURL id.ContentURIString
if puppet, _ := m.Bridge.GetExistingGhostByID(r.Context(), waid.MakeUserID(jid)); puppet != nil {
avatarURL = puppet.AvatarMXC
}
augmentedContacts[jid] = map[string]interface{}{
"Found": contact.Found,
"FirstName": contact.FirstName,
"FullName": contact.FullName,
"PushName": contact.PushName,
"BusinessName": contact.BusinessName,
"AvatarURL": avatarURL,
}
}
exhttp.WriteJSONResponse(w, http.StatusOK, augmentedContacts)
}
}
func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
number := mux.Vars(r)["number"]
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if userLogin == nil {
return
}
startChat := strings.Contains(r.URL.Path, "/v1/pm/")
resp, err := userLogin.Client.(*connector.WhatsAppClient).ResolveIdentifier(r.Context(), number, startChat)
if err != nil {
hlog.FromRequest(r).Warn().Err(err).Str("identifier", number).Msg("Failed to resolve identifier")
matrix.RespondWithError(w, err, "Internal error resolving identifier")
return
}
portal, _ := m.Bridge.GetExistingPortalByKey(r.Context(), resp.Chat.PortalKey)
var roomID id.RoomID
if portal != nil {
roomID = portal.MXID
}
exhttp.WriteJSONResponse(w, http.StatusOK, PortalInfo{
RoomID: roomID,
OtherUser: &OtherUserInfo{
JID: waid.ParseUserID(resp.UserID),
MXID: resp.Ghost.Intent.GetMXID(),
Name: resp.Ghost.Name,
Avatar: resp.Ghost.AvatarMXC,
},
})
}

View file

@ -1,6 +1,8 @@
package main
import (
"net/http"
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
@ -39,6 +41,16 @@ func main() {
true,
)
}
m.PostStart = func() {
if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.Router.HandleFunc("/v1/login", legacyProvLogin).Methods(http.MethodGet)
m.Matrix.Provisioning.Router.HandleFunc("/v1/logout", legacyProvLogout).Methods(http.MethodPost)
m.Matrix.Provisioning.Router.HandleFunc("/v1/contacts", legacyProvContacts).Methods(http.MethodPost)
m.Matrix.Provisioning.Router.HandleFunc("/v1/resolve_identifier/{number}", legacyProvResolveIdentifier).Methods(http.MethodGet)
m.Matrix.Provisioning.Router.HandleFunc("/v1/pm/{number}", legacyProvResolveIdentifier).Methods(http.MethodPost)
m.Matrix.Provisioning.GetAuthFromRequest = legacyProvAuth
}
}
m.InitVersion(Tag, Commit, BuildTime)
m.Run()
}

2
go.mod
View file

@ -20,7 +20,7 @@ require (
golang.org/x/sync v0.8.0
google.golang.org/protobuf v1.34.2
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.20.1-0.20240910112932-ffdb1d575e5f
maunium.net/go/mautrix v0.20.1-0.20240911105342-008836806673
)
require (

4
go.sum
View file

@ -114,5 +114,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.20.1-0.20240910112932-ffdb1d575e5f h1:T4K6V2Ige2mshH7FW69xLep23GBrW7xS1sxomaKe1Hg=
maunium.net/go/mautrix v0.20.1-0.20240910112932-ffdb1d575e5f/go.mod h1:l6nYvD5/FMSrAZ/IP1AqJV0b47SRl/0uQNRiy4CcSVk=
maunium.net/go/mautrix v0.20.1-0.20240911105342-008836806673 h1:/sQYbRh37iwNe4qV63PQ3PjiZBl7p+8F+YANPJ7tLW0=
maunium.net/go/mautrix v0.20.1-0.20240911105342-008836806673/go.mod h1:l6nYvD5/FMSrAZ/IP1AqJV0b47SRl/0uQNRiy4CcSVk=

View file

@ -100,19 +100,20 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) error {
}
func (wa *WhatsAppClient) Disconnect() {
if wa.Client != nil {
wa.Client.Disconnect()
if cli := wa.Client; cli != nil {
cli.Disconnect()
wa.Client = nil
}
}
func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) {
if wa.Client == nil {
return
}
err := wa.Client.Logout()
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to log out")
if cli := wa.Client; cli != nil {
err := cli.Logout()
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to log out")
}
}
wa.Disconnect()
}
func (wa *WhatsAppClient) IsLoggedIn() bool {

View file

@ -12,6 +12,7 @@ import (
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
@ -281,6 +282,10 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
ul, err := wl.User.NewLogin(ctx, &database.UserLogin{
ID: newLoginID,
RemoteName: "+" + wl.LoginSuccess.ID.User,
RemoteProfile: status.RemoteProfile{
Phone: "+" + wl.LoginSuccess.ID.User,
Name: wl.LoginSuccess.BusinessName,
},
Metadata: &UserLoginMetadata{
WADeviceID: wl.LoginSuccess.ID.Device,
},