Documentation
¶
Overview ¶
Package push handles Web Push subscriptions and notification delivery. Subscriptions and the singleton VAPID keypair live in the shared Postgres database.
Index ¶
- Constants
- Variables
- func CandidateOther(primary string, names []string) string
- func PurgeNotifications(ctx context.Context, db *sql.DB) (int64, error)
- func SafeNotificationURL(u string) string
- func ValidDeviceToken(t string) bool
- func ValidEndpoint(raw string) error
- type APNs
- type APNsConfig
- type APNsDevice
- type APNsError
- type APNsExportRow
- type Factory
- type NetworkConfirm
- type Notification
- type Payload
- type Preferences
- type Sender
- type Service
- func (s *Service) APNsEnabled() bool
- func (s *Service) APNsTopic() string
- func (s *Service) DeliverMilestone(ctx context.Context, store *Store, title, body string) (int, error)
- func (s *Service) DeliverPlugInReminder(ctx context.Context, store *Store, soc, threshold int) (int, error)
- func (s *Service) NotifyAnomaly(store *Store, title, body, deepLink string)
- func (s *Service) NotifyChargingDone(store *Store, sessionID, summary string)
- func (s *Service) NotifyNetworkConfirm(store *Store, userKey string, e NetworkConfirm, pending int)
- func (s *Service) NotifySoftwareUpdate(store *Store, vehicle, version, notesURL string, installed bool)
- func (s *Service) PublicKey() string
- func (s *Service) SendAPNsTest(ctx context.Context, store *Store) int
- func (s *Service) SendTest(ctx context.Context, store *Store, endpoint string) error
- func (s *Service) WithAPNs(a *APNs) *Service
- type Store
- func (s *Store) APNsPrefs(ctx context.Context, token string) (Preferences, bool, error)
- func (s *Store) Close() error
- func (s *Store) Count(ctx context.Context) (int, error)
- func (s *Store) CreateNotification(ctx context.Context, kind, title, body, url string) (uuid.UUID, error)
- func (s *Store) Delete(ctx context.Context, endpoint string) error
- func (s *Store) DeleteAPNsDevice(ctx context.Context, token string) error
- func (s *Store) ExportAPNsDevices(ctx context.Context) ([]APNsExportRow, error)
- func (s *Store) Get(ctx context.Context, endpoint string) (*Subscription, error)
- func (s *Store) GetNotification(ctx context.Context, id uuid.UUID) (Notification, error)
- func (s *Store) GetVAPID(ctx context.Context) (*VAPID, error)
- func (s *Store) List(ctx context.Context) ([]Subscription, error)
- func (s *Store) ListLiveAPNsDevices(ctx context.Context) ([]APNsDevice, error)
- func (s *Store) SaveVAPID(ctx context.Context, v VAPID) error
- func (s *Store) SetAPNsOptIns(ctx context.Context, token string, plugIn, milestones *bool) (bool, error)
- func (s *Store) SetAPNsPrefs(ctx context.Context, token string, chargingDone, anomaly, softwareUpdate *bool) (bool, error)
- func (s *Store) SetOptIns(ctx context.Context, endpoint string, plugIn, milestones *bool) (bool, error)
- func (s *Store) SwitchOffAll(ctx context.Context, kind string) (int64, error)
- func (s *Store) Upsert(ctx context.Context, sub Subscription) error
- func (s *Store) UpsertAPNsDevice(ctx context.Context, sessionID uuid.UUID, d APNsDevice) error
- type Subscription
- type VAPID
Constants ¶
const ( EnvSandbox = "sandbox" EnvProduction = "production" )
Device environments.
const NotificationTTL = 24 * time.Hour
NotificationTTL is how long a notification's text is kept for the app to fetch.
Variables ¶
var ErrEndpoint = errors.New("push: endpoint is not an accepted push service")
ErrEndpoint is returned for an endpoint that is not an accepted push service.
var ErrNotFound = errors.New("push: not found")
ErrNotFound is "no such notification (for this user, still fresh)".
Functions ¶
func CandidateOther ¶ added in v0.33.2
CandidateOther picks the second distinct network from detection evidence candidates (names in nearest-first order); "" when none.
func PurgeNotifications ¶ added in v0.38.11
PurgeNotifications deletes every notification past NotificationTTL (all users). Run hourly.
func SafeNotificationURL ¶ added in v0.38.11
SafeNotificationURL keeps a notification's tap target inside the app: a same-site relative path, else "/" (an external link, such as a release-notes page, is dropped).
func ValidDeviceToken ¶ added in v0.38.11
ValidDeviceToken reports whether t has the shape of an APNs device token.
func ValidEndpoint ¶ added in v0.37.5
ValidEndpoint reports whether raw is an https URL on an accepted push service: no credentials, the default port, a named host (never an IP literal) on the list above.
Types ¶
type APNs ¶ added in v0.38.11
type APNs struct {
// contains filtered or unexported fields
}
APNs sends pushes to Apple. Safe for concurrent use.
func NewAPNs ¶ added in v0.38.11
func NewAPNs(cfg APNsConfig) (*APNs, error)
NewAPNs parses the key and returns a sender. The key is a PKCS#8 P-256 PEM (the .p8 file). It errors, naming nothing from the key, when the config is incomplete or the key is not usable.
type APNsConfig ¶ added in v0.38.11
type APNsConfig struct {
KeyID string // RIVOLT_APNS_KEY_ID
TeamID string // RIVOLT_APNS_TEAM_ID
KeyPEM string // RIVOLT_APNS_KEY: the .p8 file's text
Topic string // RIVOLT_APNS_TOPIC: the bundle id
}
APNsConfig is the server's Apple Push identity: the token-auth key (.p8) and the app's bundle id (the apns-topic). All four must be set for APNs to be on.
func (APNsConfig) Enabled ¶ added in v0.38.11
func (c APNsConfig) Enabled() bool
Enabled reports whether every field is set.
type APNsDevice ¶ added in v0.38.11
type APNsDevice struct {
ID uuid.UUID
SessionID uuid.UUID
Token string
Env string
BundleID string
Prefs Preferences
CreatedAt time.Time
LastOKAt *time.Time
}
APNsDevice is an iPhone app install registered for push (push_apns_devices). Token is the raw APNs device token; it is needed to send, is never logged and is not part of the data export (ExportRow).
type APNsError ¶ added in v0.38.11
APNsError is a refusal from Apple. Reason is Apple's code ("BadDeviceToken", "Unregistered", …), never anything from our payload.
type APNsExportRow ¶ added in v0.38.11
type APNsExportRow struct {
BundleID string `json:"bundle_id"`
Env string `json:"env"`
CreatedAt time.Time `json:"created_at"`
LastOKAt *time.Time `json:"last_ok_at"`
Prefs Preferences `json:"prefs"`
}
APNsExportRow is a device as the data export lists it: no token.
type Factory ¶
type Factory struct {
// contains filtered or unexported fields
}
Factory hands out per-user *Store instances. push.Store wraps (db, userID); the per-user split is for routing, not for per-user-VAPID — the VAPID keypair is global (push_vapid.id = 1) and any *Store can read it.
type NetworkConfirm ¶ added in v0.33.2
type NetworkConfirm struct {
ChargeID string
// Network is the detected network's display name ("" if unmatched).
Network string
// Other is a second, co-located network, if the evidence has one.
Other string
Cost float64
Rate float64
KWh float64
// Matched is false when nothing was within range (no price).
Matched bool
// State is "estimated" or "needs_confirmation".
State string
}
NetworkConfirm is what the close hook knows about an auto-priced charge. Cost/Rate are zero when nothing matched.
type Notification ¶ added in v0.38.11
type Notification struct {
ID uuid.UUID `json:"id"`
Kind string `json:"kind"`
Title string `json:"title"`
Body string `json:"body"`
URL string `json:"url"`
CreatedAt time.Time `json:"created_at"`
}
Notification is the text of a push, fetched by the app after Apple delivers the generic one.
type Payload ¶
type Payload struct {
Title string `json:"title"`
Body string `json:"body"`
// Tag collapses multiple notifications in the same category into a
// single entry on the device (so a run of back-to-back shots doesn't
// spam the notification center).
Tag string `json:"tag,omitempty"`
// URL is the path the PWA should navigate to when the user taps the
// notification. Must be same-origin; the SW uses clients.openWindow.
URL string `json:"url,omitempty"`
// Kind lets the SW / UI disambiguate without string-matching titles.
Kind string `json:"kind,omitempty"`
}
Payload is the JSON body we deliver to the service worker. The SW parses it in its 'push' handler and turns it into a Notification.
func NetworkConfirmPayload ¶ added in v0.33.2
func NetworkConfirmPayload(e NetworkConfirm) Payload
NetworkConfirmPayload builds the notification for e, using the contract's three wordings.
func PlugInReminderPayload ¶ added in v0.37.5
PlugInReminderPayload is the plug-in reminder: functional, SoC and the user's own threshold, no place and no promotion.
type Preferences ¶
type Preferences struct {
OnChargingDone bool `json:"on_charging_done"`
OnPlugInReminder bool `json:"on_plug_in_reminder"`
OnAnomaly bool `json:"on_anomaly"`
OnSoftwareUpdate bool `json:"on_software_update"`
OnMilestones bool `json:"on_milestones"`
}
Preferences captures the per-subscription event filters.
type Sender ¶
type Sender interface {
SendNotification(message []byte, sub *webpush.Subscription, options *webpush.Options) (*http.Response, error)
}
Sender is the subset of webpush used by Service. Extracted so tests can feed a fake without spinning up a push service.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service sends push notifications to every stored subscription that opted in for the given event. All public methods are non-blocking: they hand the work off to a background goroutine so they can be called from latency-sensitive spots (recorder flush, analysis completion) without holding anything up.
func NewService ¶
NewService builds a Service. Callers should only construct one per process; it's safe for concurrent use.
func (*Service) APNsEnabled ¶ added in v0.38.11
APNsEnabled reports whether the iPhone app's push is on.
func (*Service) APNsTopic ¶ added in v0.38.12
APNsTopic is the app bundle id notifications are sent for ("" when off).
func (*Service) DeliverMilestone ¶ added in v0.37.5
func (s *Service) DeliverMilestone(ctx context.Context, store *Store, title, body string) (int, error)
DeliverMilestone sends a milestone push to every subscription of the store's user that has milestones on, and waits. It reports how many sends succeeded; an error means none did.
func (*Service) DeliverPlugInReminder ¶ added in v0.37.5
func (s *Service) DeliverPlugInReminder(ctx context.Context, store *Store, soc, threshold int) (int, error)
DeliverPlugInReminder sends the plug-in reminder to every subscription of the store's user that has it on, and waits. It reports how many sends succeeded; an error means none did, or the subscriptions could not be read. Zero subscriptions with the reminder on is an error too: nothing was delivered.
func (*Service) NotifyAnomaly ¶
NotifyAnomaly fires when an anomaly is detected for the user. Scoped to the user via store.
func (*Service) NotifyChargingDone ¶
NotifyChargingDone fires when a charge session reaches its terminal state. The provided store must be scoped to the owning user.
func (*Service) NotifyNetworkConfirm ¶ added in v0.33.2
func (s *Service) NotifyNetworkConfirm(store *Store, userKey string, e NetworkConfirm, pending int)
NotifyNetworkConfirm sends the confirm-or-change push for an auto-priced DC fast charge, rate limited per user. userKey scopes the limiter (the user id); pending is how many charges the user has open for confirmation, used in the collapsed summary. The limiter is per-process: with several pods a user can see up to confirmPerWindow per pod.
func (*Service) NotifySoftwareUpdate ¶ added in v0.36.10
func (s *Service) NotifySoftwareUpdate(store *Store, vehicle, version, notesURL string, installed bool)
NotifySoftwareUpdate fires when a vehicle is offered a new software version (installed=false) or its installed version changed (installed=true). Scoped to the user via store. notesURL is the release-notes link, opened by tapping the notification; it must be an https URL, anything else is dropped and the tap opens the app.
func (*Service) PublicKey ¶
PublicKey exposes the VAPID public key so the API layer can return it to the frontend for pushManager.subscribe().
func (*Service) SendAPNsTest ¶ added in v0.38.14
SendAPNsTest sends the user's own test notification to each of their signed-in iPhones, whatever its switches say, and returns how many Apple accepted. The words are fixed here, so nothing a caller sends reaches Apple or the phone.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps access to push_subscriptions + push_vapid. Scoped to a single user_id; the VAPID keypair is global (push_vapid.id = 1).
func NewServerStore ¶
NewServerStore returns a *Store usable only for VAPID Get/Save. Subscription ops will fail (userID is zero); that's by design — VAPID is the only push state that's truly server-wide.
func (*Store) APNsPrefs ¶ added in v0.38.12
APNsPrefs returns the five switches of one of the user's devices; ok is false when the token is not the user's.
func (*Store) CreateNotification ¶ added in v0.38.11
func (s *Store) CreateNotification(ctx context.Context, kind, title, body, url string) (uuid.UUID, error)
CreateNotification stores a notification's text for NotificationTTL and returns its random id. Only the known kinds are stored.
func (*Store) DeleteAPNsDevice ¶ added in v0.38.11
DeleteAPNsDevice removes one of the user's devices by token. No-op if absent.
func (*Store) ExportAPNsDevices ¶ added in v0.38.11
func (s *Store) ExportAPNsDevices(ctx context.Context) ([]APNsExportRow, error)
ExportAPNsDevices lists the user's devices for the data export, without tokens.
func (*Store) Get ¶ added in v0.37.2
Get returns the user's subscription for endpoint, or (nil, nil) when the endpoint is not one of theirs.
func (*Store) GetNotification ¶ added in v0.38.11
GetNotification returns one of the user's notifications that is under NotificationTTL old. Another user's id, an unknown id and an expired one are all ErrNotFound.
func (*Store) GetVAPID ¶
GetVAPID returns the persisted VAPID keypair or (nil, nil) if none is stored yet. The keypair is shared across all users of this install.
func (*Store) List ¶
func (s *Store) List(ctx context.Context) ([]Subscription, error)
List returns every subscription for this user, oldest first.
func (*Store) ListLiveAPNsDevices ¶ added in v0.38.11
func (s *Store) ListLiveAPNsDevices(ctx context.Context) ([]APNsDevice, error)
ListLiveAPNsDevices returns the user's devices whose app session is still live: not revoked and not expired. A phone that signed out stops receiving pushes whether or not it unregistered.
func (*Store) SetAPNsOptIns ¶ added in v0.38.11
func (s *Store) SetAPNsOptIns(ctx context.Context, token string, plugIn, milestones *bool) (bool, error)
SetAPNsOptIns sets the plug-in reminder and/or milestones switch (the two that start off) on one of the user's devices; nil leaves a switch alone.
func (*Store) SetAPNsPrefs ¶ added in v0.38.11
func (s *Store) SetAPNsPrefs(ctx context.Context, token string, chargingDone, anomaly, softwareUpdate *bool) (bool, error)
SetAPNsPrefs sets the three always-available switches on one of the user's devices; nil leaves a switch alone. Reports whether the token is the user's.
func (*Store) SetOptIns ¶ added in v0.37.2
func (s *Store) SetOptIns(ctx context.Context, endpoint string, plugIn, milestones *bool) (bool, error)
SetOptIns sets the plug-in reminder and/or milestones switch on one of the user's subscriptions (nil leaves a switch alone) and reports whether the endpoint is the user's.
func (*Store) SwitchOffAll ¶ added in v0.37.2
SwitchOffAll turns one opt-in kind (plug_in_reminder or milestones) off on every one of the user's subscriptions and reports how many changed.
func (*Store) Upsert ¶
func (s *Store) Upsert(ctx context.Context, sub Subscription) error
Upsert inserts or updates a subscription keyed on (user_id, endpoint). It never sets the two opt-in switches (plug-in reminder, milestones) on: a new row starts with both off and a re-subscribe leaves what is stored. Only SetOptIns, reached from the Settings screen's own control, turns them on, so a stale tab or an old pod posting its local defaults cannot opt anyone in.
func (*Store) UpsertAPNsDevice ¶ added in v0.38.11
UpsertAPNsDevice registers (or refreshes) a device for the app session it signed in with. A new row starts with the web's defaults: charging done, anomaly and software update on, plug-in reminder and milestones off. Re-registering keeps what is stored for every switch (only SetAPNsOptIns/SetAPNsPrefs change them), so a stale app posting its local defaults cannot opt anyone in.
type Subscription ¶
type Subscription struct {
Endpoint string
P256dh string
Auth string
OnChargingDone bool
OnPlugInReminder bool
OnAnomaly bool
OnSoftwareUpdate bool
// OnMilestones is the opt-in push for milestone moments (default off).
OnMilestones bool
UserAgent string
CreatedAt time.Time
UpdatedAt time.Time
}
Subscription is a stored browser push subscription.
type VAPID ¶
VAPID is the server identity used to sign outbound push requests.
func LoadOrGenerateVAPID ¶
func LoadOrGenerateVAPID(ctx context.Context, store *Store, envPub, envPriv, subject string) (VAPID, error)
LoadOrGenerateVAPID returns the persisted VAPID keypair, generating and persisting a fresh one on first run. Env-supplied values (if both publicKey and privateKey are non-empty) always win and are written back into the store so subsequent restarts stay stable if the env var is removed.
subject is the "Subscriber" claim embedded in VAPID-signed requests. Push services use it to reach out to the server owner if something misbehaves; mailto: URLs are the canonical form but any URL works.