mirror of
https://github.com/mautrix/whatsapp.git
synced 2025-03-14 14:15:38 +00:00
legacyprovision: add backwards-compatible login and SNC endpoints
This commit is contained in:
parent
d95934cbc0
commit
1ef02aa54e
6 changed files with 300 additions and 11 deletions
271
cmd/mautrix-whatsapp/legacyprovision.go
Normal file
271
cmd/mautrix-whatsapp/legacyprovision.go
Normal 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,
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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
2
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue