view calsync/gcalclient/gcalclient.go @ 52:5f7c393577e9

now gets sync updates (for 30 sec)
author drewp@bigasterisk.com
date Mon, 19 Aug 2024 19:19:13 -0700
parents a9b720445bcf
children f248f018a663
line wrap: on
line source

package gcalclient

import (
	"context"
	"log"
	"net/url"
	"strings"
	"time"

	"bigasterisk.com/go/gcalendarwatch/mongoclient"
	"google.golang.org/api/calendar/v3"
)

type GCalClient struct {
	ctx context.Context
	srv *calendar.Service
}

// Same as calendar.Event, but includes our urls
type CalendarEvent struct {
	*calendar.Event
	CalendarUrl string
	EventUrl    string
}

func MakeCalUrl(calId string) string {
	return "http://bigasterisk.com/calendar/" + url.QueryEscape(calId)
}

func MakeEventUrl(calUrl string, evId string) string {
	return calUrl + "/" + url.QueryEscape(evId)
}

func MakeEventUrl3(googleCalId string, evId string) string {
	return MakeEventUrl("http://bigasterisk.com/calendar/"+
		url.QueryEscape(googleCalId), evId)
}

func New(ctx context.Context) (*GCalClient, error) {
	err, srv := newService(ctx)
	if err != nil {
		log.Fatalf("Unable to retrieve Calendar client: %v", err)
	}
	return &GCalClient{ctx, srv}, nil
}

func (gc *GCalClient) Close() {
	// todo: disconnect watches if possible
}

func (gc *GCalClient) AllCalendars(maxResults int64) ([]*calendar.CalendarListEntry, error) {
	// todo: pagination
	list, err := gc.srv.CalendarList.List().MaxResults(maxResults).Do()
	if err != nil {
		return nil, err
	}
	list.Items = list.Items[:4]
	return list.Items, nil
}

type FindEventsMessage struct {
	// either non-nil this:
	Event *CalendarEvent
	// or these:
	CalId                    string
	OlderThanThisIsDeletable time.Time
}

// FindEvents considers all calendars. It runs forever.
func (gc *GCalClient) FindEvents(
	mc *mongoclient.MongoClient,
	// For each calendar, after events in this time range have been sent to
	// `out`, the chan will get the other kind of FindEventsMessage (CalId,
	// ...). That message signals that the caller may cull old events on the given
	// calendar. After that point, all events will be updates (including
	// deletes).
	initialFillStart, initialFillEnd time.Time,
	out chan *FindEventsMessage,
) error {

	cals, err := mc.GetAllCals()
	if err != nil {
		return err
	}
	log.Println("reading", len(cals), "calendars")
	for calNum, cal := range cals {
		t := time.Now()
		log.Println("  cal", calNum, cal.Url)
		log.Println("  cal", calNum, "readEventsInRange", "from", initialFillStart, "to", initialFillEnd)
		syncToken, err := gc.readEventsInRange(&cal, initialFillStart, initialFillEnd, out)
		if err != nil {
			return err
		}

		out <- &FindEventsMessage{nil, cal.GoogleId, t}

		ew := gc.NewEventWatch(&cal, t, syncToken, out)

		for loop := 0; loop < 30; loop++ {
			log.Println("")
			log.Println("tail loop", loop, "for", cal.Url)
			err := ew.GetMoreEvents()
			if err != nil {
				return err
			}
			time.Sleep(2 * time.Second)
		}
	}

	return nil
}

// Synchronous.
func (gc *GCalClient) readEventsInRange(
	cal *mongoclient.MongoCal,
	initialFillStart, initialFillEnd time.Time,
	out chan *FindEventsMessage,
) (string, error) {
	log.Println(
		"    get initial events for", cal.Url, "between",
		initialFillStart, "and", initialFillEnd)

	pageToken := ""
	syncToken := ""

	for {
		log.Println("      getting another page", pageToken)
		events, err := rangedEventsCall(gc.srv, cal.GoogleId, initialFillStart, initialFillEnd, pageToken).Do()
		if err != nil {
			return "", err
		}

		log.Println("        got", len(events.Items), "events, sync=", events.NextSyncToken)
		if len(events.Items) == 0 {
			break
		}

		sendEvents(events, cal, out)

		syncToken = events.NextSyncToken
		if events.NextPageToken == "" {
			break
		}
		pageToken = events.NextPageToken
	}
	return syncToken, nil
}

// Send a page of calendar.Events over a channel, as CalendarEvent structs.
func sendEvents(events *calendar.Events, cal *mongoclient.MongoCal, out chan *FindEventsMessage) {
	for _, event := range events.Items {
		if event.Status == "cancelled" {
			log.Fatal("todo")
		}
		out <- &FindEventsMessage{
			Event: &CalendarEvent{
				Event:       event,
				CalendarUrl: cal.Url,
				EventUrl:    MakeEventUrl(cal.Url, event.Id),
			}}
	}
}

type eventWatch struct {
	gc            *GCalClient
	cal           *mongoclient.MongoCal
	nextSyncToken string
	nextPageToken string
	modSince      time.Time
	out           chan *FindEventsMessage
}

func (gc *GCalClient) NewEventWatch(
	cal *mongoclient.MongoCal,
	modSince time.Time,
	syncToken string,
	out chan *FindEventsMessage,
) *eventWatch {
	ew := &eventWatch{gc, cal, syncToken, "", modSince, out}
	return ew
}

// Call this when there are likely new changes to sync.
func (w *eventWatch) GetMoreEvents() error {
	call := syncEventsCall(w.gc.srv, w.cal.GoogleId)
	log.Println("listing events on", w.cal.GoogleId, "with")

	if w.nextPageToken != "" {
		call = call.PageToken(w.nextPageToken)
		log.Println("   pageToken", w.nextPageToken)
	} else if w.nextSyncToken != "" {
		call = call.SyncToken(w.nextSyncToken)
		log.Println("   syncToken", w.nextSyncToken)
	} else {
		call = call.UpdatedMin((w.modSince.Format(time.RFC3339)))
		log.Println("   updatedMin", w.modSince.Format(time.RFC3339))
	}
	ret, err := call.Do()
	if err != nil {
		return err
	}
	w.nextSyncToken = ret.NextSyncToken
	w.nextPageToken = ret.NextPageToken
	log.Println(len(ret.Items), "more events received")
	sendEvents(ret, w.cal, w.out)
	log.Println("got nextSyncToken=", w.nextSyncToken)
	log.Println("got nextPageToken=", w.nextPageToken)
	return err
}

func rangedEventsCall(srv *calendar.Service, calGoogleId string,
	initialFillStart, initialFillEnd time.Time, pageToken string) *calendar.EventsListCall {
	return srv.Events.List(calGoogleId).
		ShowDeleted(false).
		SingleEvents(true).
		TimeMin(initialFillStart.Format(time.RFC3339)).
		TimeMax(initialFillEnd.Format(time.RFC3339)).
		MaxResults(4).
		PageToken(pageToken)
}

func syncEventsCall(srv *calendar.Service, calGoogleId string) *calendar.EventsListCall {
	return srv.Events.List(calGoogleId).
		ShowDeleted(true).
		SingleEvents(true).
		MaxResults(4)
}