Files
memby/server/internal/store/device_activity.go
T
2026-08-17 19:09:17 +12:00

74 lines
2.7 KiB
Go

package store
import (
"context"
"fmt"
"time"
)
// DeviceActivityRetention is how long the per-day marks are kept. They are not history
// anybody reads — the notification they produced is the record, and that is itself pruned
// at thirty days — so this only has to be long enough that a clock correction or a
// database restored from a backup cannot make yesterday look like a day that never
// happened.
const DeviceActivityRetention = 90 * 24 * time.Hour
// MarkDeviceDay records that a television was in use on a household-local day, and
// reports whether this is the first time it has been seen today.
//
// The answer comes from the insert rather than from a read followed by a write, because
// every set in the house can reach this at once and two of them racing on the same row
// must not both be told they were first. `ON CONFLICT DO NOTHING` makes the primary key
// the arbiter: exactly one insert affects a row.
//
// It is keyed on the viewer as well as the television because a household set that two
// people sign into is two people opening Memby, and an operator reading the feed wants to
// know which of them it was.
func (s *Store) MarkDeviceDay(
ctx context.Context, deviceID, userID string, day time.Time,
) (bool, error) {
if deviceID == "" {
return false, nil
}
tag, err := s.pool.Exec(ctx, `
INSERT INTO device_activity_days (device_id, emby_user_id, day)
VALUES ($1, $2, $3)
ON CONFLICT (device_id, emby_user_id, day) DO NOTHING`,
deviceID, userID, day.Format("2006-01-02"))
if err != nil {
return false, fmt.Errorf("store: mark device day: %w", err)
}
return tag.RowsAffected() > 0, nil
}
// PruneDeviceActivityDays drops marks older than the retention window.
func (s *Store) PruneDeviceActivityDays(
ctx context.Context, retention time.Duration,
) (int64, error) {
if retention <= 0 {
retention = DeviceActivityRetention
}
tag, err := s.pool.Exec(ctx,
`DELETE FROM device_activity_days WHERE first_seen_at < now() - $1::interval`,
fmt.Sprintf("%d seconds", int64(retention.Seconds())))
if err != nil {
return 0, fmt.Errorf("store: prune device activity days: %w", err)
}
return tag.RowsAffected(), nil
}
// DeleteDeviceActivityDays retires a television's marks along with the television, the
// way its build history is retired: a set that is gone must not be able to announce a
// first use it can no longer have.
func (s *Store) DeleteDeviceActivityDays(ctx context.Context, deviceIDs ...string) error {
if len(deviceIDs) == 0 {
return nil
}
_, err := s.pool.Exec(ctx,
`DELETE FROM device_activity_days WHERE device_id = ANY($1::text[])`, deviceIDs)
if err != nil {
return fmt.Errorf("store: delete device activity days: %w", err)
}
return nil
}