From 75b583abe266a41c228581bbb0f75dc58585ae35 Mon Sep 17 00:00:00 2001 From: Takenori Nakagawa Date: Fri, 24 Jul 2020 01:00:20 +0900 Subject: [PATCH 1/2] Update README --- README.md | 2 + docs/diagram.drawio.svg | 116 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 docs/diagram.drawio.svg diff --git a/README.md b/README.md index c3b6428..1822929 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Calendar Notifier provides event handler and actions triggered by Google Calenda ## Features +![diagram](./docs/diagram.drawio.svg) + - Events - [x] [Calendar Events List API](https://developers.google.com/calendar/v3/reference/events/list) - [ ] [Calendar Events Watch API](https://developers.google.com/calendar/v3/reference/events/watch) diff --git a/docs/diagram.drawio.svg b/docs/diagram.drawio.svg new file mode 100644 index 0000000..39b3131 --- /dev/null +++ b/docs/diagram.drawio.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Cloud Pub/Sub + +
+
+
+
+ + Cloud Pu... + +
+
+ + + + + + + + + +
+
+
+ + Cloud Tasks + +
+
+
+
+ + Cloud Ta... + +
+
+ + + + + + +
+
+
+ + HTTP Request + +
+
+
+
+ + HTTP... + +
+
+ + + + + + + + + + + + + +
+
+
+ calendar-notifier +
+
+
+
+ + calendar-notifier + +
+
+
+ + + + + Viewer does not support full SVG 1.1 + + + +
\ No newline at end of file From 0157b51fa01c1b87d522294047b6c88938aa01eb Mon Sep 17 00:00:00 2001 From: Takenori Nakagawa Date: Mon, 27 Jul 2020 02:43:59 +0900 Subject: [PATCH 2/2] wip: add calendar webhook settings --- config.sample.yml | 6 +++- domain/repository/calendar.go | 1 + domain/repository/config.go | 1 + domain/service/sync.go | 6 ++++ go.mod | 1 + go.sum | 2 ++ interface/calendar/calendar.go | 41 ++++++++++++++++++++++ interface/calendar/token.go | 22 ++++++++++++ interface/config/config.go | 58 ++++++++++++++++++++++++++----- interface/http/handler/handler.go | 21 ++++++++++- 10 files changed, 149 insertions(+), 10 deletions(-) create mode 100644 interface/calendar/token.go diff --git a/config.sample.yml b/config.sample.yml index 99f4060..e7068c2 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -1,7 +1,11 @@ -version: 1 +version: "1" mode: resident +interval: "1m" calendar_id: ja.japanese#holiday@group.v.calendar.google.com +calendar_webhook: + disable: true + address: "https://example.com/notify" handler: light: diff --git a/domain/repository/calendar.go b/domain/repository/calendar.go index 76b7d16..d690ade 100644 --- a/domain/repository/calendar.go +++ b/domain/repository/calendar.go @@ -10,4 +10,5 @@ import ( // Calendar is the interface to control calendar service. type Calendar interface { List(ctx context.Context, since, until time.Time) (model.Schedules, error) + Watch(ctx context.Context, address string, ttl time.Duration) error } diff --git a/domain/repository/config.go b/domain/repository/config.go index 00761a8..eb45703 100644 --- a/domain/repository/config.go +++ b/domain/repository/config.go @@ -13,4 +13,5 @@ type Config interface { RunningMode() model.RunningMode SyncInterval() time.Duration Calendar() string + CalendarWebhookURL() (string, bool) } diff --git a/domain/service/sync.go b/domain/service/sync.go index 7323749..cc4b779 100644 --- a/domain/service/sync.go +++ b/domain/service/sync.go @@ -50,6 +50,12 @@ func (s *synchronizer) Sync(ctx context.Context) error { } log.Println("calendar.List:", len(schedules)) + if address, ok := s.cnf.CalendarWebhookURL(); ok { + if err := s.cal.Watch(ctx, address, s.cnf.SyncInterval()); err != nil { + return fmt.Errorf("failed to register watch address: %w", err) + } + } + acm := s.cnf.ActionConfigMap() am := make(map[model.ActionName]*action, len(acm)) diff --git a/go.mod b/go.mod index a1e803f..3475897 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( cloud.google.com/go v0.58.0 cloud.google.com/go/pubsub v1.4.0 github.com/golang/protobuf v1.4.2 + github.com/google/uuid v1.1.1 github.com/google/wire v0.4.0 golang.org/x/lint v0.0.0-20200302205851-738671d3881b golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d diff --git a/go.sum b/go.sum index c938809..d8cc3fe 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,8 @@ github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.0.1 h1:/eqq+otEXm5vhfBrbREPCSVQbvofip6kIz+mX5TUH7k= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE= github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= diff --git a/interface/calendar/calendar.go b/interface/calendar/calendar.go index ac83cb6..37a024f 100644 --- a/interface/calendar/calendar.go +++ b/interface/calendar/calendar.go @@ -4,8 +4,10 @@ import ( "context" "fmt" "log" + "strconv" "time" + "github.com/google/uuid" calendar "google.golang.org/api/calendar/v3" "github.com/ww24/calendar-notifier/domain/model" @@ -16,6 +18,7 @@ import ( type Calendar struct { calendarID string newService func(ctx context.Context) (*calendar.Service, error) + token syncToken } // New returns new calendar API wrapper. @@ -45,6 +48,8 @@ func (c *Calendar) List(ctx context.Context, since, until time.Time) (model.Sche return nil, err } + c.token.update(events.NextSyncToken) + schedules := make([]model.Schedule, 0, len(events.Items)) for _, item := range events.Items { s, err := toModelSchedule(item) @@ -61,6 +66,42 @@ func (c *Calendar) List(ctx context.Context, since, until time.Time) (model.Sche return schedules, nil } +// Watch watches google calendar update event. +func (c *Calendar) Watch(ctx context.Context, address string, ttl time.Duration) error { + svc, err := c.newService(ctx) + if err != nil { + return err + } + + id, err := uuid.NewRandom() + if err != nil { + return fmt.Errorf("failed to generate UUIDv4 as channel id: %w", err) + } + + channel := &calendar.Channel{ + Address: address, + Id: id.String(), + Params: map[string]string{ + "ttl": strconv.Itoa(int(ttl.Seconds())), + }, + Payload: true, + Token: "", // TODO + } + watchCall := svc.Events.Watch(c.calendarID, channel). + Context(ctx). + SyncToken(c.token.get()) + + ch, err := watchCall.Do() + if err != nil { + return fmt.Errorf("failed to watch calendar events: %w", err) + } + + // DEBUG + fmt.Printf("channel: %+v\n", ch) + + return nil +} + func toModelSchedule(item *calendar.Event) (model.Schedule, error) { s := model.Schedule{ ID: item.Id, diff --git a/interface/calendar/token.go b/interface/calendar/token.go new file mode 100644 index 0000000..95f6bc8 --- /dev/null +++ b/interface/calendar/token.go @@ -0,0 +1,22 @@ +package calendar + +import ( + "sync" +) + +type syncToken struct { + mu sync.RWMutex + token string +} + +func (c *syncToken) update(token string) { + c.mu.Lock() + defer c.mu.Unlock() + c.token = token +} + +func (c *syncToken) get() string { + c.mu.RLock() + defer c.mu.RUnlock() + return c.token +} diff --git a/interface/config/config.go b/interface/config/config.go index 31533c0..3ad0794 100644 --- a/interface/config/config.go +++ b/interface/config/config.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/url" "os" "strings" "time" @@ -20,16 +21,47 @@ const ( // Config represents a config.yml. type Config struct { - Version string `yaml:"version"` - Mode model.RunningMode `yaml:"mode"` - Interval time.Duration `yaml:"interval"` - CalendarID string `yaml:"calendar_id"` - Handler map[string]EventHandler `yaml:"handler"` - Action map[model.ActionName]Action `yaml:"action"` + Version string `yaml:"version"` + Mode model.RunningMode `yaml:"mode"` + Interval time.Duration `yaml:"interval"` + CalendarID string `yaml:"calendar_id"` + CalendarWebhook calendarWebhook `yaml:"calendar_webhook"` + Handler map[string]eventHandler `yaml:"handler"` + Action map[model.ActionName]Action `yaml:"action"` } -// EventHandler is event handler which contains action names. -type EventHandler struct { +// calendarWebhook is settings for calendar update notification. +type calendarWebhook struct { + Disable bool `yaml:"disable"` + Address string `yaml:"address"` +} + +func (c *calendarWebhook) validate() error { + if c.Disable { + return nil + } + u, err := url.Parse(c.Address) + if err != nil { + return err + } + if u.Scheme != "https" { + return errors.New("scheme must be \"https\"") + } + if u.Host != "example.com" { + return errors.New("must replace placeholder (example.com) with valid host") + } + return nil +} + +func (c *calendarWebhook) url() (string, bool) { + if c.Disable { + return "", false + } + return c.Address, true +} + +// eventHandler is event handler which contains action names. +type eventHandler struct { Start []model.ActionName `yaml:"start"` End []model.ActionName `yaml:"end"` } @@ -130,6 +162,11 @@ func (c *Config) validate() error { return fmt.Errorf("unsupported action type: %s", a.Type) } } + + if err := c.CalendarWebhook.validate(); err != nil { + return fmt.Errorf("calendar_webhook is invalid: %w", err) + } + return nil } @@ -192,3 +229,8 @@ func (c *Config) SyncInterval() time.Duration { func (c *Config) Calendar() string { return c.CalendarID } + +// CalendarWebhookURL returns calendar webhook address and enabled status. +func (c *Config) CalendarWebhookURL() (string, bool) { + return c.CalendarWebhook.url() +} diff --git a/interface/http/handler/handler.go b/interface/http/handler/handler.go index 3173cfd..9f4dc02 100644 --- a/interface/http/handler/handler.go +++ b/interface/http/handler/handler.go @@ -14,7 +14,8 @@ func New(sync usecase.Synchronizer) http.Handler { mux := http.NewServeMux() svc := newService(sync) mux.HandleFunc("/", svc.defaultHandler) - mux.HandleFunc("/launch", svc.sync) + mux.HandleFunc("/launch", svc.sync) // TODO: change to sync + mux.HandleFunc("/notify", svc.notify) return mux } @@ -46,6 +47,8 @@ func (s *syncService) defaultHandler(w http.ResponseWriter, r *http.Request) { } func (s *syncService) sync(w http.ResponseWriter, r *http.Request) { + // TODO: IAM 認証 + switch r.Method { case http.MethodOptions: return @@ -70,6 +73,22 @@ func (s *syncService) sync(w http.ResponseWriter, r *http.Request) { w.Write(append(d, '\n')) } +func (s *syncService) notify(w http.ResponseWriter, r *http.Request) { + // TODO: api key による認証 + + if r.Header.Get("content-type") != "application/json" { + w.WriteHeader(http.StatusBadRequest) + return + } + + // FIXME: DEBUG出力 + m := make(map[string]interface{}) + json.NewDecoder(r.Body).Decode(&m) + fmt.Printf("%+v\n", m) + + w.WriteHeader(http.StatusNoContent) +} + func sendError(w http.ResponseWriter, r *http.Request, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError)