Files
warp/server/internal/storage/pocketbase/pocketbase.go
T
tajniak81andClaude Opus 5.5 b507753172 Phase 8: push and notifications
Implements PLAN.md Phase 8. internal/pushrules is the rule language: the
spec's default ruleset, all six condition kinds, glob and word-boundary
matching, intentional mentions switching off the legacy body rules, and
rule ordering as Synapse has it. internal/pushserver keeps the rules,
pushers and notifications and talks to push gateways.

Push rules are the m.push_rules account data, written only by the push
server; reads keep the user's own rules and the enabled/actions of each
default and take the rest from the current defaults, so new default rules
reach every user without a migration. PUT account_data/m.push_rules is
refused with 405.

The push server listens on the roomserver's emit stage and evaluates each
event on its own worker for every local member and an invite's target.
Notifications and per-room unread counts are stored in migration 0010;
counts are a seventh sync token stream, so /sync reports
unread_notifications and picks up count changes made after the event or by
a receipt from another device. m.read and m.read.private receipts clear
the counts; leaving a room clears it.

Pushers (http kind only) push through a bounded sender pool with retries,
honour event_id_only, drop push keys the gateway rejects, and go away with
their device. GET /notifications pages the history. New config section
push: enabled, include_content, gateway_timeout, notification_retention.

Account data writes now wake the user's blocked syncs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:43:36 +02:00

658 lines
23 KiB
Go

// Package pocketbase is the placeholder for the PocketBase storage driver.
//
// The plan (PLAN.md section 4) is an appliance deployment: one binary that
// also brings admin auth, a filesystem abstraction for media (local or S3)
// and realtime for the admin panel. The event DAG is a poor fit for
// PocketBase's record model, so the roomserver and syncapi tables will use
// its underlying dbx connection directly rather than its collection API.
//
// Until that work starts (Phase 10) the driver is compiled in, registered
// and fails loudly, so that `database.driver: pocketbase` is a clear error
// instead of an unknown-driver one, and so the interface stays honest:
// every method the other drivers implement exists here too.
package pocketbase
import (
"context"
"fmt"
"log/slog"
"github.com/dariuszjaniak/warp/server/internal/storage"
)
func init() {
storage.Register(storage.DriverPocketBase, Open)
}
// Database is the not-yet-implemented PocketBase driver.
type Database struct{}
// Open always fails with storage.ErrNotImplemented.
func Open(context.Context, storage.Config, *slog.Logger) (storage.Database, error) {
return nil, fmt.Errorf(
"pocketbase driver is not implemented yet (planned for Phase 10); use %q or %q: %w",
storage.DriverSQLite, storage.DriverPostgres, storage.ErrNotImplemented)
}
// Driver implements storage.Database.
func (*Database) Driver() string { return storage.DriverPocketBase }
// Ping implements storage.Database.
func (*Database) Ping(context.Context) error { return storage.ErrNotImplemented }
// Migrate implements storage.Database.
func (*Database) Migrate(context.Context) error { return storage.ErrNotImplemented }
// PendingMigrations implements storage.Database.
func (*Database) PendingMigrations(context.Context) ([]string, error) {
return nil, storage.ErrNotImplemented
}
// Settings implements storage.Database.
func (*Database) Settings() storage.SettingsStore { return notImplementedSettings{} }
// Users implements storage.Database.
func (*Database) Users() storage.UserStore { return notImplementedUsers{} }
// Rooms implements storage.Database.
func (*Database) Rooms() storage.RoomStore { return notImplementedRooms{} }
// Sync implements storage.Database.
func (*Database) Sync() storage.SyncStore { return notImplementedSync{} }
// Federation implements storage.Database.
func (*Database) Federation() storage.FederationStore { return notImplementedFederation{} }
// Media implements storage.Database.
func (*Database) Media() storage.MediaStore { return notImplementedMedia{} }
// Keys implements storage.Database.
func (*Database) Keys() storage.KeyStore { return notImplementedKeys{} }
// Push implements storage.Database.
func (*Database) Push() storage.PushStore { return notImplementedPush{} }
// Close implements storage.Database.
func (*Database) Close() error { return nil }
// notImplementedSettings satisfies storage.SettingsStore by refusing every
// call, so a partially wired-up PocketBase deployment cannot silently lose
// an admin's federation switch.
type notImplementedSettings struct{}
func (notImplementedSettings) Get(context.Context, string) (string, bool, error) {
return "", false, storage.ErrNotImplemented
}
func (notImplementedSettings) GetAll(context.Context) (map[string]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSettings) Set(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedSettings) Delete(context.Context, string) error {
return storage.ErrNotImplemented
}
// notImplementedUsers satisfies storage.UserStore the same way: a
// half-wired PocketBase deployment must not accept a registration it cannot
// store, or hand out an access token it cannot look up again.
type notImplementedUsers struct{}
func (notImplementedUsers) CreateAccount(context.Context, storage.NewAccount) (storage.Account, error) {
return storage.Account{}, storage.ErrNotImplemented
}
func (notImplementedUsers) Account(context.Context, string) (storage.Account, error) {
return storage.Account{}, storage.ErrNotImplemented
}
func (notImplementedUsers) PasswordHash(context.Context, string) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedUsers) SetPasswordHash(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetAdmin(context.Context, string, bool) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetDisplayName(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) SetAvatarURL(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) ListAccounts(context.Context, storage.AccountFilter) ([]storage.Account, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedUsers) Stats(context.Context) (storage.Stats, error) {
return storage.Stats{}, storage.ErrNotImplemented
}
func (notImplementedUsers) CreateSession(context.Context, storage.NewSession) (storage.Device, error) {
return storage.Device{}, storage.ErrNotImplemented
}
func (notImplementedUsers) RefreshSession(context.Context, storage.RefreshedSession) (storage.TokenInfo, error) {
return storage.TokenInfo{}, storage.ErrNotImplemented
}
func (notImplementedUsers) TokenInfo(context.Context, string) (storage.TokenInfo, error) {
return storage.TokenInfo{}, storage.ErrNotImplemented
}
func (notImplementedUsers) TouchDevice(context.Context, string, string, string, string, int64) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) Device(context.Context, string, string) (storage.Device, error) {
return storage.Device{}, storage.ErrNotImplemented
}
func (notImplementedUsers) Devices(context.Context, string) ([]storage.Device, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedUsers) SetDeviceDisplayName(context.Context, string, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedUsers) DeleteDevices(context.Context, string, []string) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedUsers) DeleteAllDevices(context.Context, string) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedUsers) AccountData(context.Context, string, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedUsers) SetAccountData(context.Context, string, string, string, []byte) error {
return storage.ErrNotImplemented
}
// notImplementedRooms satisfies storage.RoomStore the same way. It matters
// more here than anywhere else: a room whose create event was accepted and
// not stored would hand a client a room ID that refers to nothing.
type notImplementedRooms struct{}
func (notImplementedRooms) CreateRoom(context.Context, storage.NewRoom) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) Room(context.Context, string) (storage.Room, error) {
return storage.Room{}, storage.ErrNotImplemented
}
func (notImplementedRooms) Write(context.Context, storage.RoomWrite) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedRooms) Event(context.Context, string, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
func (notImplementedRooms) EventsByID(context.Context, string, []string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentState(context.Context, string) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentStateEvents(context.Context, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) CurrentStateEvent(context.Context, string, string, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
func (notImplementedRooms) StateBefore(context.Context, string, string) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Extremities(context.Context, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Members(context.Context, string, []string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MembershipEntries(context.Context, string, []string) ([]storage.MembershipEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) Membership(context.Context, string, string) (storage.MembershipEntry, error) {
return storage.MembershipEntry{}, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomsForUser(context.Context, string, []string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MembershipsForUser(context.Context, string, []string) ([]storage.SyncMembership, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) UserMembershipEvents(context.Context, string, string) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MaxStreamOrdering(context.Context) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedRooms) Timeline(context.Context, storage.TimelineRequest) ([]storage.StoredEvent, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomsWithEvents(context.Context, []string, int64, int64) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) StateAtStream(context.Context, string, int64) ([]storage.StateEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) TransactionEvent(context.Context, storage.TransactionRef) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedRooms) CreateAlias(context.Context, storage.RoomAlias) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) DeleteAlias(context.Context, string) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) Alias(context.Context, string) (storage.RoomAlias, error) {
return storage.RoomAlias{}, storage.ErrNotImplemented
}
func (notImplementedRooms) AliasesForRoom(context.Context, string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) SetPublished(context.Context, string, bool) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) PublishedRooms(context.Context, storage.RoomFilter) ([]storage.RoomSummary, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedRooms) ListRooms(context.Context, storage.RoomFilter) ([]storage.RoomSummary, int, error) {
return nil, 0, storage.ErrNotImplemented
}
func (notImplementedRooms) RoomSummary(context.Context, string) (storage.RoomSummary, error) {
return storage.RoomSummary{}, storage.ErrNotImplemented
}
func (notImplementedRooms) PurgeRoom(context.Context, string) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) RoomStats(context.Context) (storage.RoomStats, error) {
return storage.RoomStats{}, storage.ErrNotImplemented
}
func (notImplementedRooms) WriteOutliers(context.Context, string, []storage.StoredEvent) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) WriteBackfill(context.Context, string, []storage.BackfillEvent) error {
return storage.ErrNotImplemented
}
func (notImplementedRooms) InviteState(context.Context, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) HasJoinedMemberOn(context.Context, string, string) (bool, error) {
return false, storage.ErrNotImplemented
}
func (notImplementedRooms) EventByID(context.Context, string) (storage.StoredEvent, error) {
return storage.StoredEvent{}, storage.ErrNotImplemented
}
func (notImplementedRooms) UsersSharingRooms(context.Context, string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedRooms) MemberChanges(context.Context, []string, int64, int64) ([]storage.MemberChange, error) {
return nil, storage.ErrNotImplemented
}
// notImplementedFederation satisfies storage.FederationStore the same way.
// A queue that accepted events and never stored them would lose them the
// moment federation was switched on.
type notImplementedFederation struct{}
func (notImplementedFederation) StoreServerKeys(context.Context, []storage.ServerKey) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) ServerKeys(context.Context, string, []string) ([]storage.ServerKey, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Enqueue(context.Context, []storage.QueuedItem) ([]storage.QueuedItem, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Queued(context.Context, string, string, int) ([]storage.QueuedItem, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Dequeue(context.Context, []int64) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) QueuedDestinations(context.Context) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) Destination(context.Context, string) (storage.Destination, error) {
return storage.Destination{}, storage.ErrNotImplemented
}
func (notImplementedFederation) SetDestination(context.Context, storage.Destination) error {
return storage.ErrNotImplemented
}
func (notImplementedFederation) Destinations(context.Context) ([]storage.DestinationSummary, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedFederation) InboundTransaction(context.Context, string, string) ([]byte, bool, error) {
return nil, false, storage.ErrNotImplemented
}
func (notImplementedFederation) RecordInboundTransaction(context.Context, string, string, []byte, int64) error {
return storage.ErrNotImplemented
}
// notImplementedSync satisfies storage.SyncStore the same way. A sync that
// returned an empty success would be worse than an error: a client would
// take it for "nothing has happened" and stop asking.
type notImplementedSync struct{}
func (notImplementedSync) StreamPositions(context.Context) (storage.StreamPositions, error) {
return storage.StreamPositions{}, storage.ErrNotImplemented
}
func (notImplementedSync) SetReceipt(context.Context, storage.Receipt) (int64, bool, error) {
return 0, false, storage.ErrNotImplemented
}
func (notImplementedSync) Receipts(context.Context, []string, int64, int64) ([]storage.Receipt, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) ReceiptsForUser(context.Context, string, []string) ([]storage.Receipt, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) SendToDevice(context.Context, []storage.NewToDeviceMessage) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedSync) ToDeviceMessages(context.Context, string, string, int64, int64, int) ([]storage.ToDeviceMessage, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) DeleteToDeviceMessages(context.Context, string, string, int64) error {
return storage.ErrNotImplemented
}
func (notImplementedSync) SetPresence(context.Context, storage.PresenceState) (int64, bool, error) {
return 0, false, storage.ErrNotImplemented
}
func (notImplementedSync) Presence(context.Context, string) (storage.PresenceState, error) {
return storage.PresenceState{}, storage.ErrNotImplemented
}
func (notImplementedSync) PresenceForUsers(context.Context, []string, int64, int64) ([]storage.PresenceState, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) AccountDataChanges(context.Context, string, int64, int64) ([]storage.AccountDataEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) AccountDataAt(context.Context, string, int64) ([]storage.AccountDataEntry, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedSync) SaveFilter(context.Context, string, []byte, int64) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedSync) Filter(context.Context, string, string) ([]byte, error) {
return nil, storage.ErrNotImplemented
}
// notImplementedMedia satisfies storage.MediaStore the same way. An upload
// whose metadata was not stored would hand the client an mxc:// URI that
// refers to nothing.
type notImplementedMedia struct{}
func (notImplementedMedia) CreateMedia(context.Context, storage.Media) error {
return storage.ErrNotImplemented
}
func (notImplementedMedia) Media(context.Context, string, string) (storage.Media, error) {
return storage.Media{}, storage.ErrNotImplemented
}
func (notImplementedMedia) CompleteUpload(context.Context, string, string, storage.MediaContent) error {
return storage.ErrNotImplemented
}
func (notImplementedMedia) PendingUploads(context.Context, string, int64) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedMedia) DeleteExpiredUploads(context.Context, int64) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedMedia) Thumbnail(context.Context, string, string, int, int, string) (storage.Thumbnail, error) {
return storage.Thumbnail{}, storage.ErrNotImplemented
}
func (notImplementedMedia) StoreThumbnail(context.Context, storage.Thumbnail) error {
return storage.ErrNotImplemented
}
// Compile-time proof that the stub keeps up with the interfaces.
var (
_ storage.Database = (*Database)(nil)
_ storage.UserStore = notImplementedUsers{}
_ storage.RoomStore = notImplementedRooms{}
_ storage.SyncStore = notImplementedSync{}
_ storage.FederationStore = notImplementedFederation{}
_ storage.MediaStore = notImplementedMedia{}
_ storage.KeyStore = notImplementedKeys{}
)
// notImplementedKeys satisfies storage.KeyStore the same way. A key upload
// that reported success and kept nothing would leave every message sent to
// the device undecryptable.
type notImplementedKeys struct{}
func (notImplementedKeys) SetDeviceKeys(context.Context, storage.DeviceKeys) (storage.DeviceListUpdate, error) {
return storage.DeviceListUpdate{}, storage.ErrNotImplemented
}
func (notImplementedKeys) DeviceKeys(context.Context, string, []string) ([]storage.DeviceKeys, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) DeleteDeviceKeys(context.Context, string, []string) ([]storage.DeviceListUpdate, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) AddOneTimeKeys(context.Context, string, string, []storage.OneTimeKey) error {
return storage.ErrNotImplemented
}
func (notImplementedKeys) OneTimeKeyCounts(context.Context, string, string) (map[string]int, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) SetFallbackKeys(context.Context, string, string, []storage.OneTimeKey) error {
return storage.ErrNotImplemented
}
func (notImplementedKeys) UnusedFallbackKeyTypes(context.Context, string, string) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) ClaimKey(context.Context, string, string, string) (storage.ClaimedKey, bool, error) {
return storage.ClaimedKey{}, false, storage.ErrNotImplemented
}
func (notImplementedKeys) SetCrossSigningKeys(context.Context, string, map[string][]byte) (storage.DeviceListUpdate, error) {
return storage.DeviceListUpdate{}, storage.ErrNotImplemented
}
func (notImplementedKeys) CrossSigningKeys(context.Context, string) (map[string][]byte, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) AddSignatures(context.Context, []storage.KeySignature) ([]storage.DeviceListUpdate, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) Signatures(context.Context, string) ([]storage.KeySignature, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) MarkDeviceListsChanged(context.Context, []string) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedKeys) DeviceListChanges(context.Context, []string, int64, int64) ([]storage.DeviceListChange, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) DeviceListPosition(context.Context, string) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedKeys) CreateBackup(context.Context, string, string, []byte) (string, error) {
return "", storage.ErrNotImplemented
}
func (notImplementedKeys) Backup(context.Context, string, string) (storage.BackupVersion, error) {
return storage.BackupVersion{}, storage.ErrNotImplemented
}
func (notImplementedKeys) UpdateBackup(context.Context, string, string, []byte) error {
return storage.ErrNotImplemented
}
func (notImplementedKeys) DeleteBackup(context.Context, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedKeys) PutBackupKeys(context.Context, string, string, []storage.BackupKey) (storage.BackupVersion, error) {
return storage.BackupVersion{}, storage.ErrNotImplemented
}
func (notImplementedKeys) BackupKeys(context.Context, string, string, string, string) ([]storage.BackupKey, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) DeleteBackupKeys(context.Context, string, string, string, string) (storage.BackupVersion, error) {
return storage.BackupVersion{}, storage.ErrNotImplemented
}
func (notImplementedKeys) RemoteKeys(context.Context, string) (storage.RemoteKeys, bool, error) {
return storage.RemoteKeys{}, false, storage.ErrNotImplemented
}
func (notImplementedKeys) SetRemoteKeys(context.Context, storage.RemoteKeys) (bool, error) {
return false, storage.ErrNotImplemented
}
func (notImplementedKeys) UpdateRemoteDevice(context.Context, storage.RemoteDeviceUpdate) (storage.RemoteUpdateResult, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedKeys) SetRemoteCrossSigningKeys(context.Context, string, map[string][]byte) (bool, error) {
return false, storage.ErrNotImplemented
}
func (notImplementedKeys) RemoteKeyUsers(context.Context) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedKeys) DeleteRemoteKeys(context.Context, []string) error {
return storage.ErrNotImplemented
}
// notImplementedPush is the push store until the driver has one.
type notImplementedPush struct{}
func (notImplementedPush) Pushers(context.Context, string) ([]storage.Pusher, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedPush) SetPusher(context.Context, storage.Pusher, bool) error {
return storage.ErrNotImplemented
}
func (notImplementedPush) DeletePusher(context.Context, string, string, string) error {
return storage.ErrNotImplemented
}
func (notImplementedPush) DeletePushersForDevices(context.Context, string, []string) error {
return storage.ErrNotImplemented
}
func (notImplementedPush) AddNotifications(context.Context, []storage.NewNotification) (int64, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedPush) MarkNotificationsRead(context.Context, string, string, int64) (int64, bool, error) {
return 0, false, storage.ErrNotImplemented
}
func (notImplementedPush) NotificationCounts(context.Context, string, []string) (map[string]storage.NotificationCounts, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedPush) NotificationCountChanges(context.Context, string, int64, int64) ([]string, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedPush) UnreadNotifications(context.Context, string) (int, error) {
return 0, storage.ErrNotImplemented
}
func (notImplementedPush) Notifications(context.Context, string, int64, int, bool) ([]storage.Notification, error) {
return nil, storage.ErrNotImplemented
}
func (notImplementedPush) PruneNotifications(context.Context, int64) (int64, error) {
return 0, storage.ErrNotImplemented
}