Skip to content

Commit

Permalink
Make sending multiple notifications less likely
Browse files Browse the repository at this point in the history
  • Loading branch information
boreq committed Jun 23, 2023
1 parent 8402930 commit 59f6581
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 33 deletions.
4 changes: 2 additions & 2 deletions cmd/notification-service/di/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
cloud.google.com/go/firestore v1.10.0
github.com/boreq/errors v0.1.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/google/uuid v1.3.0
github.com/google/wire v0.5.0
github.com/gorilla/websocket v1.5.0
github.com/hashicorp/go-multierror v1.1.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkj
github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k=
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
github.com/google/wire v0.5.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
Expand Down
22 changes: 12 additions & 10 deletions service/adapters/apns/apns.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package apns

import (
"github.com/boreq/errors"
"github.com/planetary-social/go-notification-service/internal/logging"
"github.com/planetary-social/go-notification-service/service/config"
"github.com/planetary-social/go-notification-service/service/domain/notifications"
"github.com/sideshow/apns2"
Expand All @@ -11,9 +12,10 @@ import (
type APNS struct {
client *apns2.Client
config config.Config
logger logging.Logger
}

func NewAPNS(config config.Config) (*APNS, error) {
func NewAPNS(config config.Config, logger logging.Logger) (*APNS, error) {
cert, err := certificate.FromP12File(config.APNSCertificatePath(), "") // todo password support?
if err != nil {
return nil, errors.Wrap(err, "error loading certificate")
Expand All @@ -25,24 +27,24 @@ func NewAPNS(config config.Config) (*APNS, error) {

client := apns2.NewClient(cert).Production() // todo dev/prod

return &APNS{client: client, config: config}, nil
return &APNS{client: client, config: config, logger: logger}, nil
}

func (a *APNS) SendNotification(notification notifications.Notification) (notifications.NotificationID, error) {
func (a *APNS) SendNotification(notification notifications.Notification) error {
n := &apns2.Notification{}
n.ApnsID = notification.UUID().String()
n.DeviceToken = notification.APNSToken().Hex()
n.Topic = a.config.APNSTopic()
n.Payload = notification.Payload()

res, err := a.client.Push(n)
_, err := a.client.Push(n)
if err != nil {
return notifications.NotificationID{}, errors.Wrap(err, "error pushing the notification")
return errors.Wrap(err, "error pushing the notification")
}

id, err := notifications.NewNotificationID(res.ApnsID)
if err != nil {
return notifications.NotificationID{}, errors.Wrap(err, "error creating the notification id")
}
a.logger.Debug().
WithField("uuid", notification.UUID().String()).
Message("sent a notification")

return id, nil
return nil
}
43 changes: 40 additions & 3 deletions service/adapters/firestore/repository_event.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package firestore

import (
"context"

"cloud.google.com/go/firestore"
"github.com/boreq/errors"
"github.com/planetary-social/go-notification-service/service/domain"
"github.com/planetary-social/go-notification-service/service/domain/notifications"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

const (
collectionEvents = "events"
collectionEvents = "events"
collectionEventsNotifications = "notifications"
)

type EventRepository struct {
Expand All @@ -28,16 +34,47 @@ func NewEventRepository(
}
}

func (e *EventRepository) Save(relay domain.RelayAddress, event domain.Event) error {
func (e *EventRepository) Save(event domain.Event) error {
if err := e.saveUnderEvents(event); err != nil {
return errors.Wrap(err, "error saving under events")
}

return nil
}

func (e *EventRepository) Exists(ctx context.Context, id domain.EventId) (bool, error) {
_, err := e.client.Collection(collectionEvents).Doc(id.Hex()).Get(ctx)
if err != nil {
if status.Code(err) != codes.NotFound {
return false, nil
}
return false, errors.Wrap(err, "error checking if document exists")
}
return true, nil
}

func (e *EventRepository) SaveNotificationForEvent(notification notifications.Notification) error {
notificationDocPath := e.client.
Collection(collectionEvents).
Doc(notification.Event().Id().Hex()).
Collection(collectionEventsNotifications).
Doc(notification.UUID().String())

notificationDocData := map[string]any{
"uuid": notification.UUID().String(),
"token": notification.APNSToken(),
"payload": notification.Payload(),
}

if err := e.tx.Set(notificationDocPath, notificationDocData, firestore.MergeAll); err != nil {
return errors.Wrap(err, "error updating the notification doc")
}

return nil
}

func (e *EventRepository) saveUnderEvents(event domain.Event) error {
// todo how to handle tags
// todo how to handle tags? do we want to save tags in a searchable way?

eventDocPath := e.client.Collection(collectionEvents).Doc(event.Id().Hex())
eventDocData := map[string]any{
Expand Down
5 changes: 4 additions & 1 deletion service/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/boreq/errors"
"github.com/planetary-social/go-notification-service/service/domain"
"github.com/planetary-social/go-notification-service/service/domain/notifications"
)

type TransactionProvider interface {
Expand Down Expand Up @@ -35,7 +36,9 @@ type PublicKeyRepository interface {
}

type EventRepository interface {
Save(relay domain.RelayAddress, event domain.Event) error
Save(event domain.Event) error
Exists(ctx context.Context, id domain.EventId) (bool, error)
SaveNotificationForEvent(notification notifications.Notification) error
}

type Application struct {
Expand Down
25 changes: 20 additions & 5 deletions service/app/handler_process_received_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ func (h *ProcessReceivedEventHandler) Handle(ctx context.Context, cmd ProcessRec
}

if err := h.transactionProvider.Transact(ctx, func(ctx context.Context, adapters Adapters) error {
exists, err := adapters.Events.Exists(ctx, cmd.event.Id())
if err != nil {
return errors.Wrap(err, "error checking if event exists")
}

if exists {
return nil
}

for _, mention := range mentions {
token, err := adapters.PublicKeys.GetAPNSToken(ctx, mention)
if err != nil {
Expand All @@ -68,17 +77,23 @@ func (h *ProcessReceivedEventHandler) Handle(ctx context.Context, cmd ProcessRec
}

for _, notification := range notifications {
id, err := h.apns.SendNotification(notification)
if err != nil {
// todo send via pubsub instead
if err := h.apns.SendNotification(notification); err != nil {
return errors.Wrap(err, "error sending a notification")
}

h.logger.Debug().WithField("id", id.String()).Message("sent a notification")
if err := adapters.Events.SaveNotificationForEvent(notification); err != nil {
return errors.Wrap(err, "error saving notification")
}
}
}

// todo maybe not always save?
return adapters.Events.Save(cmd.relay, cmd.event)
// todo don't save if we don't find this event relevant in the loop above?
if err := adapters.Events.Save(cmd.event); err != nil {
return errors.Wrap(err, "error saving the event")
}

return nil
}); err != nil {
return errors.Wrap(err, "transaction error")
}
Expand Down
8 changes: 8 additions & 0 deletions service/domain/event_kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ func NewEventKind(k int) (EventKind, error) {
return EventKind{k}, nil
}

func MustNewEventKind(k int) EventKind {
v, err := NewEventKind(k)
if err != nil {
panic(err)
}
return v
}

func (k EventKind) Int() int {
return k.k
}
112 changes: 100 additions & 12 deletions service/domain/notifications/notifications.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,55 +2,143 @@ package notifications

import (
"github.com/boreq/errors"
"github.com/google/uuid"
"github.com/planetary-social/go-notification-service/internal/logging"
"github.com/planetary-social/go-notification-service/service/domain"
"github.com/sideshow/apns2/payload"
)

var (
eventKindNote = domain.MustNewEventKind(1)
eventKindReaction = domain.MustNewEventKind(7)
eventKindEncryptedDirectMessage = domain.MustNewEventKind(4)
)

type Generator struct {
logger logging.Logger
}

func NewGenerator() *Generator {
return &Generator{}
func NewGenerator(logger logging.Logger) *Generator {
return &Generator{
logger: logger.New("generator"),
}
}

func (g *Generator) Generate(mention domain.PublicKey, token domain.APNSToken, event domain.Event) ([]Notification, error) {
if mentionedThemself(mention, event) {
payload, err := g.createPayload(mention, event)
if err != nil {
return nil, errors.Wrap(err, "error creating the payload")
}

if payload == nil {
return nil, nil
}

// todo
id, err := NewNotificationUUID()
if err != nil {
return nil, errors.Wrap(err, "error generating a notification id")
}

j, err := payload.MarshalJSON()
if err != nil {
return nil, errors.Wrap(err, "error marshaling payload")
}

return nil, nil
notification, err := NewNotification(event, id, token, j)
if err != nil {
return nil, errors.Wrap(err, "error creating a notification")
}

return []Notification{notification}, nil
}

func (g *Generator) createPayload(mention domain.PublicKey, event domain.Event) (*payload.Payload, error) {
if mentionedThemself(mention, event) {
return nil, nil
}

switch event.Kind() {
case eventKindNote:
// todo "Your message has new replies."/"You were mentioned in a message".
g.logger.Debug().Message("note")
return nil, nil
case eventKindReaction:
// todo "Your message has new reactions."
g.logger.Debug().Message("reaction")
return nil, nil
case eventKindEncryptedDirectMessage:
// todo "You received a private message."
g.logger.Debug().Message("encrypted direct message")
return nil, nil
default:
return nil, nil
}
}

func mentionedThemself(mention domain.PublicKey, event domain.Event) bool {
return mention == event.PubKey()
}

type Notification struct {
event domain.Event

uuid NotificationUUID
token domain.APNSToken
payload *payload.Payload
payload []byte
}

func NewNotification(
event domain.Event,
uuid NotificationUUID,
token domain.APNSToken,
payload []byte,
) (Notification, error) {
if len(payload) == 0 {
return Notification{}, errors.New("empty payload")
}
return Notification{
event: event,
uuid: uuid,
token: token,
payload: payload,
}, nil
}

func (n Notification) Event() domain.Event {
return n.event
}

func (n Notification) UUID() NotificationUUID {
return n.uuid
}

func (n Notification) APNSToken() domain.APNSToken {
return n.token
}

func (n Notification) Payload() *payload.Payload {
func (n Notification) Payload() []byte {
return n.payload
}

type NotificationID struct {
type NotificationUUID struct {
s string
}

func NewNotificationID(s string) (NotificationID, error) {
func NewNotificationUUID() (NotificationUUID, error) {
return NewNotificationUUIDFromString(uuid.New().String())
}

func NewNotificationUUIDFromString(s string) (NotificationUUID, error) {
if s == "" {
return NotificationID{}, errors.New("empty id")
return NotificationUUID{}, errors.New("empty id")
}
_, err := uuid.Parse(s)
if err != nil {
return NotificationUUID{}, errors.Wrap(err, "malformed uuid")
}
return NotificationID{s: s}, nil
return NotificationUUID{s: s}, nil
}

func (id NotificationID) String() string {
func (id NotificationUUID) String() string {
return id.s
}

0 comments on commit 59f6581

Please sign in to comment.