feat(calendar): en læst Google-kalender, med sit eget endpoint og sin egen sweep
Samme registrering som Gmail, et andet scope og en anden redirect-URI. Google udsteder en refresh-token pr. godkendelse og holder op til hundrede i live pr. konto, hver med sine egne scopes — så en forbundet kalender rører ikke en Gmail, der virker, og at afbryde den ene tager ikke den anden med. Callbacken har sin egen sti. Én callback, der ud fra gemt tilstand afgjorde, hvilket af de to flows der kom tilbage, ville lægge valget af *hvilket token-lager der skrives* bag et opslag — og går det galt, går det galt ved at overskrive en virkende Gmail-forbindelse med kalender-tokens. Begivenhederne caches i et vindue på 90 dage tilbage og et år frem. Et månedsgitter bladrer, og et kald til Google pr. piletast er en kalender, der er langsommere end den kalender, den viser. Vinduet har en kant, og endpointet siger hvor den er: et tomt svar uden forklaring er en kalender, der lydløst påstår, at en dag er fri.
This commit is contained in:
@@ -41,6 +41,10 @@ type Runner struct {
|
||||
// reason as SyncGmail: it owns turning a message into a task.
|
||||
SyncMailbox func(ctx context.Context, user *store.User, m *store.Mailbox) (int, error)
|
||||
|
||||
// SyncCalendars refreshes one person's connected calendars. Supplied the same
|
||||
// way and for the same reason: the HTTP layer owns the token refresh.
|
||||
SyncCalendars func(ctx context.Context, user *store.User) (int, error)
|
||||
|
||||
// Push delivers a notification to somebody's devices. Supplied by the HTTP
|
||||
// layer, which owns the VAPID keys and the subscription list.
|
||||
Push func(userID, title, body, projectID string)
|
||||
@@ -69,6 +73,13 @@ func (r *Runner) Start(ctx context.Context) {
|
||||
// account for a to-do app. Ten minutes is fast enough for something somebody
|
||||
// starred and slow enough not to look like abuse.
|
||||
r.every(ctx, "mailboxes", 10*time.Minute, r.syncMailboxes)
|
||||
// Fifteen minutes rather than ten. A calendar is not a mailbox: nothing here
|
||||
// becomes a task, so being a quarter of an hour behind on a meeting next
|
||||
// Tuesday costs nothing — and the "fetch now" button covers the one case where
|
||||
// it matters, which is somebody who has just moved something and is looking at
|
||||
// the grid to check. A person who has just connected does not wait for this
|
||||
// either; connecting fetches the list, and the first sync is one click away.
|
||||
r.every(ctx, "calendars", 15*time.Minute, r.syncCalendars)
|
||||
}
|
||||
|
||||
func (r *Runner) Wait() { r.wg.Wait() }
|
||||
@@ -322,6 +333,44 @@ func (r *Runner) syncMailboxes(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- calendars ------------------------------------------------------------------------
|
||||
|
||||
// syncCalendars refreshes everybody's connected calendars.
|
||||
//
|
||||
// Its own sweep rather than a branch inside the mailbox one. They share nothing
|
||||
// past the word "poll": that loop asks each mailbox how far it has read and makes
|
||||
// tasks, this one replaces a window of somebody else's events. Folding them
|
||||
// together would mean one interval for two questions that do not want the same
|
||||
// answer, and a Google that is down would count against a mailbox that is not.
|
||||
//
|
||||
// Nobody is waiting, so no budget of its own — the request handler has one because
|
||||
// a person is watching a spinner. One account that has gone quiet costs this run
|
||||
// its own timeout and nothing else: the loop moves on per person.
|
||||
func (r *Runner) syncCalendars(ctx context.Context) error {
|
||||
if r.SyncCalendars == nil {
|
||||
return nil
|
||||
}
|
||||
userIDs, err := r.db.UsersWithCalendars(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, id := range userIDs {
|
||||
user, err := r.db.UserByID(ctx, id)
|
||||
if err != nil || user == nil {
|
||||
continue
|
||||
}
|
||||
written, err := r.SyncCalendars(ctx, user)
|
||||
if err != nil {
|
||||
r.log.Warn("calendar sync", "err", err, "user", id)
|
||||
continue
|
||||
}
|
||||
if written > 0 {
|
||||
r.log.Info("calendar refreshed", "user", id, "events", written)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- housekeeping -----------------------------------------------------------------
|
||||
|
||||
func (r *Runner) emptyTrash(ctx context.Context) error {
|
||||
|
||||
Reference in New Issue
Block a user