Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to receive calendar update notification #12

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion config.sample.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
version: 1
version: "1"

mode: resident
interval: "1m"
calendar_id: ja.japanese#[email protected]
calendar_webhook:
disable: true
address: "https://example.com/notify"

handler:
light:
Expand Down
116 changes: 116 additions & 0 deletions docs/diagram.drawio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions domain/repository/calendar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
1 change: 1 addition & 0 deletions domain/repository/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ type Config interface {
RunningMode() model.RunningMode
SyncInterval() time.Duration
Calendar() string
CalendarWebhookURL() (string, bool)
}
6 changes: 6 additions & 0 deletions domain/service/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
41 changes: 41 additions & 0 deletions interface/calendar/calendar.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions interface/calendar/token.go
Original file line number Diff line number Diff line change
@@ -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
}
58 changes: 50 additions & 8 deletions interface/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
Expand All @@ -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"`
}
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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()
}
21 changes: 20 additions & 1 deletion interface/http/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down