package api import ( "context" "fmt" "net/url" "time" "github.com/ponzischeme89/memby/server/internal/adminevents" "github.com/ponzischeme89/memby/server/internal/store" ) // noteDailyFirstUse announces the first time a television opened Memby on a given day. // // It is called from the home request rather than from the auth middleware, which every // call passes through, because "opened Memby" means the launcher composing. A set left on // overnight polls /v1/status every ten seconds, so anchoring on any authenticated request // would announce that set at midnight — an event nobody did, timed to the hour nobody is // reading the feed. // // It is called before the cached response is served, deliberately: a household whose home // row cache is still warm from the last set to switch on has still just been opened by // this one, and the mark is a single indexed insert either way. // // Everything about it is best-effort. It cannot fail a launcher. func (s *Server) noteDailyFirstUse(ctx context.Context, sess store.Session) { if s.store == nil || sess.DeviceID == "" { return } first, err := s.store.MarkDeviceDay( ctx, sess.DeviceID, sess.EmbyUserID, time.Now().In(s.sonarrLocation()), ) if err != nil { s.loggerFor(ctx).Warn("device day mark failed", "device_id", sess.DeviceID, "error", err) return } if !first { return } s.loggerFor(ctx).Info("first use today", "device_id", sess.DeviceID, "client", clientLogValue(sess.ClientVersion)) s.publishAdmin(ctx, adminevents.Event{ Type: adminevents.TypeDeviceFirstUse, Title: "Opened Memby", Summary: fmt.Sprintf("%s opened Memby on %s for the first time today", displayName(sess.Username), displayName(sess.DeviceName)), Actor: sess.Username, Target: sess.DeviceName, Link: "/admin/devices/" + url.PathEscape(sess.DeviceID), Metadata: adminevents.Meta(map[string]any{ "deviceId": sess.DeviceID, "userId": sess.EmbyUserID, "version": sess.ClientVersion, }), }) } // announceDeviceUpdate reports a television that has finished updating itself. // // A set that updates in place never signs in again, so the first request carrying the new // build is the only evidence there is that the update worked — and an update that was // offered and never taken looks exactly like one that was, until this arrives. // // `from` being empty is not an update: it is a television this gateway had never been // told the build of, which is an old APK or a session predating identity capture, and // announcing it as an upgrade would claim a version moved when nothing is known to have // moved. A version that went *backwards* is still announced, because a sideloaded // downgrade is news an operator wants at least as much as an upgrade. func (s *Server) announceDeviceUpdate(ctx context.Context, sess store.Session, from string) { if from == "" || sess.ClientVersion == "" || from == sess.ClientVersion { return } s.loggerFor(ctx).Info("client build changed", "device_id", sess.DeviceID, "from", from, "to", sess.ClientVersion) s.publishAdmin(ctx, adminevents.Event{ Type: adminevents.TypeDeviceUpdated, Title: "App updated", Summary: fmt.Sprintf("%s updated from %s to %s", displayName(sess.DeviceName), from, sess.ClientVersion), Actor: sess.Username, Target: sess.DeviceName, Link: "/admin/devices/" + url.PathEscape(sess.DeviceID), Metadata: adminevents.Meta(map[string]any{ "deviceId": sess.DeviceID, "userId": sess.EmbyUserID, "from": from, "to": sess.ClientVersion, }), }) }