diff --git a/errors.go b/errors.go index 5d856b1..286f838 100644 --- a/errors.go +++ b/errors.go @@ -9,6 +9,14 @@ package pushreceiver import "github.com/pkg/errors" +type GCMError string + +func (err GCMError) Error() string { + return "GCM error: " + string(err) +} + +const ErrGCMDeviceRegistration = GCMError("PHONE_REGISTRATION_ERROR") + // ErrGcmAuthorization is authorization error of GCM. var ErrGcmAuthorization = errors.New("GCM authorization error") diff --git a/events.go b/events.go index e895759..c08a2d8 100644 --- a/events.go +++ b/events.go @@ -55,6 +55,10 @@ type MessageEvent struct { } func newMessageEvent(data *pb.DataMessageStanza) *MessageEvent { + appID := data.GetAppID() + if appID == "" { + appID = data.GetCategory() + } return &MessageEvent{ PersistentID: data.GetPersistentId(), From: data.GetFrom(), @@ -65,7 +69,7 @@ func newMessageEvent(data *pb.DataMessageStanza) *MessageEvent { Token: data.GetToken(), RegID: data.GetRegId(), RawData: data.GetRawData(), - AppID: data.GetAppID(), + AppID: appID, } } diff --git a/gcm.go b/gcm.go index 6d83a02..b43de72 100644 --- a/gcm.go +++ b/gcm.go @@ -27,6 +27,22 @@ type GCMRegistrationOpts struct { Expiry time.Duration } +func setGCMAppID(values url.Values, appID string) { + if len(appID) == 32 && strings.Trim(appID, "abcdefghijklmnop") == "" { + values.Set("app", appID) + } else { + values.Set("app", "org.chromium.linux") + values.Set("X-subtype", appID) + } +} + +func NewGCMAppID(authorizationEntity string) string { + if _, err := strconv.ParseUint(authorizationEntity, 10, 64); err != nil { + return "wp:" + strings.ToUpper(uuid.New().String()) + } + return uuid.NewString() +} + func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, opts *GCMRegistrationOpts) (*FCMCredentials, error) { values := url.Values{} @@ -34,11 +50,11 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede if opts != nil && opts.AppID != "" { appID = opts.AppID } else { - appID = "wp:" + strings.ToUpper(uuid.New().String()) + appID = NewGCMAppID(authorizationEntity) } if opts != nil && opts.InstanceID != "" { - values.Set("appId", opts.InstanceID) + values.Set("appid", opts.InstanceID) } if opts != nil && opts.Expiry != 0 { @@ -46,10 +62,9 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede values.Set("ttl", ttl) } - values.Set("app", "org.chromium.linux") + setGCMAppID(values, appID) values.Set("scope", "GCM") values.Set("X-scope", "GCM") - values.Set("X-subtype", appID) values.Set("device", fmt.Sprint(creds.AndroidID)) values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) values.Set("sender", authorizationEntity) @@ -61,18 +76,14 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede if err != nil { return nil, errors.Wrap(err, "request GCM register") } - defer closeResponse(res) - - data, err := io.ReadAll(res.Body) + subscription, err := parseGCMResponse(res) if err != nil { return nil, errors.Wrap(err, "read GCM register response") } - - subscription, err := url.ParseQuery(string(data)) - if err != nil { - return nil, errors.Wrap(err, "parse GCM register URL") - } token := subscription.Get("token") + if token == "" { + return nil, errors.New("GCM register response missing token") + } return &FCMCredentials{ GCM: creds, @@ -83,10 +94,9 @@ func RegisterGCM(ctx context.Context, authorizationEntity string, creds GCMCrede func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCredentials, appID string) error { values := url.Values{} - values.Set("app", "org.chromium.linux") + setGCMAppID(values, appID) values.Set("scope", "GCM") values.Set("X-scope", "GCM") - values.Set("X-subtype", appID) values.Set("device", fmt.Sprint(creds.AndroidID)) values.Set("gmsv", strings.Split(chromeVersion, ".")[0]) values.Set("sender", authorizationEntity) @@ -99,9 +109,32 @@ func UnregisterGCM(ctx context.Context, authorizationEntity string, creds GCMCre return errors.Wrap(err, "failed to unregister with GCM") } - if res.StatusCode != http.StatusOK { - return errors.New("failed to unregister with GCM") + response, err := parseGCMResponse(res) + if err != nil { + return errors.Wrap(err, "read GCM unregister response") + } + if response.Get("token") == "" && response.Get("deleted") != appID { + return errors.New("GCM unregister response missing confirmation") } return nil } + +func parseGCMResponse(res *http.Response) (url.Values, error) { + defer closeResponse(res) + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected HTTP status %d", res.StatusCode) + } + data, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + values, err := url.ParseQuery(string(data)) + if err != nil { + return nil, err + } + if message := values.Get("Error"); message != "" { + return nil, GCMError(message) + } + return values, nil +}