diff --git a/commands.go b/commands.go index 9c6797a..f45ed28 100644 --- a/commands.go +++ b/commands.go @@ -65,6 +65,7 @@ func (br *MetaBridge) RegisterCommands() { cmdDeleteAllPortals, cmdDeleteThread, cmdSearch, + cmdRegisterPushNotifications, ) } @@ -282,6 +283,30 @@ func fnSyncSpace(ce *WrappedCommandEvent) { ce.Reply("Added %d room%s to space", count, plural) } +var cmdRegisterPushNotifications = &commands.FullHandler{ + Func: wrapCommand(fnRegisterForPushNotifications), + Name: "register-push-notifications", + Help: commands.HelpMeta{ + Section: commands.HelpSectionGeneral, + Description: "Register for push notifications", + Args: "", + }, +} + +func fnRegisterForPushNotifications(ce *WrappedCommandEvent) { + endpoint := ce.Args[0] + var err error + if ce.User.Cookies.Platform.IsMessenger() { + err = ce.User.Client.Facebook.RegisterPushNotifications(endpoint) + } else { + err = ce.User.Client.Instagram.RegisterPushNotifications(endpoint) + } + if err != nil { + ce.Reply("failed to register for push notifications") + } + ce.Reply("successfully registered for push notifications") +} + var cmdLogin = &commands.FullHandler{ Func: wrapCommand(fnLogin), Name: "login", diff --git a/messagix/cookies/cookies.go b/messagix/cookies/cookies.go index 187ee53..cec0ab8 100644 --- a/messagix/cookies/cookies.go +++ b/messagix/cookies/cookies.go @@ -50,6 +50,7 @@ type Cookies struct { values map[MetaCookieName]string lock sync.RWMutex + PushKeys *PushKeys IGWWWClaim string } @@ -81,6 +82,17 @@ func (c *Cookies) GetViewports() (width, height string) { return pxs[0], pxs[1] } +func (c *Cookies) GeneratePushKeys() error { + c.lock.RLock() + defer c.lock.RUnlock() + pushKeys, err := generatePushKeys() + if err != nil { + return err + } + c.PushKeys = pushKeys + return nil +} + func (c *Cookies) GetMissingCookieNames() []MetaCookieName { c.lock.RLock() defer c.lock.RUnlock() diff --git a/messagix/cookies/pushkeys.go b/messagix/cookies/pushkeys.go new file mode 100644 index 0000000..932caf0 --- /dev/null +++ b/messagix/cookies/pushkeys.go @@ -0,0 +1,47 @@ +package cookies + +import ( + "crypto/ecdh" + "crypto/rand" + "encoding/base64" +) + +type PushKeysPublic struct { + P256dh string `json:"p256dh"` + Auth string `json:"auth"` +} + +type PushKeys struct { + Public PushKeysPublic + Private string +} + +func generatePushKeys() (*PushKeys, error) { + curve := ecdh.P256() + privateKey, err := curve.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + + publicKey := privateKey.Public().(*ecdh.PublicKey) + encodedPublicKey := base64.URLEncoding.EncodeToString(publicKey.Bytes()) + + privateKeyBytes := privateKey.Bytes() + encodedPrivateKey := base64.URLEncoding.EncodeToString(privateKeyBytes) + + auth := make([]byte, 16) + _, err = rand.Read(auth) + if err != nil { + return nil, err + } + encodedAuth := base64.URLEncoding.EncodeToString(auth) + + pushKeys := &PushKeys{ + Public: PushKeysPublic{ + P256dh: encodedPublicKey, + Auth: encodedAuth, + }, + Private: encodedPrivateKey, + } + return pushKeys, nil +} diff --git a/messagix/data/endpoints/facebook.go b/messagix/data/endpoints/facebook.go index b2464f1..d724f50 100644 --- a/messagix/data/endpoints/facebook.go +++ b/messagix/data/endpoints/facebook.go @@ -21,6 +21,7 @@ func makeFacebookEndpoints(host string) map[string]string { "cookie_consent": baseURL + "/cookie/consent/", "graphql": baseURL + "/api/graphql/", "media_upload": baseURL + "/ajax/mercury/upload.php?", + "web_push": baseURL + "/push/register/service_worker/", "icdc_fetch": "https://reg-e2ee.facebook.com/v2/fb_icdc_fetch", "icdc_register": "https://reg-e2ee.facebook.com/v2/fb_register_v2", diff --git a/messagix/data/endpoints/instagram.go b/messagix/data/endpoints/instagram.go index d78329e..f6c3b56 100644 --- a/messagix/data/endpoints/instagram.go +++ b/messagix/data/endpoints/instagram.go @@ -29,4 +29,5 @@ var InstagramEndpoints = map[string]string{ "web_profile_info": instaApiV1Url + "/users/web_profile_info/?", "reels_media": instaApiV1Url + "/feed/reels_media/?", "media_info": instaApiV1Url + "/media/%s/info/", + "web_push": instaApiV1Url + "/api/v1/web/push/register/", } diff --git a/messagix/facebook.go b/messagix/facebook.go index e557a89..845927a 100644 --- a/messagix/facebook.go +++ b/messagix/facebook.go @@ -1,9 +1,12 @@ package messagix import ( + "encoding/json" + "errors" "fmt" "reflect" "strconv" + "strings" "github.com/google/go-querystring/query" @@ -72,3 +75,67 @@ func (fb *FacebookMethods) Login(identifier, password string) (*cookies.Cookies, return fb.client.cookies, nil } + +func (fb *FacebookMethods) RegisterPushNotifications(endpoint string) error { + c := fb.client + jsonKeys, err := json.Marshal(c.cookies.PushKeys.Public) + if err != nil { + c.Logger.Err(err).Msg("failed to encode push keys to json") + return err + } + + payload := c.NewHttpQuery() + payload.AppID = "1443096165982425" + payload.PushEndpoint = endpoint + payload.SubscriptionKeys = string(jsonKeys) + + form, err := query.Values(payload) + if err != nil { + return err + } + + payloadBytes := []byte(form.Encode()) + + headers := c.buildHeaders(true) + headers.Set("Referer", c.getEndpoint("host")) + headers.Set("Sec-fetch-site", "same-origin") + + url := c.getEndpoint("web_push") + + resp, body, err := c.MakeRequest(url, "POST", headers, payloadBytes, types.FORM) + if err != nil { + return err + } + + if resp.StatusCode >= 300 || resp.StatusCode < 200 { + return fmt.Errorf("bad status code: %d", resp.StatusCode) + } + + bodyStr := string(body) + jsonStr := strings.TrimPrefix(bodyStr, "for (;;);") + jsonBytes := []byte(jsonStr) + + var r pushNotificationsResponse + err = json.Unmarshal(jsonBytes, &r) + if err != nil { + c.Logger.Err(err).Str("body", bodyStr).Msg("failed to unmarshal response") + return err + } + + if !r.Payload.Success { + return errors.New("failed to register for push notifications") + } + + return nil +} + +type pushNotificationsResponse struct { + Ar int `json:"__ar"` + Payload payload `json:"payload"` + DtsgToken string `json:"dtsgToken"` + Lid string `json:"lid"` +} + +type payload struct { + Success bool `json:"success"` +} diff --git a/messagix/http.go b/messagix/http.go index cac8573..d0889a4 100644 --- a/messagix/http.go +++ b/messagix/http.go @@ -42,7 +42,13 @@ type HttpQuery struct { Variables string `url:"variables,omitempty"` ServerTimestamps string `url:"server_timestamps,omitempty"` // "true" or "false" DocID string `url:"doc_id,omitempty"` - D string `url:"__d,omitempty"` // for insta + D string `url:"__d,omitempty"` // for insta + AppID string `url:"app_id,omitempty"` // not required + PushEndpoint string `url:"push_endpoint,omitempty"` // not required + SubscriptionKeys string `url:"subscription_keys,omitempty"` // not required + DeviceToken string `url:"device_token,omitempty"` // not required + DeviceType string `url:"device_type,omitempty"` // not required + Mid string `url:"mid,omitempty"` // not required } func (c *Client) NewHttpQuery() *HttpQuery { diff --git a/messagix/instagram.go b/messagix/instagram.go index 794b83c..956ab99 100644 --- a/messagix/instagram.go +++ b/messagix/instagram.go @@ -2,11 +2,13 @@ package messagix import ( "encoding/json" + "errors" "fmt" "net/url" "strconv" "github.com/google/go-querystring/query" + "github.com/google/uuid" "go.mau.fi/mautrix-meta/messagix/cookies" "go.mau.fi/mautrix-meta/messagix/crypto" @@ -175,3 +177,53 @@ func (ig *InstagramMethods) FetchReel(reelIds []string, mediaID string) (*respon func (ig *InstagramMethods) FetchHighlights(highlightIds []string) (*responses.ReelInfoResponse, error) { return ig.FetchReel(highlightIds, "") } + +func (ig *InstagramMethods) RegisterPushNotifications(endpoint string) error { + c := ig.client + + jsonKeys, err := json.Marshal(c.cookies.PushKeys.Public) + if err != nil { + c.Logger.Err(err).Msg("failed to encode push keys to json") + return err + } + + u := uuid.New() + payload := c.NewHttpQuery() + payload.Mid = u.String() + payload.DeviceType = "web_vapid" + payload.DeviceToken = endpoint + payload.SubscriptionKeys = string(jsonKeys) + + form, err := query.Values(payload) + if err != nil { + return err + } + + payloadBytes := []byte(form.Encode()) + + headers := c.buildHeaders(true) + headers.Set("x-requested-with", "XMLHttpRequest") + headers.Set("Referer", c.getEndpoint("host")) + headers.Set("Referrer-Policy", "strict-origin-when-cross-origin") + + url := c.getEndpoint("web_push") + resp, body, err := c.MakeRequest(url, "POST", headers, payloadBytes, types.FORM) + if err != nil { + return err + } + + if resp.StatusCode >= 300 || resp.StatusCode < 200 { + return fmt.Errorf("bad status code: %d", resp.StatusCode) + } + + resBody := &struct { + Status string `json:"status"` + }{} + + err = json.Unmarshal(body, resBody) + if err != nil { + return errors.New("failed to decode response payload, not subscribed to push notifications") + } + + return nil +} diff --git a/user.go b/user.go index 415f51f..98fd54f 100644 --- a/user.go +++ b/user.go @@ -560,7 +560,13 @@ func (user *User) unlockedConnect() { func (user *User) Login(ctx context.Context, cookies *cookies.Cookies) error { user.Lock() defer user.Unlock() - err := user.unlockedConnectWithCookies(cookies) + + err := cookies.GeneratePushKeys() + if err != nil { + return err + } + + err = user.unlockedConnectWithCookies(cookies) if err != nil { return err }