57 lines
1.7 KiB
Go
57 lines
1.7 KiB
Go
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
//
|
|
// Dispatcher fan-outs an event payload to every registered device, encrypting
|
|
// the payload to each device's X25519 public key (NaCl sealed box) and sending
|
|
// the ciphertext via Firebase Cloud Messaging.
|
|
|
|
package notify
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
|
|
"github.com/cyrilinait/birdwatch-relay/internal/crypto"
|
|
"github.com/cyrilinait/birdwatch-relay/internal/fcm"
|
|
"github.com/cyrilinait/birdwatch-relay/internal/storage"
|
|
)
|
|
|
|
type Dispatcher struct {
|
|
store *storage.Store
|
|
sender *fcm.Sender
|
|
}
|
|
|
|
func NewDispatcher(store *storage.Store, sender *fcm.Sender) *Dispatcher {
|
|
return &Dispatcher{store: store, sender: sender}
|
|
}
|
|
|
|
// Send fans the payload out to every registered device. The payload is sealed
|
|
// to each device's public key independently — Google FCM only ever sees
|
|
// ciphertext + the device token. Errors per device are logged but do not stop
|
|
// the rest of the fanout.
|
|
func (d *Dispatcher) Send(ctx context.Context, payload []byte) {
|
|
devices, err := d.store.ListAll()
|
|
if err != nil {
|
|
slog.Error("dispatcher: list devices failed", "err", err)
|
|
return
|
|
}
|
|
if len(devices) == 0 {
|
|
slog.Debug("dispatcher: no registered devices, dropping event")
|
|
return
|
|
}
|
|
var sent, failed int
|
|
for _, dev := range devices {
|
|
ct, err := crypto.Seal(payload, dev.PublicKey)
|
|
if err != nil {
|
|
slog.Error("dispatcher: seal failed", "err", err, "device_id", dev.ID)
|
|
failed++
|
|
continue
|
|
}
|
|
if err := d.sender.SendCiphertext(ctx, dev.FCMToken, ct); err != nil {
|
|
slog.Warn("dispatcher: fcm send failed", "err", err, "device_id", dev.ID)
|
|
failed++
|
|
continue
|
|
}
|
|
sent++
|
|
}
|
|
slog.Info("dispatcher fanout", "sent", sent, "failed", failed, "total", len(devices))
|
|
}
|