diff --git a/config.sample.yml b/config.sample.yml index 99f4060..c194b57 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -1,7 +1,10 @@ -version: 1 +version: "1" mode: resident calendar_id: ja.japanese#holiday@group.v.calendar.google.com +calendar_webhook: + disable: true + address: "https://example.com/notify" handler: light: 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..ee0228a 100644 --- a/interface/calendar/calendar.go +++ b/interface/calendar/calendar.go @@ -6,6 +6,7 @@ import ( "log" "time" + "github.com/google/uuid" calendar "google.golang.org/api/calendar/v3" "github.com/ww24/calendar-notifier/domain/model" @@ -16,6 +17,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 +47,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 +65,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) 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": "600", // 10min + }, + 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)