changeset 49:2991c1166852

start calsync in go. Calendar list seems to sync
author drewp@bigasterisk.com
date Mon, 19 Aug 2024 13:25:03 -0700
parents f2dd88b9964c
children dade5bbd02e3
files calsync/cal_sync.go calsync/convert/convert.go calsync/event_sync.go calsync/gcalclient/gcalclient.go calsync/gcalclient/service.go calsync/go.mod calsync/go.sum calsync/main.go calsync/mongoclient/mongoclient.go
diffstat 9 files changed, 798 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/cal_sync.go	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,29 @@
+package main
+
+import (
+	"log"
+
+	"bigasterisk.com/go/gcalendarwatch/convert"
+	"bigasterisk.com/go/gcalendarwatch/gcalclient"
+	"bigasterisk.com/go/gcalendarwatch/mongoclient"
+)
+
+func updateMongoCalsToMatchGoogle(mc *mongoclient.MongoClient, gc *gcalclient.GCalClient) (err error) {
+	log.Println("updateMongoCalsToMatchGoogle")
+
+	seen := make(map[string]bool)
+
+	cals, err := gc.AllCalendars()
+	if err != nil {
+		return err
+	}
+
+	for _, cal := range cals {
+		calUrl := gcalclient.MakeCalUrl(cal.Id)
+		log.Println("syncing", calUrl)
+		seen[calUrl] = true
+		mc.UpsertOneCal(convert.MongoCalFromGoogleCal(cal))
+	}
+
+	return mc.DeleteCalsNotInSet(seen)
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/convert/convert.go	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,68 @@
+package convert
+
+import (
+	"net/url"
+	"time"
+
+	"bigasterisk.com/go/gcalendarwatch/gcalclient"
+	"bigasterisk.com/go/gcalendarwatch/mongoclient"
+	"google.golang.org/api/calendar/v3"
+)
+
+func MongoCalFromGoogleCal(cal *calendar.CalendarListEntry) mongoclient.MongoCal {
+	return mongoclient.MongoCal{
+		Url:         gcalclient.MakeCalUrl(cal.Id),
+		GoogleId:    cal.Id,
+		Summary:     cal.Summary,
+		Description: cal.Description,
+	}
+}
+
+func MakeEventUrl(calUrl string, evId string) string {
+	return calUrl + "/" + url.QueryEscape(evId)
+}
+
+func MakeEventUrl2(cal mongoclient.MongoCal, evId string) string {
+	return MakeEventUrl3(cal.GoogleId, evId)
+}
+
+func MakeEventUrl3(googleCalId string, evId string) string {
+	return MakeEventUrl("http://bigasterisk.com/calendar/"+
+		url.QueryEscape(googleCalId), evId)
+}
+
+func MongoEventFromGoogleEvent2(
+	calUrl string,
+	ev *gcalclient.CalendarEvent,
+	now time.Time,
+) mongoclient.MongoEvent {
+	return mongoEventFromGoogleEvent(calUrl, ev, now)
+
+}
+func MongoEventFromGoogleEvent(
+	cal *calendar.CalendarListEntry,
+	ev *gcalclient.CalendarEvent,
+	now time.Time,
+) mongoclient.MongoEvent {
+	return mongoEventFromGoogleEvent(MakeEventUrl3(cal.Id, ev.Id), ev, now)
+}
+
+func mongoEventFromGoogleEvent(calUrl string, ev *gcalclient.CalendarEvent, now time.Time) mongoclient.MongoEvent {
+	return mongoclient.MongoEvent{
+		Url:      calUrl,
+		GoogleId: ev.Event.Id,
+		HtmlLink: ev.HtmlLink,
+		Title:    ev.Summary, //?
+		// FeedId             : ev.Event.FeedId, // or calid?
+		// FeedTitle          : ev.Event.FeedTitle, // /or what
+		EndTimeUnspecified: ev.Event.EndTimeUnspecified,
+		// Start              : ev.Start.DateTime,
+		// StartDate          : ev.Event.StartDate,
+		// StartTime          : ev.Event.StartTime,
+		// End                : ev.Event.End,
+		// EndDate            : ev.Event.EndDate,
+		// EndTime            : ev.Event.EndTime,
+		LastUpdated: now,
+	}
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/event_sync.go	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,25 @@
+package main
+
+import (
+	"log"
+	"time"
+
+	"bigasterisk.com/go/gcalendarwatch/convert"
+	"bigasterisk.com/go/gcalendarwatch/gcalclient"
+	"bigasterisk.com/go/gcalendarwatch/mongoclient"
+)
+
+func updateMongoEventsToMatchGoogle(mc *mongoclient.MongoClient, gc *gcalclient.GCalClient) error {
+	t := time.Now()
+	events, err := gc.FindEvents(t)
+	if err != nil {
+		return err
+	}
+	log.Println("Found", len(events), "events")
+	// todo: wipe mongo time period
+	log.Println("Upcoming events:")
+	for _, ev := range events {
+		mc.UpsertOneEvent(convert.MongoEventFromGoogleEvent2(ev.CalendarUrl, ev, t))
+	}
+	return nil
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/gcalclient/gcalclient.go	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,81 @@
+package gcalclient
+
+import (
+	"context"
+	"log"
+	"net/url"
+	"time"
+
+	"google.golang.org/api/calendar/v3"
+)
+
+type GCalClient struct {
+	ctx context.Context
+	srv *calendar.Service
+}
+
+// Same as calendar.Event, but includes the source calendar url
+type CalendarEvent struct {
+	*calendar.Event
+	CalendarUrl string
+}
+
+func MakeCalUrl(calId string) string {
+	return "http://bigasterisk.com/calendar/" + url.QueryEscape(calId)
+}
+
+func New(ctx context.Context) (*GCalClient, error) {
+	// If modifying these scopes, delete your previously saved token.json.
+	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() ([]*calendar.CalendarListEntry, error) {
+	// todo: pagination
+	list, err := gc.srv.CalendarList.List().MaxResults(100).Do()
+	if err != nil {
+		return nil, err
+	}
+	list.Items = list.Items[:4]
+	return list.Items, nil
+}
+
+// FindEvents considers all calendars
+func (gc *GCalClient) FindEvents(s time.Time) ([]*CalendarEvent, error) {
+	cals, err := gc.AllCalendars()
+	if err != nil {
+		return nil, err
+	}
+
+	ret := make([]*CalendarEvent, 0)
+	for _, cal := range cals {
+		calUrl := MakeCalUrl(cal.Id)
+		log.Println("  getting events from ", calUrl)
+		list, err := gc.srv.
+			Events.List(cal.Id).
+			ShowDeleted(false).
+			SingleEvents(true).
+			TimeMin(s.Format(time.RFC3339)).
+			MaxResults(2).
+			OrderBy("startTime").
+			Do()
+		if err != nil {
+			return nil, err
+		}
+		for _, event := range list.Items {
+			ev := &CalendarEvent{
+				Event:       event,
+				CalendarUrl: calUrl,
+			}
+			ret = append(ret, ev)
+		}
+	}
+	return ret, nil
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/gcalclient/service.go	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,86 @@
+package gcalclient
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+
+	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/google"
+	"google.golang.org/api/calendar/v3"
+	"google.golang.org/api/option"
+)
+
+// Retrieve a token, saves the token, then returns the generated client.
+func getClient(config *oauth2.Config) *http.Client {
+	// The file token.json stores the user's access and refresh tokens, and is
+	// created automatically when the authorization flow completes for the first
+	// time.
+	tokFile := "token.json"
+	tok, err := tokenFromFile(tokFile)
+	if err != nil {
+		tok = getTokenFromWeb(config)
+		saveToken(tokFile, tok)
+	}
+	return config.Client(context.Background(), tok)
+}
+
+// Request a token from the web, then returns the retrieved token.
+func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
+	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
+	fmt.Printf("Go to the following link in your browser then type the "+
+		"authorization code: \n%v\n", authURL)
+
+	var authCode string
+	if _, err := fmt.Scan(&authCode); err != nil {
+		log.Fatalf("Unable to read authorization code: %v", err)
+	}
+
+	tok, err := config.Exchange(context.TODO(), authCode)
+	if err != nil {
+		log.Fatalf("Unable to retrieve token from web: %v", err)
+	}
+	return tok
+}
+
+// Retrieves a token from a local file.
+func tokenFromFile(file string) (*oauth2.Token, error) {
+	f, err := os.Open(file)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	tok := &oauth2.Token{}
+	err = json.NewDecoder(f).Decode(tok)
+	return tok, err
+}
+
+// Saves a token to a file path.
+func saveToken(path string, token *oauth2.Token) {
+	fmt.Printf("Saving credential file to: %s\n", path)
+	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
+	if err != nil {
+		log.Fatalf("Unable to cache oauth token: %v", err)
+	}
+	defer f.Close()
+	json.NewEncoder(f).Encode(token)
+}
+
+func newService(ctx context.Context) (error, *calendar.Service) {
+	b, err := os.ReadFile("./credentials.json")
+	if err != nil {
+		log.Fatalf("Unable to read client secret file: %v", err)
+	}
+
+	config, err := google.ConfigFromJSON(b, calendar.CalendarReadonlyScope)
+	if err != nil {
+		log.Fatalf("Unable to parse client secret file to config: %v", err)
+	}
+	client := getClient(config)
+
+	srv, err := calendar.NewService(ctx, option.WithHTTPClient(client))
+	return err, srv
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/go.mod	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,41 @@
+module bigasterisk.com/go/gcalendarwatch
+
+go 1.21.1
+
+require (
+	cloud.google.com/go/auth v0.8.1 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
+	cloud.google.com/go/compute/metadata v0.5.0 // indirect
+	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/go-logr/logr v1.4.2 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/snappy v0.0.4 // indirect
+	github.com/google/s2a-go v0.1.8 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
+	github.com/googleapis/gax-go/v2 v2.13.0 // indirect
+	github.com/gorilla/mux v1.8.1 // indirect
+	github.com/klauspost/compress v1.13.6 // indirect
+	github.com/montanaflynn/stats v0.7.1 // indirect
+	github.com/xdg-go/pbkdf2 v1.0.0 // indirect
+	github.com/xdg-go/scram v1.1.2 // indirect
+	github.com/xdg-go/stringprep v1.0.4 // indirect
+	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
+	go.mongodb.org/mongo-driver v1.16.1 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
+	go.opentelemetry.io/otel v1.24.0 // indirect
+	go.opentelemetry.io/otel/metric v1.24.0 // indirect
+	go.opentelemetry.io/otel/trace v1.24.0 // indirect
+	golang.org/x/crypto v0.25.0 // indirect
+	golang.org/x/net v0.27.0 // indirect
+	golang.org/x/oauth2 v0.22.0 // indirect
+	golang.org/x/sync v0.8.0 // indirect
+	golang.org/x/sys v0.22.0 // indirect
+	golang.org/x/text v0.16.0 // indirect
+	google.golang.org/api v0.192.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf // indirect
+	google.golang.org/grpc v1.64.1 // indirect
+	google.golang.org/protobuf v1.34.2 // indirect
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/go.sum	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,181 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
+cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo=
+cloud.google.com/go/auth v0.8.1/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
+cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
+cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
+github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
+github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
+github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
+github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
+github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
+github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
+github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
+github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
+github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
+github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
+github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
+github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
+github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
+github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8=
+go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
+go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
+go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
+go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
+go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
+go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
+go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
+golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
+golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.192.0 h1:PljqpNAfZaaSpS+TnANfnNAXKdzHM/B9bKhwRlo7JP0=
+google.golang.org/api v0.192.0/go.mod h1:9VcphjvAxPKLmSxVSzPlSRXy/5ARMEw5bf58WoVXafQ=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20240730163845-b1a4ccb954bf h1:OqdXDEakZCVtDiZTjcxfwbHPCT11ycCEsTKesBVKvyY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf h1:liao9UHurZLtiEwBgT9LMOnKYsHze6eA6w1KQCMVN2Q=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240730163845-b1a4ccb954bf/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
+google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/main.go	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,87 @@
+package main
+
+/*
+let python continue to serve these:
+		Route('/', getRoot),
+		Route('/graph/calendar/upcoming', StaticGraph(agendaGraph)),
+		use https://github.com/cayleygraph/quad
+		Route('/graph/calendar/upcoming/events', GraphEvents(agendaGraph)),
+		use https://github.com/tmaxmax/go-sse
+		Route('/graph/calendar/countdown', StaticGraph(countdownGraph)),
+		Route('/graph/calendar/countdown/events', GraphEvents(countdownGraph)),
+		Route('/graph/currentEvents', StaticGraph(currentEventsGraph)),
+		Route('/graph/currentEvents/events', GraphEvents(currentEventsGraph)),
+*/
+
+import (
+	"context"
+	"log"
+	"net/http"
+
+	"bigasterisk.com/go/gcalendarwatch/gcalclient"
+	"bigasterisk.com/go/gcalendarwatch/mongoclient"
+	"github.com/gorilla/mux"
+)
+
+func main() {
+	ctx := context.Background()
+
+	log.SetFlags(log.LstdFlags | log.Lshortfile)
+	gc, err := gcalclient.New(ctx)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer gc.Close()
+
+	mc, err := mongoclient.New(ctx)
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer mc.Close()
+
+	err = updateMongoCalsToMatchGoogle(mc, gc)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	err = updateMongoEventsToMatchGoogle(mc, gc)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	/*
+		------------------
+		connect to mongodb with these ops:
+		    save cals list
+		    get/set incremental token
+			add/edit/del events
+		    collection.find({"startTime": {"$gte": t1, "$lt": t2}}).sort([("startTime", 1)])
+		    collection.find({"startTime": {"$lte": now}, "endTime": {"$gte": now}}))
+
+		connect to https://github.com/googleapis/google-api-go-client/tree/main/calendar/v3 and:
+			get all my cals
+
+			subscribe to events
+
+			get cal event changes from incremental token
+
+			get cal events in range, for initial fill?
+
+		    write add/edit/del changes to mongo
+	*/
+
+	r := mux.NewRouter()
+	http.Handle("/", r)
+
+	home := func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("calsync service for calendar updates"))
+	}
+	r.HandleFunc("/", home)
+	r.HandleFunc("/gcalendarwatch", home)
+
+	notificationHandler := func(w http.ResponseWriter, r *http.Request) {
+	}
+	r.HandleFunc("/gcalendarwatch/notification", notificationHandler).Methods("POST")
+	log.Println(("serving /gcalendarwatch/notification on :8080"))
+	log.Fatal(http.ListenAndServe(":8080", nil))
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/calsync/mongoclient/mongoclient.go	Mon Aug 19 13:25:03 2024 -0700
@@ -0,0 +1,200 @@
+package mongoclient
+
+import (
+	"context"
+	"log"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
+
+type MongoClient struct {
+	ctx              context.Context
+	client           *mongo.Client
+	db               *mongo.Database
+	calsCollection   *mongo.Collection
+	eventsCollection *mongo.Collection
+}
+
+// docs in calsCollection
+type MongoCal struct {
+	Url         string `bson:"_id"` // bigasterisk.com/...
+	GoogleId    string `bson:"googleId"`
+	Summary     string `bson:"summary"`
+	Description string `bson:"description"`
+}
+
+// docs in eventsCollection
+type MongoEvent struct {
+	// e.g.
+	// {
+	// 	"_id" : "http://bigasterisk.com/calendar/08s5m1_20140929T030000Z",
+	// 	"htmlLink" : "https://www.google.com/calendar/event?eid=MDhz",
+	// 	"title" : "meeting",
+	// 	"feedId" : "drewpca@gmail.com",
+	// 	"feedTitle" : "drewpca@gmail.com",
+	// 	"endTimeUnspecified" : false,
+	// 	"start" : "2014-09-28T20:00:00-07:00",
+	// 	"startDate" : "2014-09-28",
+	// 	"startTime" : ISODate("2014-09-29T03:00:00Z"),
+	// 	"end" : "2014-09-28T21:00:00-07:00",
+	// 	"endDate" : "2014-09-28",
+	// 	"endTime" : ISODate("2014-09-29T04:00:00Z")
+	// }
+
+	Url                string    `bson:"_id"`
+	GoogleId           string    `bson:"googleId"`
+	HtmlLink           string    `bson:"htmlLink"`
+	Title              string    `bson:"title"`
+	FeedId             string    `bson:"feedId"`
+	FeedTitle          string    `bson:"feedTitle"`
+	EndTimeUnspecified bool      `bson:"endTimeUnspecified"`
+	Start              string    `bson:"start"`
+	StartDate          string    `bson:"startDate"`
+	StartTime          time.Time `bson:"startTime"`
+	End                string    `bson:"end"`
+	EndDate            string    `bson:"endDate"`
+	EndTime            time.Time `bson:"endTime"`
+	LastUpdated        time.Time `bson:"lastUpdated"`
+}
+
+func New(ctx context.Context) (*MongoClient, error) {
+	log.Println("todo: using fixed ip")
+	mclient, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://10.43.98.221:27017"))
+	if err != nil {
+		return nil, err
+	}
+	db := mclient.Database("pim")
+	return &MongoClient{
+			ctx,
+			mclient,
+			db,
+			db.Collection("test_gcalendar_cals"),
+			db.Collection("test_gcalendar")},
+		nil
+}
+
+func (c *MongoClient) Close() {
+	c.client.Disconnect(c.ctx)
+}
+
+func (c *MongoClient) GetAllCals() ([]MongoCal, error) {
+	cur, err := c.calsCollection.Find(c.ctx, bson.D{})
+	if err != nil {
+		return nil, err
+	}
+	defer cur.Close(c.ctx)
+
+	var cals []MongoCal
+	for cur.Next(c.ctx) {
+		var cal MongoCal
+		if err := cur.Decode(&cal); err != nil {
+			return nil, err
+		}
+		cals = append(cals, cal)
+	}
+	return cals, nil
+}
+
+func (c *MongoClient) GetOneCal(calId string) (MongoCal, error) {
+	res := c.calsCollection.FindOne(c.ctx, bson.M{"_id": calId})
+	var cal MongoCal
+	err := res.Decode(&cal)
+	return cal, err
+}
+
+func (c *MongoClient) UpsertOneCal(cal MongoCal) error {
+	filter := bson.M{"_id": cal.Url}
+	update := bson.M{
+		"$set": cal,
+		// bson.M{
+		// 	"googleId":    cal.GoogleId,
+		// 	"summary":     cal.Summary,
+		// 	"description": cal.Description,
+		// },
+	}
+	_, err := c.calsCollection.UpdateOne(c.ctx, filter, update, options.Update().SetUpsert(true))
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (c *MongoClient) DeleteCalsNotInSet(urlsToKeep map[string]bool) error {
+	curs, err := c.calsCollection.Find(c.ctx, bson.M{})
+	if err != nil {
+		return err
+	}
+	defer curs.Close(c.ctx)
+
+	for curs.Next(c.ctx) {
+		var doc bson.M
+		err = curs.Decode(&doc)
+		if err != nil {
+			return err
+		}
+		calUrl, ok := doc["_id"].(string)
+		if !ok {
+			continue
+		}
+		if !urlsToKeep[calUrl] {
+			log.Println("  cleaning up", calUrl)
+			_, err = c.calsCollection.DeleteOne(c.ctx, bson.M{"_id": doc["_id"]})
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return curs.Err()
+}
+
+func (c *MongoClient) UpsertOneEvent(ev MongoEvent) error {
+	filter := bson.M{"_id": ev.Url}
+	setFields := bson.M{
+		"googleId":    ev.GoogleId,
+		"lastUpdated": ev.LastUpdated,
+		"htmlLink":    ev.HtmlLink,
+		"title":       ev.Title,
+	}
+	update := bson.M{"$set": setFields}
+	_, err := c.eventsCollection.UpdateOne(c.ctx, filter, update, options.Update().SetUpsert(true))
+	if err != nil {
+		return err
+	}
+	log.Println("  mongo upserted", ev.Url)
+	return nil
+}
+
+// func (c *MongoClient) FindEventsIntersecting(t1, t2 time.Time) ([]MongoEvent, error) {
+// 	cur, err := c.eventsCollection.Find(c.ctx, bson.M{
+// 		"end":   bson.M{"$gte": t1},
+// 		"start": bson.M{"$lte": t2},
+// 	}) // todo: allday evs
+// 	if err != nil {
+// 		return nil, err
+// 	}
+// 	defer cur.Close(c.ctx)
+
+// 	var events []MongoEvent
+// 	for cur.Next(c.ctx) {
+// 		var ev MongoEvent
+// 		if err := cur.Decode(&ev); err != nil {
+// 			return nil, err
+// 		}
+// 		events = append(events, ev)
+// 	}
+// 	return events, nil
+// }
+
+// func (c *MongoClient) CurrentEvents(now time.Time) ([]MongoEvent, error) {
+// 	return c.FindEventsIntersecting(now, now)
+// }
+// func (c *MongoClient) UpdateOrInsertEvent(ev MongoEvent) error {
+// 	return errors.New("todo")
+// }
+
+// func (c *MongoClient) DeleteEvent(ev MongoEvent) error {
+// 	return errors.New("todo")
+// }