Skip to content

Commit

Permalink
wip: add calendar webhook settings
Browse files Browse the repository at this point in the history
  • Loading branch information
ww24 committed Jul 28, 2020
1 parent 75b583a commit 0157b51
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 10 deletions.
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
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

0 comments on commit 0157b51

Please sign in to comment.