diff --git a/fixtures/envelopes.json b/fixtures/envelopes.json new file mode 100644 index 0000000..ae867bd --- /dev/null +++ b/fixtures/envelopes.json @@ -0,0 +1,19 @@ +[ + { + "kind": "v2-conversation", + "envelope": "0a502f786d74702f302f6d2d376433323232323932356239366361343339613862376230663933663231623661316563306665373966653432636136643534636164653135653263323765302f70726f746f10c0efebc8c295a3dc171af80412f5040a5c08c0efebc8c295a3dc1712502f786d74702f302f6d2d376433323232323932356239366361343339613862376230663933663231623661316563306665373966653432636136643534636164653135653263323765302f70726f746f12f0030aed030a201dee97828dbfed4768970bbd58b8445386994dcb5f9e4884f561aeca8fb37257120ca6c63e244cb94ec5522e4ca51aba032a46ba2cde18362bdd6e71b7bb3cae54d1a83a112d8087e804f0f23a392c81767def67a6bd96bf1869f71849cdc0f6a7c2ec76c411e6a0d097995feb834a258b1b0eafa873cdd81e722fd36c5ebfdc0c75eda606773db9d562eb1816123966d0d730947ff5003bce19ccc6c890254c7bd7afe7d2f3ebcb2561fc940dc99da9227d2372ac77030001fa12e303549f8bae0610fc8d133179adbce181dfb1fff630e9a9d8982d79323a688dac3ebd60f899b2a0a8614ce0e3a3feee81293638033b85f0495d84a3bd10e06c315c7016b662fe4bf277a9111adcda074a5f6a92f2108913be78d17ef982de8f8f6a261ec237e8c4e44964e297c79c8f3521d04436eafc46ed4f224156b152cff79787d1bb8b25c8b6d5cbee9262f8234067d4b7cea88b6c1b7caf23f8de9c7a6329449b19b45ed220d005fbc58cf76d6588b0ea352b257eba36e579b123c1c48b7e26846fad5e66b4059a18cecdab3d86eef0cd4a8f3343663aa0a924c1104ea78999a13b0f6ee6ede91dcdc94473f238d7e3dd6bd3e40fdab2f0dfb1d0b29d4a6ac1bbaaba14598f17aa40a7f8a1931ce79c481add157c73a9345986934868f9a3305171870780de1271b0a896e8a71a208acd62dae9ed5c2f7ecdbd9fbd1c14ccf0228af94cb8386f4dcd9ebace43ba952001", + "hmacKey": "ce3baf2fa9ce4ca617e30fa17f109f95ffd285101845d4f15b2a7b48b85e3779" + }, + { + "kind": "v1-conversation", + "envelope": "0a662f786d74702f302f646d2d3078354437653035304635386336413235326534624443456635313637383465614262363536313231332d3078383135324632313466363138316666316230343862664246303333314165393930354436646231332f70726f746f10c0ccb1fbc295a3dc171adc050ad9050ae5040aaa020a9201089983e6c3df3112440a420a4041237f75422fe52a5599edc50c1123167d814f5f6fa969bf7b486d07371101ef54dfec71ddef639be5cafb9717357a7e588a705e62eab8043ea9ae77e96bf08d1a430a4104365cf010eaa7f63338f2252595007ea69489b9cbf736526dd7b1fbbfccd9aa28dbc5e5569ac78f9be005aa002c6e81b40d68b1f93decc418342902e025c35134129201089b83e6c3df3112440a420a403a46f3fd19ef7b04babb050675595a1da83ae39126d0d5cdc220c07b0df942df602ddd730cdbacccc390d42e924a8e0360a07cabe3ba285af783f148cc7ab7d11a430a4104208becd14fe7226c47bb3c1bfac605e087a5eac5af1fd346c07f65c887df6dc90e1a377aecb95017461c14ff939a3af68cce7773b598257deba5d8045374795812ae020a940108c4ffe5c3df3112460a440a40501acd07ae8bf4068aa5f9144c8ba6b0fbfeea8cdb46b2ed13f435a03d1cced576d6afe6505b1697b2d8f97ee1643c0e913307d00ccf39f7be55cb6aa25fb42d10011a430a410428a877eb7c8257359f8a4b1ac10927fa2cb10b342cdae46147b0f60fe22c3daa7b2554a57f7fefeb5a8815560db9b36815cbfbc08826f1c60a12d90c0e189cc212940108e1ffe5c3df3112460a440a403734434965ed7cdb60a1d0a89454a39dbc44ac8c3396193e3cd2b1b2e04eb20360d0c9a7fced46ca9160b230f09e3571663540decea8d9495512629eb5123b5710011a430a4104a6651d9765917f7e28eb113ed9e3408cd1101702feb26df9d95a7bea9bb930ec44f86acf92e545e43f007e0e063356fa19f626e38b4ad7f86522206b24a11b7a189188e6c3df31126f0a6d0a20c0672d1e14e88770d4a61614ee9f2a797c3b9ee53bcd3ef521981999378c7120120c07621fdab3ac2cbc3e6f78231a3b46ce78ff9878222e08426000ccb37fdcd5507bf995148308e4f375167ecb2bb738997664a3957304bcc14848828c46b1d6be7b5de3beb8b4ebb955" + }, + { + "kind": "v2-invite", + "envelope": "0a3f2f786d74702f302f696e766974652d3078354437653035304635386336413235326534624443456635313637383465614262363536313231332f70726f746f10c0cca881c295a3dc171ab5060ab2060af0040ab2020a96010a4c08c4ffe5c3df311a430a410428a877eb7c8257359f8a4b1ac10927fa2cb10b342cdae46147b0f60fe22c3daa7b2554a57f7fefeb5a8815560db9b36815cbfbc08826f1c60a12d90c0e189cc2124612440a40501acd07ae8bf4068aa5f9144c8ba6b0fbfeea8cdb46b2ed13f435a03d1cced576d6afe6505b1697b2d8f97ee1643c0e913307d00ccf39f7be55cb6aa25fb42d10011296010a4c08e1ffe5c3df311a430a4104a6651d9765917f7e28eb113ed9e3408cd1101702feb26df9d95a7bea9bb930ec44f86acf92e545e43f007e0e063356fa19f626e38b4ad7f86522206b24a11b7a12460a440a403734434965ed7cdb60a1d0a89454a39dbc44ac8c3396193e3cd2b1b2e04eb20360d0c9a7fced46ca9160b230f09e3571663540decea8d9495512629eb5123b57100112ae020a94010a4c089983e6c3df311a430a4104365cf010eaa7f63338f2252595007ea69489b9cbf736526dd7b1fbbfccd9aa28dbc5e5569ac78f9be005aa002c6e81b40d68b1f93decc418342902e025c35134124412420a4041237f75422fe52a5599edc50c1123167d814f5f6fa969bf7b486d07371101ef54dfec71ddef639be5cafb9717357a7e588a705e62eab8043ea9ae77e96bf08d1294010a4c089b83e6c3df311a430a4104208becd14fe7226c47bb3c1bfac605e087a5eac5af1fd346c07f65c887df6dc90e1a377aecb95017461c14ff939a3af68cce7773b598257deba5d8045374795812440a420a403a46f3fd19ef7b04babb050675595a1da83ae39126d0d5cdc220c07b0df942df602ddd730cdbacccc390d42e924a8e0360a07cabe3ba285af783f148cc7ab7d118c0cca881c295a3dc1712bc010ab9010a200eea5c8214689486c512d4a6e1df7880ee6d670affb0e1a8ffe7c40f92078109120c350a743d91befd7da46255e01a8601fc82e6b54fcb1c55ebd59fb1abfc080427730b5e094bd0279948e50ed2de30beae92ecd58561686bb0d537d785881d393c9a5e8e9c2eba997f5181cee95551dc46893bcdfdfca2b71f2a3772607ffdc77e666154e43bfc59f8b8ed6704e7c7983ede18cf22f5fba59e67df11ec52ec33f8a076db9b11e610f2abf6977d7eb0af1bf58be53e2a" + }, + { + "kind": "v1-intro", + "envelope": "0a3e2f786d74702f302f696e74726f2d3078354437653035304635386336413235326534624443456635313637383465614262363536313231332f70726f746f10c0ccb1fbc295a3dc171adc050ad9050ae5040aaa020a9201089983e6c3df3112440a420a4041237f75422fe52a5599edc50c1123167d814f5f6fa969bf7b486d07371101ef54dfec71ddef639be5cafb9717357a7e588a705e62eab8043ea9ae77e96bf08d1a430a4104365cf010eaa7f63338f2252595007ea69489b9cbf736526dd7b1fbbfccd9aa28dbc5e5569ac78f9be005aa002c6e81b40d68b1f93decc418342902e025c35134129201089b83e6c3df3112440a420a403a46f3fd19ef7b04babb050675595a1da83ae39126d0d5cdc220c07b0df942df602ddd730cdbacccc390d42e924a8e0360a07cabe3ba285af783f148cc7ab7d11a430a4104208becd14fe7226c47bb3c1bfac605e087a5eac5af1fd346c07f65c887df6dc90e1a377aecb95017461c14ff939a3af68cce7773b598257deba5d8045374795812ae020a940108c4ffe5c3df3112460a440a40501acd07ae8bf4068aa5f9144c8ba6b0fbfeea8cdb46b2ed13f435a03d1cced576d6afe6505b1697b2d8f97ee1643c0e913307d00ccf39f7be55cb6aa25fb42d10011a430a410428a877eb7c8257359f8a4b1ac10927fa2cb10b342cdae46147b0f60fe22c3daa7b2554a57f7fefeb5a8815560db9b36815cbfbc08826f1c60a12d90c0e189cc212940108e1ffe5c3df3112460a440a403734434965ed7cdb60a1d0a89454a39dbc44ac8c3396193e3cd2b1b2e04eb20360d0c9a7fced46ca9160b230f09e3571663540decea8d9495512629eb5123b5710011a430a4104a6651d9765917f7e28eb113ed9e3408cd1101702feb26df9d95a7bea9bb930ec44f86acf92e545e43f007e0e063356fa19f626e38b4ad7f86522206b24a11b7a189188e6c3df31126f0a6d0a20c0672d1e14e88770d4a61614ee9f2a797c3b9ee53bcd3ef521981999378c7120120c07621fdab3ac2cbc3e6f78231a3b46ce78ff9878222e08426000ccb37fdcd5507bf995148308e4f375167ecb2bb738997664a3957304bcc14848828c46b1d6be7b5de3beb8b4ebb955" + } +] diff --git a/mocks/Installations.go b/mocks/Installations.go index 3bade5f..f588302 100644 --- a/mocks/Installations.go +++ b/mocks/Installations.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package mocks @@ -18,6 +18,10 @@ type Installations struct { func (_m *Installations) Delete(ctx context.Context, installationId string) error { ret := _m.Called(ctx, installationId) + if len(ret) == 0 { + panic("no return value specified for Delete") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { r0 = rf(ctx, installationId) @@ -32,7 +36,15 @@ func (_m *Installations) Delete(ctx context.Context, installationId string) erro func (_m *Installations) GetInstallations(ctx context.Context, installationIds []string) ([]interfaces.Installation, error) { ret := _m.Called(ctx, installationIds) + if len(ret) == 0 { + panic("no return value specified for GetInstallations") + } + var r0 []interfaces.Installation + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string) ([]interfaces.Installation, error)); ok { + return rf(ctx, installationIds) + } if rf, ok := ret.Get(0).(func(context.Context, []string) []interfaces.Installation); ok { r0 = rf(ctx, installationIds) } else { @@ -41,7 +53,6 @@ func (_m *Installations) GetInstallations(ctx context.Context, installationIds [ } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { r1 = rf(ctx, installationIds) } else { @@ -55,7 +66,15 @@ func (_m *Installations) GetInstallations(ctx context.Context, installationIds [ func (_m *Installations) Register(ctx context.Context, installation interfaces.Installation) (*interfaces.RegisterResponse, error) { ret := _m.Called(ctx, installation) + if len(ret) == 0 { + panic("no return value specified for Register") + } + var r0 *interfaces.RegisterResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, interfaces.Installation) (*interfaces.RegisterResponse, error)); ok { + return rf(ctx, installation) + } if rf, ok := ret.Get(0).(func(context.Context, interfaces.Installation) *interfaces.RegisterResponse); ok { r0 = rf(ctx, installation) } else { @@ -64,7 +83,6 @@ func (_m *Installations) Register(ctx context.Context, installation interfaces.I } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, interfaces.Installation) error); ok { r1 = rf(ctx, installation) } else { @@ -74,13 +92,12 @@ func (_m *Installations) Register(ctx context.Context, installation interfaces.I return r0, r1 } -type mockConstructorTestingTNewInstallations interface { +// NewInstallations creates a new instance of Installations. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewInstallations(t interface { mock.TestingT Cleanup(func()) -} - -// NewInstallations creates a new instance of Installations. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewInstallations(t mockConstructorTestingTNewInstallations) *Installations { +}) *Installations { mock := &Installations{} mock.Mock.Test(t) diff --git a/mocks/Subscriptions.go b/mocks/Subscriptions.go index f760591..4b36cff 100644 --- a/mocks/Subscriptions.go +++ b/mocks/Subscriptions.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. +// Code generated by mockery v2.38.0. DO NOT EDIT. package mocks @@ -14,22 +14,29 @@ type Subscriptions struct { mock.Mock } -// GetSubscriptions provides a mock function with given fields: ctx, topic -func (_m *Subscriptions) GetSubscriptions(ctx context.Context, topic string) ([]interfaces.Subscription, error) { - ret := _m.Called(ctx, topic) +// GetSubscriptions provides a mock function with given fields: ctx, topic, thirtyDayPeriod +func (_m *Subscriptions) GetSubscriptions(ctx context.Context, topic string, thirtyDayPeriod int) ([]interfaces.Subscription, error) { + ret := _m.Called(ctx, topic, thirtyDayPeriod) + + if len(ret) == 0 { + panic("no return value specified for GetSubscriptions") + } var r0 []interfaces.Subscription - if rf, ok := ret.Get(0).(func(context.Context, string) []interfaces.Subscription); ok { - r0 = rf(ctx, topic) + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, int) ([]interfaces.Subscription, error)); ok { + return rf(ctx, topic, thirtyDayPeriod) + } + if rf, ok := ret.Get(0).(func(context.Context, string, int) []interfaces.Subscription); ok { + r0 = rf(ctx, topic, thirtyDayPeriod) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]interfaces.Subscription) } } - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, topic) + if rf, ok := ret.Get(1).(func(context.Context, string, int) error); ok { + r1 = rf(ctx, topic, thirtyDayPeriod) } else { r1 = ret.Error(1) } @@ -37,10 +44,32 @@ func (_m *Subscriptions) GetSubscriptions(ctx context.Context, topic string) ([] return r0, r1 } +// SetHmacKeys provides a mock function with given fields: ctx, installationId, updates +func (_m *Subscriptions) SetHmacKeys(ctx context.Context, installationId string, updates interfaces.HmacUpdates) error { + ret := _m.Called(ctx, installationId, updates) + + if len(ret) == 0 { + panic("no return value specified for SetHmacKeys") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, interfaces.HmacUpdates) error); ok { + r0 = rf(ctx, installationId, updates) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Subscribe provides a mock function with given fields: ctx, installationId, topics func (_m *Subscriptions) Subscribe(ctx context.Context, installationId string, topics []string) error { ret := _m.Called(ctx, installationId, topics) + if len(ret) == 0 { + panic("no return value specified for Subscribe") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, []string) error); ok { r0 = rf(ctx, installationId, topics) @@ -51,10 +80,32 @@ func (_m *Subscriptions) Subscribe(ctx context.Context, installationId string, t return r0 } +// SubscribeWithMetadata provides a mock function with given fields: ctx, installationId, subscriptions +func (_m *Subscriptions) SubscribeWithMetadata(ctx context.Context, installationId string, subscriptions []interfaces.SubscriptionInput) error { + ret := _m.Called(ctx, installationId, subscriptions) + + if len(ret) == 0 { + panic("no return value specified for SubscribeWithMetadata") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, []interfaces.SubscriptionInput) error); ok { + r0 = rf(ctx, installationId, subscriptions) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // Unsubscribe provides a mock function with given fields: ctx, installationId, topics func (_m *Subscriptions) Unsubscribe(ctx context.Context, installationId string, topics []string) error { ret := _m.Called(ctx, installationId, topics) + if len(ret) == 0 { + panic("no return value specified for Unsubscribe") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, string, []string) error); ok { r0 = rf(ctx, installationId, topics) @@ -65,13 +116,12 @@ func (_m *Subscriptions) Unsubscribe(ctx context.Context, installationId string, return r0 } -type mockConstructorTestingTNewSubscriptions interface { +// NewSubscriptions creates a new instance of Subscriptions. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSubscriptions(t interface { mock.TestingT Cleanup(func()) -} - -// NewSubscriptions creates a new instance of Subscriptions. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSubscriptions(t mockConstructorTestingTNewSubscriptions) *Subscriptions { +}) *Subscriptions { mock := &Subscriptions{} mock.Mock.Test(t) diff --git a/pkg/api/api.go b/pkg/api/api.go index df2a140..d537e01 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -140,6 +140,10 @@ func (s *ApiServer) Unsubscribe( return connect.NewResponse(&emptypb.Empty{}), nil } +func (s *ApiServer) SubscribeWithMetadata(ctx context.Context, req *connect.Request[proto.SubscribeWithMetadataRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, nil +} + func convertDeliveryMechanism(mechanism *proto.DeliveryMechanism) *interfaces.DeliveryMechanism { if mechanism == nil { return nil @@ -152,3 +156,23 @@ func convertDeliveryMechanism(mechanism *proto.DeliveryMechanism) *interfaces.De return &interfaces.DeliveryMechanism{Kind: interfaces.FCM, Token: fcmToken} } } + +// func convertHmacUpdates(protoUpdates []*proto.Subscription_HmacKey) (interfaces.HmacUpdates, error) { +// out := make(interfaces.HmacUpdates) +// for _, update := range protoUpdates { +// if update == nil { +// continue +// } +// if _, exists := out[update.Topic]; exists { +// return nil, fmt.Errorf("duplicate topic: %s", update.Topic) +// } +// for _, key := range update.HmacKey { +// out[update.Topic] = append(out[update.Topic], interfaces.HmacKey{ +// ThirtyDayPeriodsSinceEpoch: int(key.ThirtyDayPeriodsSinceEpoch), +// Key: key.Key, +// }) +// } +// } + +// return out, nil +// } diff --git a/pkg/db/migrations/20240229061721_add-hmac-keys.down.sql b/pkg/db/migrations/20240229061721_add-hmac-keys.down.sql new file mode 100644 index 0000000..c3313c9 --- /dev/null +++ b/pkg/db/migrations/20240229061721_add-hmac-keys.down.sql @@ -0,0 +1,13 @@ +SET statement_timeout = 0; + +--bun:split + +DROP TABLE subscription_hmac_keys; + +--bun:split + +ALTER TABLE subscriptions DROP COLUMN is_silent; + +--bun:split + +DROP INDEX CONCURRENTLY IF EXISTS subscriptions_installation_id_topic_idx; diff --git a/pkg/db/migrations/20240229061721_add-hmac-keys.up.sql b/pkg/db/migrations/20240229061721_add-hmac-keys.up.sql new file mode 100644 index 0000000..d92eaba --- /dev/null +++ b/pkg/db/migrations/20240229061721_add-hmac-keys.up.sql @@ -0,0 +1,21 @@ +SET statement_timeout = 0; + +--bun:split + +CREATE TABLE subscription_hmac_keys ( + subscription_id INTEGER NOT NULL, + thirty_day_periods_since_epoch INTEGER NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + key BYTEA NOT NULL, + PRIMARY KEY (subscription_id, thirty_day_periods_since_epoch), + FOREIGN KEY (subscription_id) REFERENCES subscriptions(id) ON DELETE CASCADE +); + +--bun:split + +ALTER TABLE subscriptions ADD COLUMN is_silent BOOLEAN DEFAULT FALSE; + +--bun:split + +CREATE UNIQUE INDEX CONCURRENTLY subscriptions_installation_id_topic_idx ON subscriptions (installation_id, topic); diff --git a/pkg/db/models.go b/pkg/db/models.go index 2950b41..4c1f44a 100644 --- a/pkg/db/models.go +++ b/pkg/db/models.go @@ -31,9 +31,20 @@ type DeviceDeliveryMechanism struct { type Subscription struct { bun.BaseModel `bun:"table:subscriptions"` - Id int64 `bun:",pk,autoincrement"` - CreatedAt time.Time `bun:"created_at,notnull,default:current_timestamp"` - InstallationId string `bun:"installation_id,notnull"` - Topic string `bun:"topic,notnull"` - IsActive bool `bun:"is_active,notnull"` + Id int64 `bun:",pk,autoincrement"` + CreatedAt time.Time `bun:"created_at,notnull"` + InstallationId string `bun:"installation_id,notnull"` + Topic string `bun:"topic,notnull"` + IsActive bool `bun:"is_active,notnull"` + IsSilent bool `bun:"is_silent,notnull"` + HmacKeys []*SubscriptionHmacKeys `bun:"rel:has-many,join:id=subscription_id"` +} + +type SubscriptionHmacKeys struct { + bun.BaseModel `bun:"table:subscription_hmac_keys"` + SubscriptionId int64 `bun:"subscription_id,notnull,pk"` + ThirtyDayPeriodsSinceEpoch int32 `bun:"thirty_day_periods_since_epoch,notnull,pk"` + Key []byte `bun:"key,notnull,type:bytea"` + CreatedAt time.Time `bun:"created_at,notnull"` + UpdatedAt time.Time `bun:"updated_at,notnull,default:current_timestamp"` } diff --git a/pkg/interfaces/interfaces.go b/pkg/interfaces/interfaces.go index 66cb074..4197ba4 100644 --- a/pkg/interfaces/interfaces.go +++ b/pkg/interfaces/interfaces.go @@ -41,6 +41,8 @@ type Subscription struct { InstallationId string Topic string IsActive bool + IsSilent bool + HmacKey *HmacKey } type SendRequest struct { @@ -49,7 +51,22 @@ type SendRequest struct { Message *v1.Envelope } +type HmacKey struct { + ThirtyDayPeriodsSinceEpoch int + Key []byte +} + +type SubscriptionInput struct { + Topic string + IsSilent bool + HmacKeys []HmacKey +} + +type HmacUpdates map[string][]HmacKey + // Pluggable Installation Service interface +// +//go:generate mockery --dir ../interfaces --name Installations --output ../../mocks --outpkg mocks type Installations interface { Register(ctx context.Context, installation Installation) (*RegisterResponse, error) Delete(ctx context.Context, installationId string) error @@ -57,10 +74,13 @@ type Installations interface { } // This interface is not expected to be pluggable +// +//go:generate mockery --dir ../interfaces --name Subscriptions --output ../../mocks --outpkg mocks type Subscriptions interface { Subscribe(ctx context.Context, installationId string, topics []string) error Unsubscribe(ctx context.Context, installationId string, topics []string) error - GetSubscriptions(ctx context.Context, topic string) ([]Subscription, error) + GetSubscriptions(ctx context.Context, topic string, thirtyDayPeriod int) ([]Subscription, error) + SubscribeWithMetadata(ctx context.Context, installationId string, subscriptions []SubscriptionInput) error } // Pluggable interface for sending push notifications diff --git a/pkg/proto/notifications/v1/notificationsv1connect/service.connect.go b/pkg/proto/notifications/v1/notificationsv1connect/service.connect.go index 91e94a4..26a8bfd 100644 --- a/pkg/proto/notifications/v1/notificationsv1connect/service.connect.go +++ b/pkg/proto/notifications/v1/notificationsv1connect/service.connect.go @@ -42,6 +42,9 @@ const ( NotificationsDeleteInstallationProcedure = "/notifications.v1.Notifications/DeleteInstallation" // NotificationsSubscribeProcedure is the fully-qualified name of the Notifications's Subscribe RPC. NotificationsSubscribeProcedure = "/notifications.v1.Notifications/Subscribe" + // NotificationsSubscribeWithMetadataProcedure is the fully-qualified name of the Notifications's + // SubscribeWithMetadata RPC. + NotificationsSubscribeWithMetadataProcedure = "/notifications.v1.Notifications/SubscribeWithMetadata" // NotificationsUnsubscribeProcedure is the fully-qualified name of the Notifications's Unsubscribe // RPC. NotificationsUnsubscribeProcedure = "/notifications.v1.Notifications/Unsubscribe" @@ -49,11 +52,12 @@ const ( // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. var ( - notificationsServiceDescriptor = v1.File_notifications_v1_service_proto.Services().ByName("Notifications") - notificationsRegisterInstallationMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("RegisterInstallation") - notificationsDeleteInstallationMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("DeleteInstallation") - notificationsSubscribeMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("Subscribe") - notificationsUnsubscribeMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("Unsubscribe") + notificationsServiceDescriptor = v1.File_notifications_v1_service_proto.Services().ByName("Notifications") + notificationsRegisterInstallationMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("RegisterInstallation") + notificationsDeleteInstallationMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("DeleteInstallation") + notificationsSubscribeMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("Subscribe") + notificationsSubscribeWithMetadataMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("SubscribeWithMetadata") + notificationsUnsubscribeMethodDescriptor = notificationsServiceDescriptor.Methods().ByName("Unsubscribe") ) // NotificationsClient is a client for the notifications.v1.Notifications service. @@ -61,6 +65,7 @@ type NotificationsClient interface { RegisterInstallation(context.Context, *connect.Request[v1.RegisterInstallationRequest]) (*connect.Response[v1.RegisterInstallationResponse], error) DeleteInstallation(context.Context, *connect.Request[v1.DeleteInstallationRequest]) (*connect.Response[emptypb.Empty], error) Subscribe(context.Context, *connect.Request[v1.SubscribeRequest]) (*connect.Response[emptypb.Empty], error) + SubscribeWithMetadata(context.Context, *connect.Request[v1.SubscribeWithMetadataRequest]) (*connect.Response[emptypb.Empty], error) Unsubscribe(context.Context, *connect.Request[v1.UnsubscribeRequest]) (*connect.Response[emptypb.Empty], error) } @@ -92,6 +97,12 @@ func NewNotificationsClient(httpClient connect.HTTPClient, baseURL string, opts connect.WithSchema(notificationsSubscribeMethodDescriptor), connect.WithClientOptions(opts...), ), + subscribeWithMetadata: connect.NewClient[v1.SubscribeWithMetadataRequest, emptypb.Empty]( + httpClient, + baseURL+NotificationsSubscribeWithMetadataProcedure, + connect.WithSchema(notificationsSubscribeWithMetadataMethodDescriptor), + connect.WithClientOptions(opts...), + ), unsubscribe: connect.NewClient[v1.UnsubscribeRequest, emptypb.Empty]( httpClient, baseURL+NotificationsUnsubscribeProcedure, @@ -103,10 +114,11 @@ func NewNotificationsClient(httpClient connect.HTTPClient, baseURL string, opts // notificationsClient implements NotificationsClient. type notificationsClient struct { - registerInstallation *connect.Client[v1.RegisterInstallationRequest, v1.RegisterInstallationResponse] - deleteInstallation *connect.Client[v1.DeleteInstallationRequest, emptypb.Empty] - subscribe *connect.Client[v1.SubscribeRequest, emptypb.Empty] - unsubscribe *connect.Client[v1.UnsubscribeRequest, emptypb.Empty] + registerInstallation *connect.Client[v1.RegisterInstallationRequest, v1.RegisterInstallationResponse] + deleteInstallation *connect.Client[v1.DeleteInstallationRequest, emptypb.Empty] + subscribe *connect.Client[v1.SubscribeRequest, emptypb.Empty] + subscribeWithMetadata *connect.Client[v1.SubscribeWithMetadataRequest, emptypb.Empty] + unsubscribe *connect.Client[v1.UnsubscribeRequest, emptypb.Empty] } // RegisterInstallation calls notifications.v1.Notifications.RegisterInstallation. @@ -124,6 +136,11 @@ func (c *notificationsClient) Subscribe(ctx context.Context, req *connect.Reques return c.subscribe.CallUnary(ctx, req) } +// SubscribeWithMetadata calls notifications.v1.Notifications.SubscribeWithMetadata. +func (c *notificationsClient) SubscribeWithMetadata(ctx context.Context, req *connect.Request[v1.SubscribeWithMetadataRequest]) (*connect.Response[emptypb.Empty], error) { + return c.subscribeWithMetadata.CallUnary(ctx, req) +} + // Unsubscribe calls notifications.v1.Notifications.Unsubscribe. func (c *notificationsClient) Unsubscribe(ctx context.Context, req *connect.Request[v1.UnsubscribeRequest]) (*connect.Response[emptypb.Empty], error) { return c.unsubscribe.CallUnary(ctx, req) @@ -134,6 +151,7 @@ type NotificationsHandler interface { RegisterInstallation(context.Context, *connect.Request[v1.RegisterInstallationRequest]) (*connect.Response[v1.RegisterInstallationResponse], error) DeleteInstallation(context.Context, *connect.Request[v1.DeleteInstallationRequest]) (*connect.Response[emptypb.Empty], error) Subscribe(context.Context, *connect.Request[v1.SubscribeRequest]) (*connect.Response[emptypb.Empty], error) + SubscribeWithMetadata(context.Context, *connect.Request[v1.SubscribeWithMetadataRequest]) (*connect.Response[emptypb.Empty], error) Unsubscribe(context.Context, *connect.Request[v1.UnsubscribeRequest]) (*connect.Response[emptypb.Empty], error) } @@ -161,6 +179,12 @@ func NewNotificationsHandler(svc NotificationsHandler, opts ...connect.HandlerOp connect.WithSchema(notificationsSubscribeMethodDescriptor), connect.WithHandlerOptions(opts...), ) + notificationsSubscribeWithMetadataHandler := connect.NewUnaryHandler( + NotificationsSubscribeWithMetadataProcedure, + svc.SubscribeWithMetadata, + connect.WithSchema(notificationsSubscribeWithMetadataMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) notificationsUnsubscribeHandler := connect.NewUnaryHandler( NotificationsUnsubscribeProcedure, svc.Unsubscribe, @@ -175,6 +199,8 @@ func NewNotificationsHandler(svc NotificationsHandler, opts ...connect.HandlerOp notificationsDeleteInstallationHandler.ServeHTTP(w, r) case NotificationsSubscribeProcedure: notificationsSubscribeHandler.ServeHTTP(w, r) + case NotificationsSubscribeWithMetadataProcedure: + notificationsSubscribeWithMetadataHandler.ServeHTTP(w, r) case NotificationsUnsubscribeProcedure: notificationsUnsubscribeHandler.ServeHTTP(w, r) default: @@ -198,6 +224,10 @@ func (UnimplementedNotificationsHandler) Subscribe(context.Context, *connect.Req return nil, connect.NewError(connect.CodeUnimplemented, errors.New("notifications.v1.Notifications.Subscribe is not implemented")) } +func (UnimplementedNotificationsHandler) SubscribeWithMetadata(context.Context, *connect.Request[v1.SubscribeWithMetadataRequest]) (*connect.Response[emptypb.Empty], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("notifications.v1.Notifications.SubscribeWithMetadata is not implemented")) +} + func (UnimplementedNotificationsHandler) Unsubscribe(context.Context, *connect.Request[v1.UnsubscribeRequest]) (*connect.Response[emptypb.Empty], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("notifications.v1.Notifications.Unsubscribe is not implemented")) } diff --git a/pkg/proto/notifications/v1/service.pb.go b/pkg/proto/notifications/v1/service.pb.go index 06ebe9a..bfefe7c 100644 --- a/pkg/proto/notifications/v1/service.pb.go +++ b/pkg/proto/notifications/v1/service.pb.go @@ -21,6 +21,7 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// An union of possible delibery mechanisms type DeliveryMechanism struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -102,6 +103,7 @@ func (*DeliveryMechanism_ApnsDeviceToken) isDeliveryMechanism_DeliveryMechanismT func (*DeliveryMechanism_FirebaseDeviceToken) isDeliveryMechanism_DeliveryMechanismType() {} +// A request to register an installation with the service type RegisterInstallationRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -157,6 +159,7 @@ func (x *RegisterInstallationRequest) GetDeliveryMechanism() *DeliveryMechanism return nil } +// Response to RegisterInstallationRequest type RegisterInstallationResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -212,6 +215,7 @@ func (x *RegisterInstallationResponse) GetValidUntil() uint64 { return 0 } +// Delete an installation from the service type DeleteInstallationRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -259,6 +263,127 @@ func (x *DeleteInstallationRequest) GetInstallationId() string { return "" } +// A subscription with associated metadata +type Subscription struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Topic string `protobuf:"bytes,1,opt,name=topic,proto3" json:"topic,omitempty"` + HmacKeys []*Subscription_HmacKey `protobuf:"bytes,2,rep,name=hmac_keys,json=hmacKeys,proto3" json:"hmac_keys,omitempty"` + IsSilent bool `protobuf:"varint,3,opt,name=is_silent,json=isSilent,proto3" json:"is_silent,omitempty"` +} + +func (x *Subscription) Reset() { + *x = Subscription{} + if protoimpl.UnsafeEnabled { + mi := &file_notifications_v1_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Subscription) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Subscription) ProtoMessage() {} + +func (x *Subscription) ProtoReflect() protoreflect.Message { + mi := &file_notifications_v1_service_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Subscription.ProtoReflect.Descriptor instead. +func (*Subscription) Descriptor() ([]byte, []int) { + return file_notifications_v1_service_proto_rawDescGZIP(), []int{4} +} + +func (x *Subscription) GetTopic() string { + if x != nil { + return x.Topic + } + return "" +} + +func (x *Subscription) GetHmacKeys() []*Subscription_HmacKey { + if x != nil { + return x.HmacKeys + } + return nil +} + +func (x *Subscription) GetIsSilent() bool { + if x != nil { + return x.IsSilent + } + return false +} + +// A request to subscribe to a list of topics and update the associated metadata +type SubscribeWithMetadataRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + InstallationId string `protobuf:"bytes,1,opt,name=installation_id,json=installationId,proto3" json:"installation_id,omitempty"` + Subscriptions []*Subscription `protobuf:"bytes,2,rep,name=subscriptions,proto3" json:"subscriptions,omitempty"` +} + +func (x *SubscribeWithMetadataRequest) Reset() { + *x = SubscribeWithMetadataRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_notifications_v1_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubscribeWithMetadataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeWithMetadataRequest) ProtoMessage() {} + +func (x *SubscribeWithMetadataRequest) ProtoReflect() protoreflect.Message { + mi := &file_notifications_v1_service_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscribeWithMetadataRequest.ProtoReflect.Descriptor instead. +func (*SubscribeWithMetadataRequest) Descriptor() ([]byte, []int) { + return file_notifications_v1_service_proto_rawDescGZIP(), []int{5} +} + +func (x *SubscribeWithMetadataRequest) GetInstallationId() string { + if x != nil { + return x.InstallationId + } + return "" +} + +func (x *SubscribeWithMetadataRequest) GetSubscriptions() []*Subscription { + if x != nil { + return x.Subscriptions + } + return nil +} + +// Subscribe to a list of topics type SubscribeRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -271,7 +396,7 @@ type SubscribeRequest struct { func (x *SubscribeRequest) Reset() { *x = SubscribeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_notifications_v1_service_proto_msgTypes[4] + mi := &file_notifications_v1_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -284,7 +409,7 @@ func (x *SubscribeRequest) String() string { func (*SubscribeRequest) ProtoMessage() {} func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_notifications_v1_service_proto_msgTypes[4] + mi := &file_notifications_v1_service_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -297,7 +422,7 @@ func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeRequest.ProtoReflect.Descriptor instead. func (*SubscribeRequest) Descriptor() ([]byte, []int) { - return file_notifications_v1_service_proto_rawDescGZIP(), []int{4} + return file_notifications_v1_service_proto_rawDescGZIP(), []int{6} } func (x *SubscribeRequest) GetInstallationId() string { @@ -314,6 +439,7 @@ func (x *SubscribeRequest) GetTopics() []string { return nil } +// Unsubscribe from a list of topics type UnsubscribeRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -326,7 +452,7 @@ type UnsubscribeRequest struct { func (x *UnsubscribeRequest) Reset() { *x = UnsubscribeRequest{} if protoimpl.UnsafeEnabled { - mi := &file_notifications_v1_service_proto_msgTypes[5] + mi := &file_notifications_v1_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -339,7 +465,7 @@ func (x *UnsubscribeRequest) String() string { func (*UnsubscribeRequest) ProtoMessage() {} func (x *UnsubscribeRequest) ProtoReflect() protoreflect.Message { - mi := &file_notifications_v1_service_proto_msgTypes[5] + mi := &file_notifications_v1_service_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -352,7 +478,7 @@ func (x *UnsubscribeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UnsubscribeRequest.ProtoReflect.Descriptor instead. func (*UnsubscribeRequest) Descriptor() ([]byte, []int) { - return file_notifications_v1_service_proto_rawDescGZIP(), []int{5} + return file_notifications_v1_service_proto_rawDescGZIP(), []int{7} } func (x *UnsubscribeRequest) GetInstallationId() string { @@ -369,6 +495,61 @@ func (x *UnsubscribeRequest) GetTopics() []string { return nil } +type Subscription_HmacKey struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ThirtyDayPeriodsSinceEpoch uint32 `protobuf:"varint,1,opt,name=thirty_day_periods_since_epoch,json=thirtyDayPeriodsSinceEpoch,proto3" json:"thirty_day_periods_since_epoch,omitempty"` + Key []byte `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` +} + +func (x *Subscription_HmacKey) Reset() { + *x = Subscription_HmacKey{} + if protoimpl.UnsafeEnabled { + mi := &file_notifications_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Subscription_HmacKey) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Subscription_HmacKey) ProtoMessage() {} + +func (x *Subscription_HmacKey) ProtoReflect() protoreflect.Message { + mi := &file_notifications_v1_service_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Subscription_HmacKey.ProtoReflect.Descriptor instead. +func (*Subscription_HmacKey) Descriptor() ([]byte, []int) { + return file_notifications_v1_service_proto_rawDescGZIP(), []int{4, 0} +} + +func (x *Subscription_HmacKey) GetThirtyDayPeriodsSinceEpoch() uint32 { + if x != nil { + return x.ThirtyDayPeriodsSinceEpoch + } + return 0 +} + +func (x *Subscription_HmacKey) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + var File_notifications_v1_service_proto protoreflect.FileDescriptor var file_notifications_v1_service_proto_rawDesc = []byte{ @@ -407,56 +588,85 @@ var file_notifications_v1_service_proto_rawDesc = []byte{ 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x64, 0x22, 0x53, 0x0a, 0x10, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, - 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x16, - 0x0a, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, - 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x22, 0x55, 0x0a, 0x12, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, - 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, + 0x64, 0x22, 0xe7, 0x01, 0x0a, 0x0c, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x12, 0x43, 0x0a, 0x09, 0x68, 0x6d, 0x61, 0x63, + 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6e, 0x6f, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x48, 0x6d, 0x61, 0x63, + 0x4b, 0x65, 0x79, 0x52, 0x08, 0x68, 0x6d, 0x61, 0x63, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x1b, 0x0a, + 0x09, 0x69, 0x73, 0x5f, 0x73, 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x08, 0x69, 0x73, 0x53, 0x69, 0x6c, 0x65, 0x6e, 0x74, 0x1a, 0x5f, 0x0a, 0x07, 0x48, 0x6d, + 0x61, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x42, 0x0a, 0x1e, 0x74, 0x68, 0x69, 0x72, 0x74, 0x79, 0x5f, + 0x64, 0x61, 0x79, 0x5f, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x73, 0x5f, 0x73, 0x69, 0x6e, 0x63, + 0x65, 0x5f, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x1a, 0x74, + 0x68, 0x69, 0x72, 0x74, 0x79, 0x44, 0x61, 0x79, 0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x73, 0x53, + 0x69, 0x6e, 0x63, 0x65, 0x45, 0x70, 0x6f, 0x63, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x8d, 0x01, 0x0a, 0x1c, + 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x57, 0x69, 0x74, 0x68, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x32, 0xf7, 0x02, - 0x0a, 0x0d, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, - 0x75, 0x0a, 0x14, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, - 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, - 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x6e, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x44, 0x0a, 0x0d, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, - 0x79, 0x12, 0x47, 0x0a, 0x09, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x22, - 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4b, 0x0a, 0x0b, 0x55, 0x6e, - 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x24, 0x2e, 0x6e, 0x6f, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0xe0, 0x01, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, + 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x75, + 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x53, 0x0a, 0x10, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x70, 0x69, + 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, + 0x22, 0x55, 0x0a, 0x12, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, + 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x32, 0xd8, 0x03, 0x0a, 0x0d, 0x4e, 0x6f, 0x74, 0x69, + 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x75, 0x0a, 0x14, 0x52, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x12, 0x2d, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2e, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x59, 0x0a, 0x12, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2b, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x47, 0x0a, 0x09, 0x53, + 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x22, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x73, + 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x12, 0x5f, 0x0a, 0x15, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, + 0x65, 0x57, 0x69, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2e, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, - 0x42, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, - 0x5a, 0x59, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x6d, 0x74, - 0x70, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 0x67, 0x6f, - 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x6e, 0x6f, 0x74, 0x69, - 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4e, 0x58, - 0x58, 0xaa, 0x02, 0x10, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x69, 0x6f, 0x6e, 0x73, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, - 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x57, 0x69, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x4b, 0x0a, 0x0b, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x12, 0x24, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, + 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x42, 0xe0, 0x01, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x42, 0x0c, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x59, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x6d, 0x74, 0x70, 0x2f, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x2d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2d, 0x67, 0x6f, 0x2f, 0x70, 0x6b, 0x67, 0x2f, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x3b, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4e, 0x58, 0x58, 0xaa, 0x02, 0x10, 0x4e, + 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x56, 0x31, 0xca, + 0x02, 0x10, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x5c, + 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0xea, 0x02, 0x11, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -471,31 +681,38 @@ func file_notifications_v1_service_proto_rawDescGZIP() []byte { return file_notifications_v1_service_proto_rawDescData } -var file_notifications_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_notifications_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_notifications_v1_service_proto_goTypes = []interface{}{ (*DeliveryMechanism)(nil), // 0: notifications.v1.DeliveryMechanism (*RegisterInstallationRequest)(nil), // 1: notifications.v1.RegisterInstallationRequest (*RegisterInstallationResponse)(nil), // 2: notifications.v1.RegisterInstallationResponse (*DeleteInstallationRequest)(nil), // 3: notifications.v1.DeleteInstallationRequest - (*SubscribeRequest)(nil), // 4: notifications.v1.SubscribeRequest - (*UnsubscribeRequest)(nil), // 5: notifications.v1.UnsubscribeRequest - (*emptypb.Empty)(nil), // 6: google.protobuf.Empty + (*Subscription)(nil), // 4: notifications.v1.Subscription + (*SubscribeWithMetadataRequest)(nil), // 5: notifications.v1.SubscribeWithMetadataRequest + (*SubscribeRequest)(nil), // 6: notifications.v1.SubscribeRequest + (*UnsubscribeRequest)(nil), // 7: notifications.v1.UnsubscribeRequest + (*Subscription_HmacKey)(nil), // 8: notifications.v1.Subscription.HmacKey + (*emptypb.Empty)(nil), // 9: google.protobuf.Empty } var file_notifications_v1_service_proto_depIdxs = []int32{ 0, // 0: notifications.v1.RegisterInstallationRequest.delivery_mechanism:type_name -> notifications.v1.DeliveryMechanism - 1, // 1: notifications.v1.Notifications.RegisterInstallation:input_type -> notifications.v1.RegisterInstallationRequest - 3, // 2: notifications.v1.Notifications.DeleteInstallation:input_type -> notifications.v1.DeleteInstallationRequest - 4, // 3: notifications.v1.Notifications.Subscribe:input_type -> notifications.v1.SubscribeRequest - 5, // 4: notifications.v1.Notifications.Unsubscribe:input_type -> notifications.v1.UnsubscribeRequest - 2, // 5: notifications.v1.Notifications.RegisterInstallation:output_type -> notifications.v1.RegisterInstallationResponse - 6, // 6: notifications.v1.Notifications.DeleteInstallation:output_type -> google.protobuf.Empty - 6, // 7: notifications.v1.Notifications.Subscribe:output_type -> google.protobuf.Empty - 6, // 8: notifications.v1.Notifications.Unsubscribe:output_type -> google.protobuf.Empty - 5, // [5:9] is the sub-list for method output_type - 1, // [1:5] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 8, // 1: notifications.v1.Subscription.hmac_keys:type_name -> notifications.v1.Subscription.HmacKey + 4, // 2: notifications.v1.SubscribeWithMetadataRequest.subscriptions:type_name -> notifications.v1.Subscription + 1, // 3: notifications.v1.Notifications.RegisterInstallation:input_type -> notifications.v1.RegisterInstallationRequest + 3, // 4: notifications.v1.Notifications.DeleteInstallation:input_type -> notifications.v1.DeleteInstallationRequest + 6, // 5: notifications.v1.Notifications.Subscribe:input_type -> notifications.v1.SubscribeRequest + 5, // 6: notifications.v1.Notifications.SubscribeWithMetadata:input_type -> notifications.v1.SubscribeWithMetadataRequest + 7, // 7: notifications.v1.Notifications.Unsubscribe:input_type -> notifications.v1.UnsubscribeRequest + 2, // 8: notifications.v1.Notifications.RegisterInstallation:output_type -> notifications.v1.RegisterInstallationResponse + 9, // 9: notifications.v1.Notifications.DeleteInstallation:output_type -> google.protobuf.Empty + 9, // 10: notifications.v1.Notifications.Subscribe:output_type -> google.protobuf.Empty + 9, // 11: notifications.v1.Notifications.SubscribeWithMetadata:output_type -> google.protobuf.Empty + 9, // 12: notifications.v1.Notifications.Unsubscribe:output_type -> google.protobuf.Empty + 8, // [8:13] is the sub-list for method output_type + 3, // [3:8] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_notifications_v1_service_proto_init() } @@ -553,7 +770,7 @@ func file_notifications_v1_service_proto_init() { } } file_notifications_v1_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SubscribeRequest); i { + switch v := v.(*Subscription); i { case 0: return &v.state case 1: @@ -565,6 +782,30 @@ func file_notifications_v1_service_proto_init() { } } file_notifications_v1_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubscribeWithMetadataRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_notifications_v1_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubscribeRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_notifications_v1_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UnsubscribeRequest); i { case 0: return &v.state @@ -576,6 +817,18 @@ func file_notifications_v1_service_proto_init() { return nil } } + file_notifications_v1_service_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Subscription_HmacKey); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_notifications_v1_service_proto_msgTypes[0].OneofWrappers = []interface{}{ (*DeliveryMechanism_ApnsDeviceToken)(nil), @@ -587,7 +840,7 @@ func file_notifications_v1_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_notifications_v1_service_proto_rawDesc, NumEnums: 0, - NumMessages: 6, + NumMessages: 9, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/subscriptions/subscriptions.go b/pkg/subscriptions/subscriptions.go index 22d0cbf..9620220 100644 --- a/pkg/subscriptions/subscriptions.go +++ b/pkg/subscriptions/subscriptions.go @@ -67,6 +67,60 @@ func (s SubscriptionsService) Subscribe(ctx context.Context, installationId stri }) } +func (s SubscriptionsService) SubscribeWithMetadata(ctx context.Context, installationId string, subscriptions []interfaces.SubscriptionInput) error { + return s.db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { + toUpdate := make([]*db.Subscription, len(subscriptions)) + for idx, sub := range subscriptions { + toUpdate[idx] = &db.Subscription{ + InstallationId: installationId, + Topic: sub.Topic, + IsActive: true, + IsSilent: sub.IsSilent, + } + } + + updated := make([]*db.Subscription, 0) + _, err := tx.NewInsert(). + Model(&toUpdate). + On("CONFLICT (installation_id, topic) DO UPDATE"). + Set("is_active = true"). + Set("is_silent = EXCLUDED.is_silent"). + Returning("id, topic"). + Exec(ctx, &updated) + + if err != nil { + return err + } + + topicIdMap := makeTopicIdMap(updated) + hmacKeyUpdates := []db.SubscriptionHmacKeys{} + for _, sub := range subscriptions { + subscriptionId, exists := topicIdMap[sub.Topic] + if !exists { + s.logger.Info("Skipping topic because subscription not found", zap.String("topic", sub.Topic)) + continue + } + for _, keyUpdate := range sub.HmacKeys { + hmacKeyUpdates = append(hmacKeyUpdates, db.SubscriptionHmacKeys{ + SubscriptionId: subscriptionId, + ThirtyDayPeriodsSinceEpoch: int32(keyUpdate.ThirtyDayPeriodsSinceEpoch), + Key: keyUpdate.Key, + }) + } + } + + if len(hmacKeyUpdates) > 0 { + _, err = tx.NewInsert(). + Model(&hmacKeyUpdates). + On("CONFLICT (subscription_id, thirty_day_periods_since_epoch) DO UPDATE"). + Set("key = EXCLUDED.key"). + Exec(ctx) + } + + return err + }) +} + func (s SubscriptionsService) Unsubscribe(ctx context.Context, installationId string, topics []string) error { return s.db.RunInTx(ctx, &sql.TxOptions{}, func(ctx context.Context, tx bun.Tx) error { _, err := tx.NewUpdate(). @@ -80,18 +134,21 @@ func (s SubscriptionsService) Unsubscribe(ctx context.Context, installationId st }) } -func (s SubscriptionsService) GetSubscriptions(ctx context.Context, topic string) (out []interfaces.Subscription, err error) { +func (s SubscriptionsService) GetSubscriptions(ctx context.Context, topic string, thirtyDayPeriod int) (out []interfaces.Subscription, err error) { results := make([]db.Subscription, 0) err = s.db.NewSelect(). Model(&results). Where("topic = ?", topic). Where("is_active = TRUE"). + Relation("HmacKeys", func(q *bun.SelectQuery) *bun.SelectQuery { + return q.Where("thirty_day_periods_since_epoch = ?", thirtyDayPeriod) + }). Scan(ctx) if err != nil { return nil, err } - + s.logger.Info("Results", zap.Any("results", results)) for _, result := range results { out = append(out, transformResult(result)) } @@ -99,6 +156,14 @@ func (s SubscriptionsService) GetSubscriptions(ctx context.Context, topic string return out, err } +func makeTopicIdMap(subscriptions []*db.Subscription) map[string]int64 { + out := make(map[string]int64) + for _, sub := range subscriptions { + out[sub.Topic] = sub.Id + } + return out +} + func transformResult(dbSubscription db.Subscription) interfaces.Subscription { return interfaces.Subscription{ Id: dbSubscription.Id, @@ -106,5 +171,17 @@ func transformResult(dbSubscription db.Subscription) interfaces.Subscription { InstallationId: dbSubscription.InstallationId, Topic: dbSubscription.Topic, IsActive: dbSubscription.IsActive, + IsSilent: dbSubscription.IsSilent, + HmacKey: extractHmacKey(dbSubscription.HmacKeys), + } +} + +func extractHmacKey(dbKeys []*db.SubscriptionHmacKeys) *interfaces.HmacKey { + for _, key := range dbKeys { + return &interfaces.HmacKey{ + ThirtyDayPeriodsSinceEpoch: int(key.ThirtyDayPeriodsSinceEpoch), + Key: key.Key, + } } + return nil } diff --git a/pkg/subscriptions/subscriptions_test.go b/pkg/subscriptions/subscriptions_test.go index affa6c9..1088889 100644 --- a/pkg/subscriptions/subscriptions_test.go +++ b/pkg/subscriptions/subscriptions_test.go @@ -14,6 +14,7 @@ import ( const INSTALLATION_ID = "installation_1" const TOPIC = "topic1" +const TOPIC_2 = "topic2" func createService(db *bun.DB) interfaces.Subscriptions { return NewSubscriptionsService( @@ -132,6 +133,107 @@ func Test_UnsubscribeResubscribe(t *testing.T) { require.True(t, stored.IsActive) } +func Test_SubscribeWithMetadata(t *testing.T) { + ctx := context.Background() + db, cleanup := test.CreateTestDb() + defer cleanup() + + svc := createService(db) + + key := []byte("key") + + err := svc.SubscribeWithMetadata(ctx, INSTALLATION_ID, []interfaces.SubscriptionInput{{ + Topic: TOPIC, + IsSilent: true, + HmacKeys: []interfaces.HmacKey{{ + ThirtyDayPeriodsSinceEpoch: 1, + Key: key, + }}, + }}) + require.NoError(t, err) + + results, err := svc.GetSubscriptions(ctx, TOPIC, 1) + require.NoError(t, err) + require.Len(t, results, 1) + + sub := results[0] + require.Equal(t, sub.Topic, TOPIC) + require.NotNil(t, sub.HmacKey) + require.True(t, sub.IsSilent) + require.Equal(t, sub.HmacKey.Key, key) +} + +func Test_UpdateIsSilent(t *testing.T) { + ctx := context.Background() + db, cleanup := test.CreateTestDb() + defer cleanup() + + svc := createService(db) + + err := svc.SubscribeWithMetadata(ctx, INSTALLATION_ID, []interfaces.SubscriptionInput{{ + Topic: TOPIC, + IsSilent: false, + }}) + require.NoError(t, err) + + results, err := svc.GetSubscriptions(ctx, TOPIC, 1) + require.NoError(t, err) + require.Len(t, results, 1) + + sub := results[0] + require.False(t, sub.IsSilent) + + err = svc.SubscribeWithMetadata(ctx, INSTALLATION_ID, []interfaces.SubscriptionInput{{ + Topic: TOPIC, + IsSilent: true, + }}) + require.NoError(t, err) + + results, err = svc.GetSubscriptions(ctx, TOPIC, 1) + require.NoError(t, err) + require.Len(t, results, 1) + + sub = results[0] + require.True(t, sub.IsSilent) +} + +func Test_UpdateHmacKeys(t *testing.T) { + ctx := context.Background() + db, cleanup := test.CreateTestDb() + defer cleanup() + + svc := createService(db) + + key1 := []byte("key") + key2 := []byte("key2") + + err := svc.SubscribeWithMetadata(ctx, INSTALLATION_ID, []interfaces.SubscriptionInput{{ + Topic: TOPIC, + IsSilent: true, + HmacKeys: []interfaces.HmacKey{{ + ThirtyDayPeriodsSinceEpoch: 1, + Key: key1, + }}, + }}) + require.NoError(t, err) + err = svc.SubscribeWithMetadata(ctx, INSTALLATION_ID, []interfaces.SubscriptionInput{{ + Topic: TOPIC, + IsSilent: true, + HmacKeys: []interfaces.HmacKey{{ + ThirtyDayPeriodsSinceEpoch: 1, + Key: key2, + }}, + }}) + require.NoError(t, err) + + results, err := svc.GetSubscriptions(ctx, TOPIC, 1) + require.NoError(t, err) + require.Len(t, results, 1) + + sub := results[0] + require.Equal(t, sub.HmacKey.Key, key2) +} + func Test_GetSubscriptions(t *testing.T) { ctx := context.Background() db, cleanup := test.CreateTestDb() @@ -142,7 +244,7 @@ func Test_GetSubscriptions(t *testing.T) { err := svc.Subscribe(ctx, INSTALLATION_ID, []string{TOPIC}) require.NoError(t, err) - subs, err := svc.GetSubscriptions(ctx, TOPIC) + subs, err := svc.GetSubscriptions(ctx, TOPIC, 1) require.NoError(t, err) require.Len(t, subs, 1) } diff --git a/pkg/xmtp/listener.go b/pkg/xmtp/listener.go index cfbce67..ca09818 100644 --- a/pkg/xmtp/listener.go +++ b/pkg/xmtp/listener.go @@ -137,7 +137,7 @@ func (l *Listener) processEnvelope(env *v1.Envelope) error { return nil } - subs, err := l.subscriptions.GetSubscriptions(l.ctx, env.ContentTopic) + subs, err := l.subscriptions.GetSubscriptions(l.ctx, env.ContentTopic, getThirtyDayPeriodsFromEpoch(env)) if err != nil { return err } diff --git a/pkg/xmtp/message.go b/pkg/xmtp/message.go new file mode 100644 index 0000000..85b7d3c --- /dev/null +++ b/pkg/xmtp/message.go @@ -0,0 +1,61 @@ +package xmtp + +import ( + "crypto/hmac" + "crypto/sha256" + "errors" + "fmt" + + messageApi "github.com/xmtp/example-notification-server-go/pkg/proto/message_api/v1" + messageContents "github.com/xmtp/example-notification-server-go/pkg/proto/message_contents" + "google.golang.org/protobuf/proto" +) + +type MessageContext struct { + MessageType MessageType + ShouldPush *bool + IsSender *bool +} + +func parseConversationMessage(message []byte) (*messageContents.MessageV2, error) { + var msg messageContents.Message + err := proto.Unmarshal(message, &msg) + if err != nil { + return nil, err + } + v2Message := msg.GetV2() + if v2Message != nil { + return v2Message, nil + } + return nil, errors.New("Not a V1 message") +} + +func getIsSender(msg *messageContents.MessageV2, hmacKey *[]byte) *bool { + isSender := false + if len(msg.SenderHmac) > 0 && hmacKey != nil { + fmt.Printf("Got HMAC key %x and sender hmac %x", hmacKey, msg.SenderHmac) + // Calculate HMAC of the HeaderBytes using the provided key and compare it with the SenderHmac + hmacHash := hmac.New(sha256.New, *hmacKey) + hmacHash.Write(msg.HeaderBytes) + expectedHmac := hmacHash.Sum(nil) + isSender = hmac.Equal(msg.SenderHmac, expectedHmac) + } + return &isSender +} + +func getContext(env *messageApi.Envelope, hmacKey *[]byte) MessageContext { + messageType := getMessageType(env) + var shouldPush, isSender *bool + if messageType == V2Conversation { + if parsed, err := parseConversationMessage(env.Message); err == nil { + shouldPush = parsed.ShouldPush + isSender = getIsSender(parsed, hmacKey) + } + } + + return MessageContext{ + MessageType: messageType, + ShouldPush: shouldPush, + IsSender: isSender, + } +} diff --git a/pkg/xmtp/message_test.go b/pkg/xmtp/message_test.go new file mode 100644 index 0000000..4fd2d3a --- /dev/null +++ b/pkg/xmtp/message_test.go @@ -0,0 +1,78 @@ +package xmtp + +import ( + "encoding/hex" + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/require" + messageApi "github.com/xmtp/example-notification-server-go/pkg/proto/message_api/v1" + "google.golang.org/protobuf/proto" +) + +type rawFixture struct { + Kind string + Envelope string + HmacKey string +} + +func getRawFixture(t *testing.T, kind string) *rawFixture { + data, err := os.ReadFile("../../fixtures/envelopes.json") + require.NoError(t, err) + fixtures := []rawFixture{} + err = json.Unmarshal(data, &fixtures) + require.NoError(t, err) + + for _, fixture := range fixtures { + if fixture.Kind == kind { + return &fixture + } + } + + t.Fail() + return nil +} + +func getEnvelope(t *testing.T, fixture *rawFixture) *messageApi.Envelope { + envelopeBytes, err := hex.DecodeString(fixture.Envelope) + require.NoError(t, err) + + envelope := &messageApi.Envelope{} + err = proto.Unmarshal(envelopeBytes, envelope) + require.NoError(t, err) + + return envelope +} + +func getHmacKey(t *testing.T, fixture *rawFixture) []byte { + hmacKey, err := hex.DecodeString(fixture.HmacKey) + require.NoError(t, err) + + return hmacKey +} + +func Test_IdentifyV2Invite(t *testing.T) { + rawFixture := getRawFixture(t, "v2-invite") + envelope := getEnvelope(t, rawFixture) + context := getContext(envelope, nil) + require.Equal(t, string(context.MessageType), V2Invite) + require.Nil(t, context.IsSender) + require.Nil(t, context.ShouldPush) +} + +func Test_IdentifyV2Conversation(t *testing.T) { + rawFixture := getRawFixture(t, "v2-conversation") + envelope := getEnvelope(t, rawFixture) + hmacKey := getHmacKey(t, rawFixture) + context := getContext(envelope, &hmacKey) + require.True(t, *context.IsSender) + require.True(t, *context.ShouldPush) + require.Equal(t, string(context.MessageType), "v2-conversation") + + wrongKey := []byte("foo") + contextWithWrongKey := getContext(envelope, &wrongKey) + require.False(t, *contextWithWrongKey.IsSender) + require.True(t, *contextWithWrongKey.ShouldPush) + require.Equal(t, string(contextWithWrongKey.MessageType), "v2-conversation") +} diff --git a/pkg/xmtp/topic.go b/pkg/xmtp/topic.go new file mode 100644 index 0000000..7e08e4d --- /dev/null +++ b/pkg/xmtp/topic.go @@ -0,0 +1,58 @@ +package xmtp + +import ( + "strings" + + v1 "github.com/xmtp/example-notification-server-go/pkg/proto/message_api/v1" +) + +const V1_PREFIX = "/xmtp/0/" +const V3_PREFIX = "/xmtp/1/" + +type MessageType string + +const ( + Test MessageType = "test" + Private = "private" + Contact = "contact" + V1Intro = "v1-intro" + V2Invite = "v2-invite" + V1Conversation = "v1-conversation" + V2Conversation = "v2-conversation" + V3Welcome = "v3-welcome" + V3Conversation = "v3-conversation" + Unknown = "unknown" +) + +var messageTypeByPrefix = map[string]MessageType{ + "test": Test, + "privatestore": Private, + "contact": Contact, + "intro": V1Intro, + "dm": V1Conversation, + "invite": V2Invite, + "m": V2Conversation, + "g": V3Conversation, + "w": V3Welcome, +} + +func getMessageType(env *v1.Envelope) MessageType { + topic := env.ContentTopic + if strings.HasPrefix(topic, "test-") { + return Test + } + if strings.HasPrefix(topic, V1_PREFIX) { + topic = strings.TrimPrefix(topic, V1_PREFIX) + } + if strings.HasPrefix(topic, V3_PREFIX) { + topic = strings.TrimPrefix(topic, V3_PREFIX) + } + prefix, _, hasPrefix := strings.Cut(topic, "-") + if hasPrefix { + if category, found := messageTypeByPrefix[prefix]; found { + return category + } + } + + return Unknown +} diff --git a/pkg/xmtp/utils.go b/pkg/xmtp/utils.go new file mode 100644 index 0000000..bcc29ec --- /dev/null +++ b/pkg/xmtp/utils.go @@ -0,0 +1,9 @@ +package xmtp + +import ( + v1 "github.com/xmtp/example-notification-server-go/pkg/proto/message_api/v1" +) + +func getThirtyDayPeriodsFromEpoch(env *v1.Envelope) int { + return int(env.TimestampNs / 1_000_000_000 / 60 / 60 / 24 / 30) +} diff --git a/pkg/xmtp/utils_test.go b/pkg/xmtp/utils_test.go new file mode 100644 index 0000000..4b7627c --- /dev/null +++ b/pkg/xmtp/utils_test.go @@ -0,0 +1,18 @@ +package xmtp + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + message_apiv1 "github.com/xmtp/example-notification-server-go/pkg/proto/message_api/v1" +) + +func Test_get_thirty_day_periods(t *testing.T) { + nowNs := time.Now().UnixNano() + periods := getThirtyDayPeriodsFromEpoch(&message_apiv1.Envelope{ + TimestampNs: uint64(nowNs), + }) + // This test should cover us for 28 years, but will catch cases where we are off by an order of magnitude + require.Less(t, periods, 1000) +} diff --git a/proto/notifications/v1/service.proto b/proto/notifications/v1/service.proto index 2f26aad..dbc26a8 100644 --- a/proto/notifications/v1/service.proto +++ b/proto/notifications/v1/service.proto @@ -5,6 +5,7 @@ import "google/protobuf/empty.proto"; option go_package = "github.com/xmtp/example-notification-server-go/pkg/proto/notifications/v1"; +// An union of possible delibery mechanisms message DeliveryMechanism { oneof delivery_mechanism_type { string apns_device_token = 1; @@ -12,25 +13,48 @@ message DeliveryMechanism { } } +// A request to register an installation with the service message RegisterInstallationRequest { string installation_id = 1; DeliveryMechanism delivery_mechanism = 2; } +// Response to RegisterInstallationRequest message RegisterInstallationResponse { string installation_id = 1; uint64 valid_until = 2; } +// Delete an installation from the service message DeleteInstallationRequest { string installation_id = 1; } +// A subscription with associated metadata +message Subscription { + message HmacKey { + uint32 thirty_day_periods_since_epoch = 1; + bytes key = 2; + } + + string topic = 1; + repeated HmacKey hmac_keys = 2; + bool is_silent = 3; +} + +// A request to subscribe to a list of topics and update the associated metadata +message SubscribeWithMetadataRequest { + string installation_id = 1; + repeated Subscription subscriptions = 2; +} + +// Subscribe to a list of topics message SubscribeRequest { string installation_id = 1; repeated string topics = 2; } +// Unsubscribe from a list of topics message UnsubscribeRequest { string installation_id = 1; repeated string topics = 2; @@ -42,5 +66,7 @@ service Notifications { rpc DeleteInstallation(DeleteInstallationRequest) returns (google.protobuf.Empty); rpc Subscribe(SubscribeRequest) returns (google.protobuf.Empty); + rpc SubscribeWithMetadata(SubscribeWithMetadataRequest) + returns (google.protobuf.Empty); rpc Unsubscribe(UnsubscribeRequest) returns (google.protobuf.Empty); } diff --git a/test/helpers.go b/test/helpers.go index 067af9c..1b8f1ca 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -26,5 +26,6 @@ func CreateTestDb() (*bun.DB, func()) { _, _ = db.NewTruncateTable().Model((*database.Installation)(nil)).Cascade().Exec(ctx) _, _ = db.NewTruncateTable().Model((*database.DeviceDeliveryMechanism)(nil)).Cascade().Exec(ctx) _, _ = db.NewTruncateTable().Model((*database.Subscription)(nil)).Cascade().Exec(ctx) + _, _ = db.NewTruncateTable().Model((*database.SubscriptionHmacKeys)(nil)).Cascade().Exec(ctx) } }