diff --git a/Makefile b/Makefile index a06042e4..451a033f 100644 --- a/Makefile +++ b/Makefile @@ -158,7 +158,7 @@ ifneq ($(HAS_WEBAPP),) mkdir -p dist/$(PLUGIN_ID)/webapp/dist; cp -r webapp/dist/* dist/$(PLUGIN_ID)/webapp/dist/; endif - cd dist && tar -cvzf $(BUNDLE_NAME) $(PLUGIN_ID) + cd dist && tar -cvzf $(BUNDLE_NAME) -C $(PLUGIN_ID) . @echo plugin built at: dist/$(BUNDLE_NAME) @@ -224,6 +224,55 @@ ifneq ($(HAS_WEBAPP),) endif rm -fr build/bin/ +## Setup dlv for attaching, identifying the plugin PID for other targets. +.PHONY: setup-attach +setup-attach: + $(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}')) + $(eval NUM_PID := $(shell echo -n ${PLUGIN_PID} | wc -w)) + + @if [ ${NUM_PID} -gt 2 ]; then \ + echo "** There is more than 1 plugin process running. Run 'make kill reset' to restart just one."; \ + exit 1; \ + fi + +## Check if setup-attach succeeded. +.PHONY: check-attach +check-attach: + @if [ -z ${PLUGIN_PID} ]; then \ + echo "Could not find plugin PID; the plugin is not running. Exiting."; \ + exit 1; \ + else \ + echo "Located Plugin running with PID: ${PLUGIN_PID}"; \ + fi + +## Attach dlv to an existing plugin instance. +.PHONY: attach +attach: setup-attach check-attach + dlv attach ${PLUGIN_PID} + +## Attach dlv to an existing plugin instance, exposing a headless instance on $DLV_DEBUG_PORT. +.PHONY: attach-headless +attach-headless: setup-attach check-attach + dlv attach ${PLUGIN_PID} --listen :$(DLV_DEBUG_PORT) --headless=true --api-version=2 --accept-multiclient + +## Detach dlv from an existing plugin instance, if previously attached. +.PHONY: detach +detach: setup-attach + @DELVE_PID=$(shell ps aux | grep "dlv attach ${PLUGIN_PID}" | grep -v "grep" | awk -F " " '{print $$2}') && \ + if [ "$$DELVE_PID" -gt 0 ] > /dev/null 2>&1 ; then \ + echo "Located existing delve process running with PID: $$DELVE_PID. Killing." ; \ + kill -9 $$DELVE_PID ; \ + fi + +## Kill all instances of the plugin, detaching any existing dlv instance. +.PHONY: kill +kill: detach + $(eval PLUGIN_PID := $(shell ps aux | grep "plugins/${PLUGIN_ID}" | grep -v "grep" | awk -F " " '{print $$2}')) + + @for PID in ${PLUGIN_PID}; do \ + echo "Killing plugin pid $$PID"; \ + kill -9 $$PID; \ + done; \ # Help documentation à la https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html help: @cat Makefile | grep -v '\.PHONY' | grep -v '\help:' | grep -B1 -E '^[a-zA-Z0-9_.-]+:.*' | sed -e "s/:.*//" | sed -e "s/^## //" | grep -v '\-\-' | sed '1!G;h;$$!d' | awk 'NR%2{printf "\033[36m%-30s\033[0m",$$0;next;}1' | sort diff --git a/assets/profile.png b/assets/profile.png index eb2fea2e..73bdebf3 100644 Binary files a/assets/profile.png and b/assets/profile.png differ diff --git a/go.mod b/go.mod index 79bbd4c7..73efb195 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mattermost/mattermost-plugin-mscalendar -go 1.18 +go 1.20 require ( github.com/golang/mock v1.6.0 @@ -12,12 +12,19 @@ require ( github.com/pkg/errors v0.9.1 github.com/rudderlabs/analytics-go v3.3.1+incompatible github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.1 github.com/yaegashi/msgraph.go v0.0.0-20191104022859-3f9096c750b2 + golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.4.0 + golang.org/x/sys v0.5.0 // indirect + google.golang.org/api v0.103.0 + google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + google.golang.org/grpc v1.53.0 // indirect ) require ( + cloud.google.com/go/compute v1.15.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -29,9 +36,12 @@ require ( github.com/francoispqt/gojay v1.2.13 // indirect github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect + github.com/googleapis/gax-go/v2 v2.7.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/go-hclog v1.0.0 // indirect github.com/hashicorp/go-plugin v1.4.3 // indirect @@ -75,13 +85,10 @@ require ( github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/yuin/goldmark v1.4.4 // indirect + go.opencensus.io v0.24.0 // indirect golang.org/x/crypto v0.1.0 // indirect - golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect - google.golang.org/grpc v1.53.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.64.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index bf30f9a6..24196064 100644 --- a/go.sum +++ b/go.sum @@ -27,15 +27,21 @@ cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWc cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute v1.15.1 h1:7UGq3QknM33pw5xATlpzeoomNxsacIVvTqTTvbfajmE= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -554,6 +560,7 @@ github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4er github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -642,10 +649,14 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20211111143520-d0d5ecc1a356/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI= @@ -1275,8 +1286,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -1285,8 +1297,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -1399,6 +1412,8 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/otel v0.11.0/go.mod h1:G8UCk+KooF2HLkgo8RHX9epABH/aRGYET7gQOqBVdB0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1831,6 +1846,7 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= @@ -1868,6 +1884,8 @@ google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtuk google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/plugin.json b/plugin.json index 6889a3cb..e29aac16 100644 --- a/plugin.json +++ b/plugin.json @@ -1,12 +1,12 @@ { - "id": "com.mattermost.mscalendar", - "name": "Microsoft Calendar", - "description": "Microsoft Calendar Integration", - "homepage_url": "https://mattermost.gitbook.io/plugin-mscalendar", - "support_url": "https://github.com/mattermost/mattermost-plugin-mscalendar/issues", - "release_notes_url": "https://github.com/mattermost/mattermost-plugin-mscalendar/releases/tag/v1.2.1", + "id": "com.mattermost.gcal", + "name": "Google Calendar", + "description": "Google Calendar Integration", + "homepage_url": "https://github.com/mattermost/mattermost-plugin-gcal", + "support_url": "https://github.com/mattermost/mattermost-plugin-gcal/issues", + "release_notes_url": "https://github.com/mattermost/mattermost-plugin-gcal/releases/tag/v0.1.0", "icon_path": "assets/profile.svg", - "version": "1.2.1", + "version": "0.1.0", "min_server_version": "6.3.0", "server": { "executables": { @@ -91,6 +91,13 @@ "help_text": "Microsoft Office Client Secret.", "placeholder": "", "default": "" + }, + { + "key": "GoogleDomainVerifyKey", + "display_name": "Google domain verify key", + "type": "text", + "help_text": "This should look something like \"googlebd09b1075898e210.html\"", + "default": "" } ] } diff --git a/server/api/api.go b/server/api/api.go index 19fdcfbe..88333378 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -4,6 +4,9 @@ package api import ( + "net/http" + "strings" + "github.com/mattermost/mattermost-plugin-mscalendar/server/config" "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/httputils" @@ -32,4 +35,23 @@ func Init(h *httputils.Handler, env mscalendar.Env, notificationProcessor mscale postActionRouter.HandleFunc(config.PathTentative, api.postActionTentative).Methods("POST") postActionRouter.HandleFunc(config.PathRespond, api.postActionRespond).Methods("POST") postActionRouter.HandleFunc(config.PathConfirmStatusChange, api.postActionConfirmStatusChange).Methods("POST") + + notificationRouter.HandleFunc("/{fname}", func(w http.ResponseWriter, r *http.Request) { + if api.GoogleDomainVerifyKey == "" { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Domain verify key is not set")) + return + } + + parts := strings.Split(r.URL.Path, "/") + fname := parts[len(parts)-1] + if fname != api.GoogleDomainVerifyKey { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("Incorrect file name requested")) + return + } + + resp := "google-site-verification: " + api.GoogleDomainVerifyKey + w.Write([]byte(resp)) + }) } diff --git a/server/command/connect_test.go b/server/command/connect_test.go index 9f6e5e81..08fcb70d 100644 --- a/server/command/connect_test.go +++ b/server/command/connect_test.go @@ -1,6 +1,7 @@ package command import ( + "fmt" "testing" "github.com/golang/mock/gomock" @@ -30,8 +31,11 @@ func TestConnect(t *testing.T) { mscal := m.(*mock_mscalendar.MockMSCalendar) mscal.EXPECT().GetRemoteUser("user_id").Return(&remote.User{Mail: "user@email.com"}, nil).Times(1) }, - expectedOutput: "Your Mattermost account is already connected to Microsoft Calendar account `user@email.com`. To connect to a different account, first run `/mscalendar disconnect`.", - expectedError: "", + expectedOutput: fmt.Sprintf( + "Your Mattermost account is already connected to %s account `user@email.com`. To connect to a different account, first run `/%s disconnect`.", + config.ApplicationName, config.CommandTrigger, + ), + expectedError: "", }, { name: "user not connected", @@ -59,7 +63,7 @@ func TestConnect(t *testing.T) { command := Command{ Context: &plugin.Context{}, Args: &model.CommandArgs{ - Command: "/mscalendar " + tc.command, + Command: fmt.Sprintf("/%s %s", config.CommandTrigger, tc.command), UserId: "user_id", }, ChannelID: "channel_id", diff --git a/server/command/disconnect_test.go b/server/command/disconnect_test.go index e3d557dc..d082d0b6 100644 --- a/server/command/disconnect_test.go +++ b/server/command/disconnect_test.go @@ -1,6 +1,7 @@ package command import ( + "fmt" "testing" "github.com/golang/mock/gomock" @@ -42,7 +43,7 @@ func TestDisconnect(t *testing.T) { mscal.EXPECT().GetRemoteUser("user_id").Return(&remote.User{}, errors.New("some error")).Times(1) }, expectedOutput: "", - expectedError: "Command /mscalendar disconnect failed: some error", + expectedError: fmt.Sprintf("Command /%s disconnect failed: some error", config.CommandTrigger), }, { name: "disconnect failed", @@ -53,7 +54,7 @@ func TestDisconnect(t *testing.T) { mscal.EXPECT().DisconnectUser("user_id").Return(errors.New("some error")).Times(1) }, expectedOutput: "", - expectedError: "Command /mscalendar disconnect failed: some error", + expectedError: fmt.Sprintf("Command /%s disconnect failed: some error", config.CommandTrigger), }, { name: "disconnect successful", @@ -82,7 +83,7 @@ func TestDisconnect(t *testing.T) { command := Command{ Context: &plugin.Context{}, Args: &model.CommandArgs{ - Command: "/mscalendar " + tc.command, + Command: fmt.Sprintf("/%s %s", config.CommandTrigger, tc.command), UserId: "user_id", }, ChannelID: "channel_id", diff --git a/server/command/help.go b/server/command/help.go index 9a3ab8a3..80cbb124 100644 --- a/server/command/help.go +++ b/server/command/help.go @@ -18,6 +18,7 @@ func (c *Command) help(parameters ...string) (string, bool, error) { } resp += getCommandText(desc) } + return resp, false, nil } diff --git a/server/config/config.go b/server/config/config.go index 93d0e1b8..0972146a 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -8,9 +8,13 @@ type StoredConfig struct { OAuth2Authority string OAuth2ClientID string OAuth2ClientSecret string - bot.Config + EnableStatusSync bool EnableDailySummary bool + + GoogleDomainVerifyKey string + + bot.Config } // Config represents the the metadata handed to all request runners (command, @@ -27,3 +31,7 @@ type Config struct { PluginVersion string StoredConfig } + +func (c *Config) GetNotificationURL() string { + return c.PluginURL + FullPathEventNotification +} diff --git a/server/config/const.go b/server/config/const.go index aea5812b..36544853 100644 --- a/server/config/const.go +++ b/server/config/const.go @@ -4,26 +4,29 @@ package config const ( - BotUserName = "mscalendar" - BotDisplayName = "Microsoft Calendar" - BotDescription = "Created by the Microsoft Calendar Plugin." + BotUserName = "gcal" + BotDisplayName = "Google Calendar" + BotDescription = "Created by the Google Calendar Plugin." - ApplicationName = "Microsoft Calendar" - Repository = "mattermost-plugin-mscalendar" - CommandTrigger = "mscalendar" - TelemetryShortName = "mscalendar" + ApplicationName = "Google Calendar" + Repository = "mattermost-plugin-gcal" + CommandTrigger = "gcal" + TelemetryShortName = "gcal" - PathOAuth2 = "/oauth2" - PathComplete = "/complete" - PathAPI = "/api/v1" - PathPostAction = "/action" - PathRespond = "/respond" - PathAccept = "/accept" - PathDecline = "/decline" - PathTentative = "/tentative" - PathConfirmStatusChange = "/confirm" - PathNotification = "/notification/v1" - PathEvent = "/event" + PathOAuth2 = "/oauth2" + PathComplete = "/complete" + PathAPI = "/api/v1" + PathDialogs = "/dialogs" + PathSetAutoRespondMessage = "/set-auto-respond-message" + PathPostAction = "/action" + PathRespond = "/respond" + PathAccept = "/accept" + PathDecline = "/decline" + PathTentative = "/tentative" + PathConfirmStatusChange = "/confirm" + PathNotification = "/notification/v1" + PathEvent = "/event" + PathVerifyDomain = "/verify" FullPathEventNotification = PathNotification + PathEvent FullPathOAuth2Redirect = PathOAuth2 + PathComplete diff --git a/server/manifest.go b/server/manifest.go index a9a96254..2ea83476 100644 --- a/server/manifest.go +++ b/server/manifest.go @@ -6,6 +6,6 @@ var manifest = struct { ID string Version string }{ - ID: "com.mattermost.mscalendar", - Version: "1.2.1", + ID: "com.mattermost.gcal", + Version: "0.1.0", } diff --git a/server/mscalendar/daily_summary_test.go b/server/mscalendar/daily_summary_test.go index 339da405..92beb8c7 100644 --- a/server/mscalendar/daily_summary_test.go +++ b/server/mscalendar/daily_summary_test.go @@ -2,11 +2,11 @@ package mscalendar import ( "context" + "errors" "testing" "time" "github.com/golang/mock/gomock" - "github.com/pkg/errors" "github.com/stretchr/testify/require" "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar/mock_plugin_api" @@ -148,7 +148,7 @@ func TestProcessAllDailySummary(t *testing.T) { gomock.InOrder( mockPoster.EXPECT().DM("user1_mm_id", "You have no upcoming events.").Return("postID1", nil).Times(1), mockPoster.EXPECT().DM("user2_mm_id", `Times are shown in Pacific Standard Time -Wednesday February 12 +Wednesday February 12, 2020 | Time | Subject | | :--: | :-- | diff --git a/server/mscalendar/notification.go b/server/mscalendar/notification.go index de0899a3..3e2a1b9b 100644 --- a/server/mscalendar/notification.go +++ b/server/mscalendar/notification.go @@ -144,7 +144,7 @@ func (processor *notificationProcessor) processNotification(n *remote.Notificati if n.RecommendRenew { var renewed *remote.Subscription - renewed, err = client.RenewSubscription(n.SubscriptionID) + renewed, err = client.RenewSubscription(processor.Config.GetNotificationURL(), sub.Remote.CreatorID, n.SubscriptionID) if err != nil { return err } diff --git a/server/mscalendar/oauth2.go b/server/mscalendar/oauth2.go index b39d0e80..1ec10945 100644 --- a/server/mscalendar/oauth2.go +++ b/server/mscalendar/oauth2.go @@ -45,7 +45,7 @@ func (app *oauth2App) InitOAuth2(mattermostUserID string) (url string, err error return "", err } - return conf.AuthCodeURL(state, oauth2.AccessTypeOffline), nil + return conf.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent")), nil } func (app *oauth2App) CompleteOAuth2(authedUserID, code, state string) error { diff --git a/server/mscalendar/oauth2_test.go b/server/mscalendar/oauth2_test.go index 84ba5f9e..00b4a61f 100644 --- a/server/mscalendar/oauth2_test.go +++ b/server/mscalendar/oauth2_test.go @@ -206,9 +206,9 @@ func TestCompleteOAuth2Errors(t *testing.T) { poster.EXPECT().DM( gomock.Eq("fake@mattermost.com"), gomock.Eq(RemoteUserAlreadyConnected), - gomock.Eq("Microsoft Calendar"), + gomock.Eq(config.ApplicationName), gomock.Eq("mail-value"), - gomock.Eq("mscalendar"), + gomock.Eq(config.CommandTrigger), gomock.Eq("sample-username"), ).Return("post_id", nil).Times(1) }, diff --git a/server/mscalendar/subscription.go b/server/mscalendar/subscription.go index 98115d86..a61fc6c8 100644 --- a/server/mscalendar/subscription.go +++ b/server/mscalendar/subscription.go @@ -8,7 +8,6 @@ import ( "github.com/pkg/errors" - "github.com/mattermost/mattermost-plugin-mscalendar/server/config" "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" "github.com/mattermost/mattermost-plugin-mscalendar/server/store" ) @@ -28,8 +27,7 @@ func (m *mscalendar) CreateMyEventSubscription() (*store.Subscription, error) { return nil, err } - sub, err := m.client.CreateMySubscription( - m.Config.PluginURL + config.FullPathEventNotification) + sub, err := m.client.CreateMySubscription(m.Config.GetNotificationURL(), m.actingUser.Remote.ID) if err != nil { return nil, err } @@ -81,7 +79,7 @@ func (m *mscalendar) RenewMyEventSubscription() (*store.Subscription, error) { if subscriptionID == "" { return nil, nil } - renewed, err := m.client.RenewSubscription(subscriptionID) + renewed, err := m.client.RenewSubscription(m.Config.GetNotificationURL(), m.actingUser.Remote.ID, subscriptionID) if err != nil { if strings.Contains(err.Error(), "The object was not found") { err = m.Store.DeleteUserSubscription(m.actingUser.User, subscriptionID) diff --git a/server/mscalendar/views/calendar.go b/server/mscalendar/views/calendar.go index cdd19f44..8cbbebd1 100644 --- a/server/mscalendar/views/calendar.go +++ b/server/mscalendar/views/calendar.go @@ -27,7 +27,7 @@ func RenderCalendarView(events []*remote.Event, timeZone string) (string, error) resp := "Times are shown in " + events[0].Start.TimeZone for _, group := range groupEventsByDate(events) { - resp += "\n" + group[0].Start.Time().Format("Monday January 02") + "\n\n" + resp += "\n" + group[0].Start.Time().Format("Monday January 02, 2006") + "\n\n" resp += renderTableHeader() for _, e := range group { eventString, err := renderEvent(e, true, timeZone) diff --git a/server/plugin/plugin.go b/server/plugin/plugin.go index dcbce32d..2c31d9e9 100644 --- a/server/plugin/plugin.go +++ b/server/plugin/plugin.go @@ -25,7 +25,7 @@ import ( "github.com/mattermost/mattermost-plugin-mscalendar/server/jobs" "github.com/mattermost/mattermost-plugin-mscalendar/server/mscalendar" "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" - "github.com/mattermost/mattermost-plugin-mscalendar/server/remote/msgraph" + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote/gcal" "github.com/mattermost/mattermost-plugin-mscalendar/server/store" "github.com/mattermost/mattermost-plugin-mscalendar/server/telemetry" "github.com/mattermost/mattermost-plugin-mscalendar/server/tracker" @@ -173,7 +173,7 @@ func (p *Plugin) OnConfigurationChange() (err error) { e.Config.PluginURLPath = pluginURLPath e.bot = e.bot.WithConfig(stored.Config) - e.Dependencies.Remote = remote.Makers[msgraph.Kind](e.Config, e.bot) + e.Dependencies.Remote = remote.Makers[gcal.Kind](e.Config, e.bot) mscalendarBot := mscalendar.NewMSCalendarBot(e.bot, e.Env, pluginURL) diff --git a/server/remote/client.go b/server/remote/client.go index 31f2c0bf..78b5c40a 100644 --- a/server/remote/client.go +++ b/server/remote/client.go @@ -9,26 +9,49 @@ import ( ) type Client interface { - AcceptEvent(remoteUserID, eventID string) error - CallFormPost(method, path string, in url.Values, out interface{}) (responseData []byte, err error) - CallJSON(method, path string, in, out interface{}) (responseData []byte, err error) - CreateCalendar(remoteUserID string, calendar *Calendar) (*Calendar, error) - CreateEvent(remoteUserID string, calendarEvent *Event) (*Event, error) - CreateMySubscription(notificationURL string) (*Subscription, error) - DeclineEvent(remoteUserID, eventID string) error - DeleteCalendar(remoteUserID, calendarID string) error - DeleteSubscription(subscriptionID string) error - FindMeetingTimes(remoteUserID string, meetingParams *FindMeetingTimesParameters) (*MeetingTimeSuggestionResults, error) + Core + Calendars + Events + Subscriptions + Utils + Unsupported +} + +type Core interface { + GetMe() (*User, error) +} + +type Calendars interface { + GetEvent(remoteUserID, eventID string) (*Event, error) GetCalendars(remoteUserID string) ([]*Calendar, error) GetDefaultCalendarView(remoteUserID string, startTime, endTime time.Time) ([]*Event, error) DoBatchViewCalendarRequests([]*ViewCalendarParams) ([]*ViewCalendarResponse, error) - GetEvent(remoteUserID, eventID string) (*Event, error) GetMailboxSettings(remoteUserID string) (*MailboxSettings, error) - GetMe() (*User, error) +} + +type Events interface { + CreateEvent(remoteUserID string, calendarEvent *Event) (*Event, error) + AcceptEvent(remoteUserID, eventID string) error + DeclineEvent(remoteUserID, eventID string) error + TentativelyAcceptEvent(remoteUserID, eventID string) error +} + +type Subscriptions interface { + CreateMySubscription(notificationURL, remoteUserID string) (*Subscription, error) + DeleteSubscription(subscriptionID string) error GetNotificationData(*Notification) (*Notification, error) - GetSchedule(requests []*ScheduleUserInfo, startTime, endTime *DateTime, availabilityViewInterval int) ([]*ScheduleInformation, error) ListSubscriptions() ([]*Subscription, error) - RenewSubscription(subscriptionID string) (*Subscription, error) - TentativelyAcceptEvent(remoteUserID, eventID string) error + RenewSubscription(notificationURL, remoteUserID, subscriptionID string) (*Subscription, error) +} + +type Utils interface { GetSuperuserToken() (string, error) + CallFormPost(method, path string, in url.Values, out interface{}) (responseData []byte, err error) + CallJSON(method, path string, in, out interface{}) (responseData []byte, err error) +} + +type Unsupported interface { + CreateCalendar(remoteUserID string, calendar *Calendar) (*Calendar, error) + DeleteCalendar(remoteUserID, calendarID string) error + FindMeetingTimes(remoteUserID string, meetingParams *FindMeetingTimesParameters) (*MeetingTimeSuggestionResults, error) } diff --git a/server/remote/date_time.go b/server/remote/date_time.go index ec430dba..fa2fec49 100644 --- a/server/remote/date_time.go +++ b/server/remote/date_time.go @@ -6,6 +6,8 @@ package remote import ( "time" + "google.golang.org/api/calendar/v3" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/tz" ) @@ -30,6 +32,15 @@ func NewDateTime(t time.Time, timeZone string) *DateTime { } } +// NewGoogleDateTime creates a DateTime that is compatible with Google's API. +func NewGoogleDateTime(dateTime *calendar.EventDateTime) *DateTime { + t, _ := time.Parse(time.RFC3339, dateTime.DateTime) + return &DateTime{ + DateTime: t.Format(RFC3339NanoNoTimezone), + TimeZone: dateTime.TimeZone, + } +} + func (dt DateTime) String() string { t := dt.Time() if t.IsZero() { diff --git a/server/remote/gcal/batch_request.go b/server/remote/gcal/batch_request.go new file mode 100644 index 00000000..a0ca18bc --- /dev/null +++ b/server/remote/gcal/batch_request.go @@ -0,0 +1,52 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "net/http" +) + +const maxNumRequestsPerBatch = 20 + +type singleRequest struct { + ID string `json:"id"` + URL string `json:"url"` + Method string `json:"method"` + Body interface{} `json:"body"` + Headers map[string]string `json:"headers"` +} + +type fullBatchRequest struct { + Requests []*singleRequest `json:"requests"` +} + +func (c *client) batchRequest(req fullBatchRequest, out interface{}) error { + u := "https://graph.microsoft.com/v1.0/$batch" + + _, err := c.CallJSON(http.MethodPost, u, req, out) + return err +} + +func prepareBatchRequests(requests []*singleRequest) []fullBatchRequest { + numFullRequests := len(requests) / maxNumRequestsPerBatch + if len(requests)%maxNumRequestsPerBatch != 0 { + numFullRequests++ + } + + result := []fullBatchRequest{} + + for i := 0; i < numFullRequests; i++ { + startIdx := i * maxNumRequestsPerBatch + endIdx := startIdx + maxNumRequestsPerBatch + if i == numFullRequests-1 { + endIdx = len(requests) + } + + slice := requests[startIdx:endIdx] + batchReq := fullBatchRequest{Requests: slice} + result = append(result, batchReq) + } + + return result +} diff --git a/server/remote/gcal/call.go b/server/remote/gcal/call.go new file mode 100644 index 00000000..5e5e56eb --- /dev/null +++ b/server/remote/gcal/call.go @@ -0,0 +1,105 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" + msgraph "github.com/yaegashi/msgraph.go/v1.0" +) + +func (c *client) CallJSON(method, path string, in, out interface{}) (responseData []byte, err error) { + contentType := "application/json" + buf := &bytes.Buffer{} + err = json.NewEncoder(buf).Encode(in) + if err != nil { + return nil, err + } + return c.call(method, path, contentType, buf, out) +} + +func (c *client) CallFormPost(method, path string, in url.Values, out interface{}) (responseData []byte, err error) { + contentType := "application/x-www-form-urlencoded" + buf := strings.NewReader(in.Encode()) + return c.call(method, path, contentType, buf, out) +} + +func (c *client) call(method, path, contentType string, inBody io.Reader, out interface{}) (responseData []byte, err error) { + errContext := fmt.Sprintf("msgraph: Call failed: method:%s, path:%s", method, path) + pathURL, err := url.Parse(path) + if err != nil { + return nil, errors.WithMessage(err, errContext) + } + + if pathURL.Scheme == "" || pathURL.Host == "" { + var baseURL *url.URL + baseURL, err = url.Parse(c.rbuilder.URL()) + if err != nil { + return nil, errors.WithMessage(err, errContext) + } + if path[0] != '/' { + path = "/" + path + } + path = baseURL.String() + path + } + + req, err := http.NewRequest(method, path, inBody) + if err != nil { + return nil, err + } + if contentType != "" { + req.Header.Add("Content-Type", contentType) + } + + if c.ctx != nil { + req = req.WithContext(c.ctx) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, err + } + + if resp.Body == nil { + return nil, nil + } + defer resp.Body.Close() + + responseData, err = ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + switch resp.StatusCode { + case http.StatusOK, http.StatusCreated: + if out != nil { + err = json.Unmarshal(responseData, out) + if err != nil { + return responseData, err + } + } + return responseData, nil + + case http.StatusNoContent: + return nil, nil + } + + errResp := msgraph.ErrorResponse{Response: resp} + err = json.Unmarshal(responseData, &errResp) + if err != nil { + return responseData, errors.WithMessagef(err, "status: %s", resp.Status) + } + if err != nil { + return responseData, err + } + return responseData, &errResp +} diff --git a/server/remote/gcal/client.go b/server/remote/gcal/client.go new file mode 100644 index 00000000..8513c25f --- /dev/null +++ b/server/remote/gcal/client.go @@ -0,0 +1,26 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "context" + "net/http" + + msgraph "github.com/yaegashi/msgraph.go/v1.0" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/config" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" +) + +type client struct { + // caching the context here since it's a "single-use" client, usually used + // within a single API request + ctx context.Context + + httpClient *http.Client + rbuilder *msgraph.GraphServiceRequestBuilder + + conf *config.Config + bot.Logger +} diff --git a/server/remote/gcal/create_calendar.go b/server/remote/gcal/create_calendar.go new file mode 100644 index 00000000..f7218955 --- /dev/null +++ b/server/remote/gcal/create_calendar.go @@ -0,0 +1,30 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" +) + +// CreateCalendar creates a calendar +func (c *client) CreateCalendar(remoteUserID string, calIn *remote.Calendar) (*remote.Calendar, error) { + if true { + return nil, errors.New("gcal CreateCalendar not implemented") + } + + var calOut = remote.Calendar{} + err := c.rbuilder.Users().ID(remoteUserID).Calendars().Request().JSONRequest(c.ctx, http.MethodPost, "", &calIn, &calOut) + if err != nil { + return nil, errors.Wrap(err, "msgraph CreateCalendar") + } + c.Logger.With(bot.LogContext{ + "v": calOut, + }).Infof("msgraph: CreateCalendar created the following calendar.") + return &calOut, nil +} diff --git a/server/remote/gcal/create_event.go b/server/remote/gcal/create_event.go new file mode 100644 index 00000000..4e696cde --- /dev/null +++ b/server/remote/gcal/create_event.go @@ -0,0 +1,26 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +// CreateEvent creates a calendar event +func (c *client) CreateEvent(remoteUserID string, in *remote.Event) (*remote.Event, error) { + if true { + return nil, errors.New("gcal CreateEvent not implemented") + } + + var out = remote.Event{} + err := c.rbuilder.Users().ID(remoteUserID).Events().Request().JSONRequest(c.ctx, http.MethodPost, "", &in, &out) + if err != nil { + return nil, errors.Wrap(err, "msgraph CreateEvent") + } + return &out, nil +} diff --git a/server/remote/gcal/delete_calendar.go b/server/remote/gcal/delete_calendar.go new file mode 100644 index 00000000..212280be --- /dev/null +++ b/server/remote/gcal/delete_calendar.go @@ -0,0 +1,23 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" +) + +func (c *client) DeleteCalendar(remoteUserID string, calID string) error { + if true { + return errors.New("gcal DeleteCalendar not implemented") + } + + err := c.rbuilder.Users().ID(remoteUserID).Calendars().ID(calID).Request().Delete(c.ctx) + if err != nil { + return errors.Wrap(err, "msgraph DeleteCalendar") + } + c.Logger.With(bot.LogContext{}).Infof("msgraph: DeleteCalendar deleted calendar `%v`.", calID) + return nil +} diff --git a/server/remote/gcal/event.go b/server/remote/gcal/event.go new file mode 100644 index 00000000..841e0094 --- /dev/null +++ b/server/remote/gcal/event.go @@ -0,0 +1,67 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "net/http" + + "github.com/pkg/errors" + msgraph "github.com/yaegashi/msgraph.go/v1.0" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +func (c *client) GetEvent(remoteUserID, eventID string) (*remote.Event, error) { + if true { + return nil, errors.New("gcal GetEvent not implemented") + } + + e := &remote.Event{} + + err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Request().JSONRequest( + c.ctx, http.MethodGet, "", nil, &e) + if err != nil { + return nil, errors.Wrap(err, "msgraph GetEvent") + } + return e, nil +} + +func (c *client) AcceptEvent(remoteUserID, eventID string) error { + if true { + return errors.New("gcal AcceptEvent not implemented") + } + + dummy := &msgraph.EventAcceptRequestParameter{} + err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Accept(dummy).Request().Post(c.ctx) + if err != nil { + return errors.Wrap(err, "msgraph Accept Event") + } + return nil +} + +func (c *client) DeclineEvent(remoteUserID, eventID string) error { + if true { + return errors.New("gcal DeclineEvent not implemented") + } + + dummy := &msgraph.EventDeclineRequestParameter{} + err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).Decline(dummy).Request().Post(c.ctx) + if err != nil { + return errors.Wrap(err, "msgraph DeclineEvent") + } + return nil +} + +func (c *client) TentativelyAcceptEvent(remoteUserID, eventID string) error { + if true { + return errors.New("gcal TentativelyAcceptEvent not implemented") + } + + dummy := &msgraph.EventTentativelyAcceptRequestParameter{} + err := c.rbuilder.Users().ID(remoteUserID).Events().ID(eventID).TentativelyAccept(dummy).Request().Post(c.ctx) + if err != nil { + return errors.Wrap(err, "msgraph TentativelyAcceptEvent") + } + return nil +} diff --git a/server/remote/gcal/find_meeting_times.go b/server/remote/gcal/find_meeting_times.go new file mode 100644 index 00000000..28ed059b --- /dev/null +++ b/server/remote/gcal/find_meeting_times.go @@ -0,0 +1,27 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +// FindMeetingTimes finds meeting time suggestions for a calendar event +func (c *client) FindMeetingTimes(remoteUserID string, params *remote.FindMeetingTimesParameters) (*remote.MeetingTimeSuggestionResults, error) { + if true { + return nil, errors.New("gcal FindMeetingTimes not implemented") + } + + meetingsOut := &remote.MeetingTimeSuggestionResults{} + req := c.rbuilder.Users().ID(remoteUserID).FindMeetingTimes(nil).Request() + err := req.JSONRequest(c.ctx, http.MethodPost, "", ¶ms, &meetingsOut) + if err != nil { + return nil, errors.Wrap(err, "msgraph FindMeetingTimes") + } + return meetingsOut, nil +} diff --git a/server/remote/gcal/get_calendars.go b/server/remote/gcal/get_calendars.go new file mode 100644 index 00000000..faaae97d --- /dev/null +++ b/server/remote/gcal/get_calendars.go @@ -0,0 +1,64 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "context" + "net/http" + + "github.com/pkg/errors" + "google.golang.org/api/calendar/v3" + "google.golang.org/api/option" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" +) + +func (c *client) GetCalendars(remoteUserID string) ([]*remote.Calendar, error) { + if true { + return nil, errors.New("gcal GetCalendars not implemented") + } + + var v struct { + Value []*remote.Calendar `json:"value"` + } + req := c.rbuilder.Users().ID(remoteUserID).Calendars().Request() + req.Expand("children") + err := req.JSONRequest(c.ctx, http.MethodGet, "", nil, &v) + if err != nil { + return nil, errors.Wrap(err, "msgraph GetCalendars") + } + c.Logger.With(bot.LogContext{ + "UserID": remoteUserID, + "v": v.Value, + }).Infof("msgraph: GetUserCalendars returned `%d` calendars.", len(v.Value)) + return v.Value, nil +} + +func (c *client) GetDefaultCalendar() (*remote.Calendar, error) { + service, err := calendar.NewService(context.Background(), option.WithHTTPClient(c.httpClient)) + if err != nil { + return nil, errors.Wrap(err, "gcal GetNotificationData, error creating service") + } + + req := service.Calendars.Get(defaultCalendarName) + googleCal, err := req.Do() + if err != nil { + return nil, errors.Wrap(err, "gcal GetDefaultCalendar, error getting calendar") + } + + remoteCal := convertGoogleCalendarToRemoteCalendar(googleCal) + + return remoteCal, nil +} + +func convertGoogleCalendarToRemoteCalendar(cal *calendar.Calendar) *remote.Calendar { + return &remote.Calendar{ + ID: cal.Id, + Name: cal.Summary, + Events: []remote.Event{}, + CalendarView: []remote.Event{}, + Owner: nil, + } +} diff --git a/server/remote/gcal/get_default_calendar_view.go b/server/remote/gcal/get_default_calendar_view.go new file mode 100644 index 00000000..e6129c8c --- /dev/null +++ b/server/remote/gcal/get_default_calendar_view.go @@ -0,0 +1,226 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "context" + "net/http" + "net/url" + "time" + + "github.com/pkg/errors" + "google.golang.org/api/calendar/v3" + "google.golang.org/api/option" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +const ( + RemoteEventBusy = "busy" + RemoteEventFree = "free" + + GoogleEventBusy = "opaque" + GoogleEventFree = "transparent" + + ResponseYes = "accepted" + ResponseMaybe = "tentativelyAccepted" + ResponseNo = "declined" + ResponseNone = "notResponded" + + GoogleResponseStatusYes = "accepted" + GoogleResponseStatusMaybe = "tentative" + GoogleResponseStatusNo = "declined" + GoogleResponseStatusNone = "needsAction" +) + +var responseStatusConversion = map[string]string{ + GoogleResponseStatusYes: ResponseYes, + GoogleResponseStatusMaybe: ResponseMaybe, + GoogleResponseStatusNo: ResponseNo, + GoogleResponseStatusNone: ResponseNone, +} + +func (c *client) GetDefaultCalendarView(remoteUserID string, start, end time.Time) ([]*remote.Event, error) { + service, err := calendar.NewService(context.Background(), option.WithHTTPClient(c.httpClient)) + if err != nil { + return nil, errors.Wrap(err, "gcal GetDefaultCalendarView, error creating service") + } + + req := service.Events.List("primary") + req.MaxResults(20) + req.TimeMin(start.Format(time.RFC3339)) + req.TimeMax(end.Format(time.RFC3339)) + req.SingleEvents(true) + req.OrderBy("startTime") + + events, err := req.Do() + if err != nil { + return nil, errors.Wrap(err, "gcal GetDefaultCalendarView, error performing request") + } + + result := []*remote.Event{} + if len(events.Items) == 0 { + return result, nil + } + + for _, event := range events.Items { + if event.ICalUID != "" { + result = append(result, convertGCalEventToRemoteEvent(event)) + } + } + + return result, nil +} + +func convertGCalEventToRemoteEvent(event *calendar.Event) *remote.Event { + showAs := RemoteEventBusy + if event.Transparency == GoogleEventFree { + showAs = RemoteEventFree + } + + start := remote.NewGoogleDateTime(event.Start) + end := remote.NewGoogleDateTime(event.End) + + location := &remote.Location{ + DisplayName: event.Location, + } + + organizer := &remote.Attendee{ + EmailAddress: &remote.EmailAddress{ + Name: event.Organizer.Email, + Address: event.Organizer.Email, + }, + } + + var responseStatus *remote.EventResponseStatus + responseRequested := false + isOrganizer := false + + attendees := []*remote.Attendee{} + for _, attendee := range event.Attendees { + attendees = append(attendees, &remote.Attendee{ + Status: &remote.EventResponseStatus{ + Response: attendee.ResponseStatus, + }, + EmailAddress: &remote.EmailAddress{ + Name: attendee.Email, + Address: attendee.Email, + }, + }) + + if attendee.Self { + if attendee.ResponseStatus == GoogleResponseStatusNone { + responseRequested = true + } + + response := responseStatusConversion[attendee.ResponseStatus] + responseStatus = &remote.EventResponseStatus{ + Response: response, + } + + isOrganizer = attendee.Organizer + } + } + + isAllDay := len(event.Start.Date) > 0 // if Date field is present, it is all-day. as opposed to DateTime field + + return &remote.Event{ + ID: event.Id, + ICalUID: event.ICalUID, + Subject: event.Summary, + Body: &remote.ItemBody{Content: event.Description}, + BodyPreview: event.Description, // GCAL TODO no body preview available? + IsAllDay: isAllDay, + ShowAs: showAs, + Weblink: event.HtmlLink, + Start: start, + End: end, + Location: location, + Organizer: organizer, + Attendees: attendees, + ResponseStatus: responseStatus, + IsCancelled: event.Status == "cancelled", + IsOrganizer: isOrganizer, + ResponseRequested: responseRequested, + // Importance string + // ReminderMinutesBeforeStart int + } +} + +/* + Rest of file is unimplemented batch request stuff +*/ + +type calendarViewResponse struct { + Value []*remote.Event `json:"value,omitempty"` + Error *remote.APIError `json:"error,omitempty"` +} + +type calendarViewSingleResponse struct { + ID string `json:"id"` + Status int `json:"status"` + Body calendarViewResponse `json:"body"` + Headers map[string]string `json:"headers"` +} + +type calendarViewBatchResponse struct { + Responses []*calendarViewSingleResponse `json:"responses"` +} + +func (c *client) DoBatchViewCalendarRequests(allParams []*remote.ViewCalendarParams) ([]*remote.ViewCalendarResponse, error) { + if true { + return nil, errors.New("gcal DoBatchViewCalendarRequests not implemented") + } + + requests := []*singleRequest{} + for _, params := range allParams { + u := getCalendarViewURL(params) + req := &singleRequest{ + ID: params.RemoteUserID, + URL: u, + Method: http.MethodGet, + Headers: map[string]string{}, + } + requests = append(requests, req) + } + + batchRequests := prepareBatchRequests(requests) + var batchResponses []*calendarViewBatchResponse + for _, req := range batchRequests { + batchRes := &calendarViewBatchResponse{} + err := c.batchRequest(req, batchRes) + if err != nil { + return nil, errors.Wrap(err, "msgraph ViewCalendar batch request") + } + + batchResponses = append(batchResponses, batchRes) + } + + result := []*remote.ViewCalendarResponse{} + for _, batchRes := range batchResponses { + for _, res := range batchRes.Responses { + viewCalRes := &remote.ViewCalendarResponse{ + RemoteUserID: res.ID, + Events: res.Body.Value, + Error: res.Body.Error, + } + result = append(result, viewCalRes) + } + } + + return result, nil +} + +func getCalendarViewURL(params *remote.ViewCalendarParams) string { + paramStr := getQueryParamStringForCalendarView(params.StartTime, params.EndTime) + return "/Users/" + params.RemoteUserID + "/calendarView" + paramStr +} + +func getQueryParamStringForCalendarView(start, end time.Time) string { + q := url.Values{} + q.Add("startDateTime", start.Format(time.RFC3339)) + q.Add("endDateTime", end.Format(time.RFC3339)) + q.Add("$top", "20") + return "?" + q.Encode() +} diff --git a/server/remote/gcal/get_mailbox_settings.go b/server/remote/gcal/get_mailbox_settings.go new file mode 100644 index 00000000..6aef4169 --- /dev/null +++ b/server/remote/gcal/get_mailbox_settings.go @@ -0,0 +1,16 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +func (c *client) GetMailboxSettings(remoteUserID string) (*remote.MailboxSettings, error) { + // GCAL TODO + out := &remote.MailboxSettings{ + TimeZone: "Eastern Standard Time", + } + return out, nil +} diff --git a/server/remote/gcal/get_me.go b/server/remote/gcal/get_me.go new file mode 100644 index 00000000..a5a04ef9 --- /dev/null +++ b/server/remote/gcal/get_me.go @@ -0,0 +1,53 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "context" + + "github.com/pkg/errors" + + "google.golang.org/api/option" + "google.golang.org/api/people/v1" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +const personFields = "names,emailAddresses" + +func (c *client) GetMe() (*remote.User, error) { + service, err := people.NewService(context.Background(), option.WithHTTPClient(c.httpClient)) + if err != nil { + return nil, errors.Wrap(err, "gcal GetMe, error creating service") + } + + req := service.People.Get("people/me") + req.PersonFields(personFields) + user, err := req.Do() + if err != nil { + return nil, errors.Wrap(err, "gcal GetMe, error performing request") + } + + name := "No name" + principalName := "" + email := "No email" + + if len(user.Names) > 0 { + name = user.Names[0].DisplayName + } + + if len(user.EmailAddresses) > 0 { + // for some reason this is always blank + email = user.EmailAddresses[0].Value + } + + remoteUser := &remote.User{ + ID: user.ResourceName, + DisplayName: name, + UserPrincipalName: principalName, + Mail: email, + } + + return remoteUser, nil +} diff --git a/server/remote/gcal/get_notification_data.go b/server/remote/gcal/get_notification_data.go new file mode 100644 index 00000000..e1069726 --- /dev/null +++ b/server/remote/gcal/get_notification_data.go @@ -0,0 +1,41 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "context" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/pkg/errors" + "google.golang.org/api/calendar/v3" + "google.golang.org/api/option" +) + +func (c *client) GetNotificationData(orig *remote.Notification) (*remote.Notification, error) { + service, err := calendar.NewService(context.Background(), option.WithHTTPClient(c.httpClient)) + if err != nil { + return nil, errors.Wrap(err, "gcal GetNotificationData, error creating service") + } + + n := *orig + wh := n.Webhook.(*webhook) + + cal, err := c.GetDefaultCalendar() + if err != nil { + return nil, errors.Wrap(err, "gcal GetNotificationData, error getting default calendar") + } + + reqBody := service.Events.Get(cal.ID, wh.Resource) + googleEvent, err := reqBody.Do() + if err != nil { + return nil, errors.Wrap(err, "gcal GetNotificationData, error fetching event data") + } + + event := convertGCalEventToRemoteEvent(googleEvent) + + n.Event = event + n.IsBare = false + + return &n, nil +} diff --git a/server/remote/gcal/get_schedule_batched.go b/server/remote/gcal/get_schedule_batched.go new file mode 100644 index 00000000..b520bcaf --- /dev/null +++ b/server/remote/gcal/get_schedule_batched.go @@ -0,0 +1,104 @@ +package gcal + +import ( + "net/http" + + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +type getScheduleResponse struct { + Value []*remote.ScheduleInformation `json:"value,omitempty"` + Error *remote.APIError `json:"error,omitempty"` +} + +type getScheduleSingleResponse struct { + ID string `json:"id"` + Status int `json:"status"` + Body getScheduleResponse `json:"body"` + Headers map[string]string `json:"headers"` +} + +type getScheduleBatchResponse struct { + Responses []*getScheduleSingleResponse `json:"responses"` +} + +type getScheduleRequestParams struct { + // List of emails of users that we want to check + Schedules []string `json:"schedules"` + + // Overall start and end of entire search window + StartTime *remote.DateTime `json:"startTime"` + EndTime *remote.DateTime `json:"endTime"` + + /* + Size of each chunk of time we want to check + This can be equal to end - start if we want, or we can get more granular results by making it shorter. + For the graph API: The default is 30 minutes, minimum is 6, maximum is 1440 + 15 is currently being used on our end + */ + AvailabilityViewInterval int `json:"availabilityViewInterval"` +} + +func (c *client) GetSchedule(requests []*remote.ScheduleUserInfo, startTime, endTime *remote.DateTime, availabilityViewInterval int) ([]*remote.ScheduleInformation, error) { + if true { + return nil, errors.New("gcal GetSchedule not implemented") + } + + params := &getScheduleRequestParams{ + StartTime: startTime, + EndTime: endTime, + AvailabilityViewInterval: availabilityViewInterval, + } + + allRequests := []*singleRequest{} + for _, req := range requests { + allRequests = append(allRequests, makeSingleRequestForGetSchedule(req, params)) + } + batchRequests := prepareBatchRequests(allRequests) + + var batchResponses []*getScheduleBatchResponse + + for _, req := range batchRequests { + res := &getScheduleBatchResponse{} + err := c.batchRequest(req, res) + if err != nil { + return nil, errors.Wrap(err, "msgraph batch GetSchedule") + } + + batchResponses = append(batchResponses, res) + } + + result := []*remote.ScheduleInformation{} + for _, batchRes := range batchResponses { + for _, r := range batchRes.Responses { + if r.Body.Error == nil { + result = append(result, r.Body.Value...) + } else { + c.Warnf("Failed to process schedule. err=%s", r.Body.Error.Message) + } + } + } + + return result, nil +} + +func makeSingleRequestForGetSchedule(request *remote.ScheduleUserInfo, params *getScheduleRequestParams) *singleRequest { + u := "/Users/" + request.RemoteUserID + "/calendar/getSchedule" + req := &singleRequest{ + URL: u, + Method: http.MethodPost, + ID: request.RemoteUserID, + Body: &getScheduleRequestParams{ + Schedules: []string{request.Mail}, + StartTime: params.StartTime, + EndTime: params.EndTime, + AvailabilityViewInterval: params.AvailabilityViewInterval, + }, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + return req +} diff --git a/server/remote/gcal/get_schedule_batched_test.go b/server/remote/gcal/get_schedule_batched_test.go new file mode 100644 index 00000000..08c81d4b --- /dev/null +++ b/server/remote/gcal/get_schedule_batched_test.go @@ -0,0 +1,39 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +func TestMakeSingleRequestForGetSchedule(t *testing.T) { + start := time.Now().UTC() + end := time.Now().UTC().Add(20) + params := &getScheduleRequestParams{ + StartTime: remote.NewDateTime(start, "UTC"), + EndTime: remote.NewDateTime(end, "UTC"), + AvailabilityViewInterval: 15, + } + req := &remote.ScheduleUserInfo{ + RemoteUserID: "remote_user_id", + Mail: "mail@example.com", + } + + out := makeSingleRequestForGetSchedule(req, params) + require.Equal(t, "/Users/remote_user_id/calendar/getSchedule", out.URL) + require.Equal(t, "POST", out.Method) + require.Equal(t, 1, len(out.Headers)) + require.Equal(t, "application/json", out.Headers["Content-Type"]) + + body := out.Body.(*getScheduleRequestParams) + require.Equal(t, params.StartTime.String(), body.StartTime.String()) + require.Equal(t, params.EndTime.String(), body.EndTime.String()) + require.Equal(t, 15, body.AvailabilityViewInterval) + require.Equal(t, "mail@example.com", body.Schedules[0]) +} diff --git a/server/remote/gcal/get_super_user_token.go b/server/remote/gcal/get_super_user_token.go new file mode 100644 index 00000000..e833b3e8 --- /dev/null +++ b/server/remote/gcal/get_super_user_token.go @@ -0,0 +1,46 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "net/http" + "net/url" + + "github.com/pkg/errors" +) + +type AuthResponse struct { + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + AccessToken string `json:"access_token"` +} + +func (c *client) GetSuperuserToken() (string, error) { + if true { + return "", errors.New("gcal GetSuperuserToken not implemented") + } + + params := map[string]string{ + "client_id": c.conf.OAuth2ClientID, + "scope": "https://graph.microsoft.com/.default", + "client_secret": c.conf.OAuth2ClientSecret, + "grant_type": "client_credentials", + } + + u := "https://login.microsoftonline.com/" + c.conf.OAuth2Authority + "/oauth2/v2.0/token" + res := AuthResponse{} + + data := url.Values{} + data.Set("client_id", params["client_id"]) + data.Set("scope", params["scope"]) + data.Set("client_secret", params["client_secret"]) + data.Set("grant_type", params["grant_type"]) + + _, err := c.CallFormPost(http.MethodPost, u, data, &res) + if err != nil { + return "", errors.Wrap(err, "msgraph GetSuperuserToken") + } + + return res.AccessToken, nil +} diff --git a/server/remote/gcal/handle_webhook.go b/server/remote/gcal/handle_webhook.go new file mode 100644 index 00000000..7f557b25 --- /dev/null +++ b/server/remote/gcal/handle_webhook.go @@ -0,0 +1,62 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "net/http" + "time" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" +) + +const renewSubscriptionBeforeExpiration = 12 * time.Hour + +type webhook struct { + ChangeType string `json:"changeType"` + ClientState string `json:"clientState,omitempty"` + Resource string `json:"resource,omitempty"` + SubscriptionExpirationDateTime string `json:"subscriptionExpirationDateTime,omitempty"` + SubscriptionID string `json:"subscriptionId"` + ResourceData struct { + DataType string `json:"@odata.type"` + } `json:"resourceData"` +} + +const ( + resourceStateSync = "sync" + resourceStateExists = "exists" + resourceStateNotExists = "not_exists" +) + +func (r *impl) HandleWebhook(w http.ResponseWriter, req *http.Request) []*remote.Notification { + resourceState := req.Header.Get("X-Goog-Resource-State") + if resourceState == resourceStateSync { + w.WriteHeader(http.StatusAccepted) + return []*remote.Notification{} + } + + notificationChannelID := req.Header.Get("X-Goog-Channel-Id") + resourceID := req.Header.Get("X-Goog-Resource-Id") + token := req.Header.Get("X-Goog-Channel-Token") + + wh := &webhook{ + SubscriptionID: notificationChannelID, + ClientState: token, + Resource: resourceID, + } + + n := &remote.Notification{ + SubscriptionID: notificationChannelID, + // ChangeType: wh.ChangeType, // not needed + ClientState: wh.ClientState, + IsBare: true, + // WebhookRawData: rawData, + Webhook: wh, + } + + w.WriteHeader(http.StatusAccepted) + + notifications := []*remote.Notification{n} + return notifications +} diff --git a/server/remote/gcal/remote.go b/server/remote/gcal/remote.go new file mode 100644 index 00000000..d86d72d1 --- /dev/null +++ b/server/remote/gcal/remote.go @@ -0,0 +1,86 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "context" + "net/http" + + "golang.org/x/oauth2" + "google.golang.org/api/calendar/v3" + "google.golang.org/api/people/v1" + + // msgraph "github.com/yaegashi/msgraph.go/v1.0" + "golang.org/x/oauth2/google" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/config" + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" +) + +const Kind = "gcal" + +type impl struct { + conf *config.Config + logger bot.Logger +} + +func init() { + remote.Makers[Kind] = NewRemote +} + +func NewRemote(conf *config.Config, logger bot.Logger) remote.Remote { + return &impl{ + conf: conf, + logger: logger, + } +} + +// MakeClient creates a new client for user-delegated permissions. +func (r *impl) MakeClient(ctx context.Context, token *oauth2.Token) remote.Client { + httpClient := r.NewOAuth2Config().Client(ctx, token) + c := &client{ + conf: r.conf, + ctx: ctx, + httpClient: httpClient, + Logger: r.logger, + rbuilder: nil, + } + return c +} + +// MakeSuperuserClient creates a new client used for app-only permissions. +func (r *impl) MakeSuperuserClient(ctx context.Context) (remote.Client, error) { + httpClient := &http.Client{} + c := &client{ + conf: r.conf, + ctx: ctx, + httpClient: httpClient, + Logger: r.logger, + // rbuilder: msgraph.NewClient(httpClient), + } + token, err := c.GetSuperuserToken() + if err != nil { + return nil, err + } + + o := &oauth2.Token{ + AccessToken: token, + TokenType: "Bearer", + } + return r.MakeClient(ctx, o), nil +} + +func (r *impl) NewOAuth2Config() *oauth2.Config { + return &oauth2.Config{ + ClientID: r.conf.OAuth2ClientID, + ClientSecret: r.conf.OAuth2ClientSecret, + RedirectURL: r.conf.PluginURL + config.FullPathOAuth2Redirect, + Scopes: []string{ + calendar.CalendarEventsScope, + people.UserinfoProfileScope, + }, + Endpoint: google.Endpoint, + } +} diff --git a/server/remote/gcal/subscription.go b/server/remote/gcal/subscription.go new file mode 100644 index 00000000..215f6f97 --- /dev/null +++ b/server/remote/gcal/subscription.go @@ -0,0 +1,113 @@ +// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved. +// See License for license information. + +package gcal + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "time" + + "github.com/pkg/errors" + "google.golang.org/api/calendar/v3" + "google.golang.org/api/option" + + "github.com/mattermost/mattermost-plugin-mscalendar/server/remote" + "github.com/mattermost/mattermost-plugin-mscalendar/server/utils/bot" +) + +const subscribeTTL = 48 * time.Hour + +const defaultCalendarName = "primary" +const googleSubscriptionType = "webhook" +const subscriptionSuffix = "_calendar_event_notifications_" + +func newRandomString() string { + b := make([]byte, 96) + rand.Read(b) + return base64.URLEncoding.EncodeToString(b) +} + +func (c *client) CreateMySubscription(notificationURL, remoteUserID string) (*remote.Subscription, error) { + service, err := calendar.NewService(context.Background(), option.WithHTTPClient(c.httpClient)) + if err != nil { + return nil, errors.Wrap(err, "gcal CreateMySubscription, error creating service") + } + + reqBody := &calendar.Channel{ + Id: remoteUserID + subscriptionSuffix + newRandomString(), + Token: newRandomString(), + Type: googleSubscriptionType, + Address: notificationURL, + Params: map[string]string{ + "ttl": fmt.Sprintf("%d", int64(subscribeTTL.Seconds())), + }, + } + + createSubscriptionRequest := service.Events.Watch(defaultCalendarName, reqBody) + googleSubscription, err := createSubscriptionRequest.Do() + if err != nil { + return nil, errors.Wrap(err, "gcal CreateMySubscription, error creating subscription") + } + + sub := &remote.Subscription{ + ID: googleSubscription.Id, + Resource: defaultCalendarName, + // ChangeType: "created,updated,deleted", + NotificationURL: notificationURL, + ExpirationDateTime: time.Now().Add(time.Second * time.Duration(googleSubscription.Expiration)).Format(time.RFC3339), + ClientState: reqBody.Token, + CreatorID: remoteUserID, + } + + c.Logger.With(bot.LogContext{ + "subscriptionID": sub.ID, + "resource": sub.Resource, + // "changeType": sub.ChangeType, + "expirationDateTime": sub.ExpirationDateTime, + }).Debugf("gcal: created subscription.") + + return sub, nil +} + +func (c *client) DeleteSubscription(subscriptionID string) error { + service, err := calendar.NewService(context.Background(), option.WithHTTPClient(c.httpClient)) + if err != nil { + return errors.Wrap(err, "gcal DeleteSubscription, error creating service") + } + + stopRequest := service.Channels.Stop(&calendar.Channel{Id: subscriptionID}) + err = stopRequest.Do() + + if err != nil { + return errors.Wrap(err, "gcal DeleteSubscription, error from google response") + } + + c.Logger.With(bot.LogContext{ + "subscriptionID": subscriptionID, + }).Debugf("gcal: deleted subscription.") + + return nil +} + +func (c *client) RenewSubscription(notificationURL, remoteUserID, subscriptionID string) (*remote.Subscription, error) { + err := c.DeleteSubscription(subscriptionID) + if err != nil { + return nil, errors.Wrap(err, "gcal RenewSubscription, error deleting subscription") + } + + sub, err := c.CreateMySubscription(notificationURL, remoteUserID) + if err != nil { + return nil, errors.Wrap(err, "gcal RenewSubscription, error deleting subscription") + } + + c.Logger.Debugf("gcal: renewed subscription.") + + return sub, nil +} + +func (c *client) ListSubscriptions() ([]*remote.Subscription, error) { + return nil, errors.New("gcal ListSubscriptions not implemented. only used for debug command") +} diff --git a/server/remote/mock_remote/mock_client.go b/server/remote/mock_remote/mock_client.go index ca6358ab..55b9c355 100644 --- a/server/remote/mock_remote/mock_client.go +++ b/server/remote/mock_remote/mock_client.go @@ -111,18 +111,18 @@ func (mr *MockClientMockRecorder) CreateEvent(arg0, arg1 interface{}) *gomock.Ca } // CreateMySubscription mocks base method. -func (m *MockClient) CreateMySubscription(arg0 string) (*remote.Subscription, error) { +func (m *MockClient) CreateMySubscription(arg0, arg1 string) (*remote.Subscription, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateMySubscription", arg0) + ret := m.ctrl.Call(m, "CreateMySubscription", arg0, arg1) ret0, _ := ret[0].(*remote.Subscription) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateMySubscription indicates an expected call of CreateMySubscription. -func (mr *MockClientMockRecorder) CreateMySubscription(arg0 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) CreateMySubscription(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMySubscription", reflect.TypeOf((*MockClient)(nil).CreateMySubscription), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMySubscription", reflect.TypeOf((*MockClient)(nil).CreateMySubscription), arg0, arg1) } // DeclineEvent mocks base method. @@ -287,21 +287,6 @@ func (mr *MockClientMockRecorder) GetNotificationData(arg0 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationData", reflect.TypeOf((*MockClient)(nil).GetNotificationData), arg0) } -// GetSchedule mocks base method. -func (m *MockClient) GetSchedule(arg0 []*remote.ScheduleUserInfo, arg1, arg2 *remote.DateTime, arg3 int) ([]*remote.ScheduleInformation, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSchedule", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].([]*remote.ScheduleInformation) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetSchedule indicates an expected call of GetSchedule. -func (mr *MockClientMockRecorder) GetSchedule(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchedule", reflect.TypeOf((*MockClient)(nil).GetSchedule), arg0, arg1, arg2, arg3) -} - // GetSuperuserToken mocks base method. func (m *MockClient) GetSuperuserToken() (string, error) { m.ctrl.T.Helper() @@ -333,18 +318,18 @@ func (mr *MockClientMockRecorder) ListSubscriptions() *gomock.Call { } // RenewSubscription mocks base method. -func (m *MockClient) RenewSubscription(arg0 string) (*remote.Subscription, error) { +func (m *MockClient) RenewSubscription(arg0, arg1, arg2 string) (*remote.Subscription, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RenewSubscription", arg0) + ret := m.ctrl.Call(m, "RenewSubscription", arg0, arg1, arg2) ret0, _ := ret[0].(*remote.Subscription) ret1, _ := ret[1].(error) return ret0, ret1 } // RenewSubscription indicates an expected call of RenewSubscription. -func (mr *MockClientMockRecorder) RenewSubscription(arg0 interface{}) *gomock.Call { +func (mr *MockClientMockRecorder) RenewSubscription(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewSubscription", reflect.TypeOf((*MockClient)(nil).RenewSubscription), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewSubscription", reflect.TypeOf((*MockClient)(nil).RenewSubscription), arg0, arg1, arg2) } // TentativelyAcceptEvent mocks base method. diff --git a/server/remote/msgraph/subscription.go b/server/remote/msgraph/subscription.go index 85e5848b..df770f0a 100644 --- a/server/remote/msgraph/subscription.go +++ b/server/remote/msgraph/subscription.go @@ -23,7 +23,7 @@ func newRandomString() string { return base64.URLEncoding.EncodeToString(b) } -func (c *client) CreateMySubscription(notificationURL string) (*remote.Subscription, error) { +func (c *client) CreateMySubscription(notificationURL, remoteUserID string) (*remote.Subscription, error) { sub := &remote.Subscription{ Resource: "me/events", ChangeType: "created,updated,deleted", @@ -59,7 +59,7 @@ func (c *client) DeleteSubscription(subscriptionID string) error { return nil } -func (c *client) RenewSubscription(subscriptionID string) (*remote.Subscription, error) { +func (c *client) RenewSubscription(notificationURL, remoteUserID, subscriptionID string) (*remote.Subscription, error) { expires := time.Now().Add(subscribeTTL) v := struct { ExpirationDateTime string `json:"expirationDateTime"`