From a60dc6ac86d55de269c8963dcc87214a12911321 Mon Sep 17 00:00:00 2001 From: Chetanya Kandhari Date: Sun, 18 Oct 2020 18:28:43 +0530 Subject: [PATCH 1/8] Create slash commands to allow each MM user to connect to their twitter account. --- .gitignore | 2 + .golangci.yml | 4 +- README.md | 43 +++++----- assets/logo.svg | 1 + assets/templates/oauth1-complete.html | 75 +++++++++++++++++ assets/twitter.png | Bin 0 -> 8862 bytes go.mod | 15 +++- go.sum | 49 +++++++++++ plugin.json | 32 +++++-- server/command/command.go | 49 +++++++++++ server/command/twitter.go | 96 +++++++++++++++++++++ server/config/main.go | 70 ++++++++++++++++ server/configuration.go | 83 ------------------ server/constant/constants.go | 17 ++++ server/controller/main.go | 87 +++++++++++++++++++ server/controller/oauth1.go | 91 ++++++++++++++++++++ server/controller/utils.go | 31 +++++++ server/manifest.go | 38 +++++++-- server/plugin.go | 108 +++++++++++++++++++++--- server/serializers/oauth1.go | 6 ++ server/serializers/twitter-user.go | 8 ++ server/store/main.go | 67 +++++++++++++++ server/store/ots.go | 54 ++++++++++++ server/store/twitter-user.go | 27 ++++++ server/store/utils.go | 116 ++++++++++++++++++++++++++ server/util/command.go | 77 +++++++++++++++++ server/util/hash.go | 19 +++++ server/util/post.go | 20 +++++ server/util/utils.go | 51 +++++++++++ 29 files changed, 1204 insertions(+), 132 deletions(-) create mode 100755 assets/logo.svg create mode 100644 assets/templates/oauth1-complete.html create mode 100755 assets/twitter.png create mode 100644 server/command/command.go create mode 100644 server/command/twitter.go create mode 100644 server/config/main.go delete mode 100644 server/configuration.go create mode 100644 server/constant/constants.go create mode 100644 server/controller/main.go create mode 100644 server/controller/oauth1.go create mode 100644 server/controller/utils.go create mode 100644 server/serializers/oauth1.go create mode 100644 server/serializers/twitter-user.go create mode 100644 server/store/main.go create mode 100644 server/store/ots.go create mode 100644 server/store/twitter-user.go create mode 100644 server/store/utils.go create mode 100644 server/util/command.go create mode 100644 server/util/hash.go create mode 100644 server/util/post.go create mode 100644 server/util/utils.go diff --git a/.gitignore b/.gitignore index 16f09f6..a6671c8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ dist/ # Jetbrains .idea/ + +vendor diff --git a/.golangci.yml b/.golangci.yml index 771c0e8..73e063f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -47,9 +47,9 @@ issues: - deadcode - unused - varcheck - - path: server/configuration.go + - path: server/util/hash.go linters: - - unused + - gosec - path: _test\.go linters: - bodyclose diff --git a/README.md b/README.md index 7b951b6..826b555 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,10 @@ # Plugin Starter Template [![CircleCI branch](https://img.shields.io/circleci/project/github/mattermost/mattermost-plugin-starter-template/master.svg)](https://circleci.com/gh/mattermost/mattermost-plugin-starter-template) -This plugin serves as a starting point for writing a Mattermost plugin. Feel free to base your own plugin off this repository. - -To learn more about plugins, see [our plugin documentation](https://developers.mattermost.com/extend/plugins/). +A Mattermost plugin to connect to twitter. ## Getting Started -Use GitHub's template feature to make a copy of this repository by clicking the "Use this template" button. - -Alternatively shallow clone the repository matching your plugin name: -``` -git clone --depth 1 https://github.com/mattermost/mattermost-plugin-starter-template com.example.my-plugin -``` -Note that this project uses [Go modules](https://github.com/golang/go/wiki/Modules). Be sure to locate the project outside of `$GOPATH`. - -Edit `plugin.json` with your `id`, `name`, and `description`: -``` -{ - "id": "com.example.my-plugin", - "name": "My Plugin", - "description": "A plugin to enhance Mattermost." -} -``` +To learn more about plugins, see [our plugin documentation](https://developers.mattermost.com/extend/plugins/). Build your plugin: ``` @@ -31,9 +14,29 @@ make This will produce a single plugin file (with support for multiple architectures) for upload to your Mattermost server: ``` -dist/com.example.my-plugin.tar.gz +dist/com.mattermost.twitter.tar.gz ``` +## Configuration + +Getting the Twitter Consumer Key (API Key) and Consumer Secret key is very simple, just follow the below 4 steps and you are ready to go. + +- Go to https://dev.twitter.com/apps/new and log in, if necessary +- Supply the necessary required fields, accept the Terms Of Service, and solve the CAPTCHA. +- Submit the form +- Go to the API Keys tab, there you will find your Consumer key and Consumer secret keys. +- Copy the consumer key (API key) and consumer secret from the screen into our application. + + +Enable the 3-legged OAuth. +- In your app settings page of the app you just created, select `Enable 3-legged OAuth`. +https://developer.twitter.com/en/portal/projects//apps//auth-settings + +- Set the callbackURL to `/plugins/com.mattermost.twitter/twitter/callback`. +- Set the Website URL to `your-mattermost-url`. + + + ## Development To avoid having to manually install your plugin, build and deploy your plugin using one of the following options. diff --git a/assets/logo.svg b/assets/logo.svg new file mode 100755 index 0000000..2832e7b --- /dev/null +++ b/assets/logo.svg @@ -0,0 +1 @@ +Twitter_Logo_Blue \ No newline at end of file diff --git a/assets/templates/oauth1-complete.html b/assets/templates/oauth1-complete.html new file mode 100644 index 0000000..3850c5e --- /dev/null +++ b/assets/templates/oauth1-complete.html @@ -0,0 +1,75 @@ + + + + + + + +
+

+ + + + Mattermost user is now connected to Twitter. +

+
+
Mattermost account: {{ .MattermostDisplayName }}
+
Twitter account: {{ .TwitterDisplayName }}
+
+ You can use `/twitter disconnect` to disconnect your account. It is now safe to close this browser window. + {{/*TODO: Remove this option to disconnect Disconnect*/}} +
+ + diff --git a/assets/twitter.png b/assets/twitter.png new file mode 100755 index 0000000000000000000000000000000000000000..af44ca5d502e7841fada40eeeefc200d151dc246 GIT binary patch literal 8862 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 NLq%J;LeV_*e*h(XLh%3q literal 0 HcmV?d00001 diff --git a/go.mod b/go.mod index 9966bd6..15e9d60 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,22 @@ -module github.com/mattermost/mattermost-plugin-starter-template +module github.com/mattermost/mattermost-plugin-twitter go 1.12 require ( + github.com/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d + github.com/dghubble/oauth1 v0.6.0 + github.com/google/go-cmp v0.5.1 // indirect + github.com/gorilla/mux v1.7.4 + github.com/mattermost/mattermost-plugin-api v0.0.12 github.com/mattermost/mattermost-server/v5 v5.26.2 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.6.1 + go.uber.org/atomic v1.6.0 + golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect + golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect + golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect + google.golang.org/grpc v1.31.0 // indirect + honnef.co/go/tools v0.0.1-2020.1.4 // indirect ) diff --git a/go.sum b/go.sum index 603bd8a..f23ec60 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,7 @@ git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqbl github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go v26.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-autorest v11.5.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/CloudyKit/fastprinter v0.0.0-20170127035650-74b38d55f37a/go.mod h1:EFZQ978U7x8IRnstaskI3IysnWY5Ao3QgZUKOXlsAdw= @@ -68,6 +69,8 @@ github.com/blevesearch/zap/v14 v14.0.0/go.mod h1:sUc/gPGJlFbSQ2ZUh/wGRYwkKx+Dg/5 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cenkalti/backoff v2.1.1+incompatible h1:tKJnvO2kl0zmb/jA5UKAt4VoEVw1qxKWjE/Bpp46npY= +github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -98,6 +101,12 @@ github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d h1:sBKr0A8iQ1qAOozedZ8Aox+Jpv+TeP1Qv7dcQyW8V+M= +github.com/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d/go.mod h1:xfg4uS5LEzOj8PgZV7SQYRHbG7jPUnelEiaAVJxmhJE= +github.com/dghubble/oauth1 v0.6.0 h1:m1yC01Ohc/eF38jwZ8JUjL1a+XHHXtGQgK+MxQbmSx0= +github.com/dghubble/oauth1 v0.6.0/go.mod h1:8pFdfPkv/jr8mkChVbNVuJ0suiHe278BtWI4Tk1ujxk= +github.com/dghubble/sling v1.3.0 h1:pZHjCJq4zJvc6qVQ5wN1jo5oNZlNE0+8T/h0XeXBUKU= +github.com/dghubble/sling v1.3.0/go.mod h1:XXShWaBWKzNLhu2OxikSNFrlsvowtz4kyRuXUG7oQKY= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/dgoogauth v0.0.0-20190221195224-5a805980a5f3/go.mod h1:hEfFauPHz7+NnjR/yHJGhrKo1Za+zStgwUETx3yzqgY= @@ -153,6 +162,7 @@ github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gorp/gorp v2.0.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= github.com/go-gorp/gorp v2.2.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -160,6 +170,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -175,6 +186,7 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er 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= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -204,6 +216,8 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -221,6 +235,7 @@ github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORR github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -307,6 +322,7 @@ github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo github.com/klauspost/cpuid v1.3.0/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -323,6 +339,7 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtB github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.7.0 h1:h93mCPfUSkaul3Ka/VG8uZdmW1uMHDGxzu0NWHuJmHY= github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -338,6 +355,9 @@ github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d h1:2DV7VIlEv6J5R5o github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ= github.com/mattermost/logr v1.0.5 h1:TST38xROPguNh8o90BfDHpp1bz6HfTdFYX5Btw/oLwM= github.com/mattermost/logr v1.0.5/go.mod h1:YzldchiJXgF789YNDFGXVoCHTQOTrCKwWft9Fwev1iI= +github.com/mattermost/mattermost-plugin-api v0.0.12 h1:k4AMBHZGKLZp8kLWia2JLGvlneNEgPbpsOGa11YfMyE= +github.com/mattermost/mattermost-plugin-api v0.0.12/go.mod h1:2P5T6ixjcDquVrhVdPZ1ASBWiilsxbdK6yYaqdKN/dI= +github.com/mattermost/mattermost-server/v5 v5.3.2-0.20200731154015-c5c6a5ce5399/go.mod h1:1udHoNFxLFYZuS9g6/NkJkNQniAcQYVqVEbDPHSumE0= github.com/mattermost/mattermost-server/v5 v5.26.2 h1:2QUO4cMxaGO3hD/+iytiWoK612taDf6A+g3C2yNvobE= github.com/mattermost/mattermost-server/v5 v5.26.2/go.mod h1:TVLwNQLSPNIkFOLoGHCGjZbSc2JEQf5PHUbQvneUSGM= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= @@ -397,6 +417,7 @@ github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/ngdinhtoan/glide-cleanup v0.2.0/go.mod h1:UQzsmiDOb8YV3nOsCxK/c9zPpCZVNoHScRE3EO9pVMM= +github.com/nicksnyder/go-i18n/v2 v2.0.3/go.mod h1:oDab7q8XCYMRlcrBnaY/7B1eOectbvj6B1UPBT+p5jo= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= @@ -413,6 +434,7 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/oov/psd v0.0.0-20200705094106-99303fb2511f/go.mod h1:GHI1bnmAcbp96z6LNfBJvtrjxhaXGkbsk967utPlvL8= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= @@ -468,6 +490,7 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/proullon/ramsql v0.0.0-20181213202341-817cee58a244/go.mod h1:jG8oAQG0ZPHPyxg5QlMERS31airDC+ZuqiAe8DUvFVo= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -511,6 +534,7 @@ github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5k github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= @@ -593,6 +617,7 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -622,6 +647,7 @@ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -676,11 +702,14 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -688,6 +717,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -704,6 +734,7 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -721,6 +752,9 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -743,6 +777,8 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190327201419-c70d86f8b7cf/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190424220101-1e8e1cfdf96b/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -755,10 +791,14 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f h1:JcoF/bowzCDI+MXu1yLqQGNO3ibqWsWq+Sk7pOT218w= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -771,6 +811,7 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -786,6 +827,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5 h1:a/Sqq5B3dGnmxhuJZIHFsIxhEkqElErr5TaU6IqBAj0= google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= @@ -800,6 +843,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -856,6 +901,10 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= willnorris.com/go/gifresize v1.0.0/go.mod h1:eBM8gogBGCcaH603vxSpnfjwXIpq6nmnj/jauBDKtAk= diff --git a/plugin.json b/plugin.json index b5b9a76..383e382 100644 --- a/plugin.json +++ b/plugin.json @@ -1,9 +1,9 @@ { - "id": "com.mattermost.plugin-starter-template", - "name": "Plugin Starter Template", - "description": "This plugin serves as a starting point for writing a Mattermost plugin.", + "id": "com.mattermost.twitter", + "name": "Twitter", + "description": "A Matermost plugin to connect to Twitter.", "version": "0.1.0", - "min_server_version": "5.12.0", + "min_server_version": "5.27.0", "server": { "executables": { "linux-amd64": "server/dist/plugin-linux-amd64", @@ -11,12 +11,28 @@ "windows-amd64": "server/dist/plugin-windows-amd64.exe" } }, - "webapp": { - "bundle_path": "webapp/dist/main.js" - }, "settings_schema": { "header": "", "footer": "", - "settings": [] + "settings": [ + { + "key": "OAuthClientID", + "display_name": "OAuth Client ID:", + "type": "text", + "help_text": "The client ID for the OAuth app registered with Twitter." + }, + { + "key": "OAuthClientSecret", + "display_name": "OAuth Client Secret:", + "type": "text", + "help_text": "The client secret for the OAuth app registered with Twitter." + }, + { + "key": "EncryptionKey", + "display_name": "At Rest Encryption Key", + "type": "generated", + "help_text": "The AES encryption key used to encrypt stored access tokens." + } + ] } } diff --git a/server/command/command.go b/server/command/command.go new file mode 100644 index 0000000..778b2e5 --- /dev/null +++ b/server/command/command.go @@ -0,0 +1,49 @@ +package command + +import ( + "strings" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin" + + "github.com/mattermost/mattermost-plugin-twitter/server/store" +) + +// Context includes the context in which the slash command is executed and allows access to +// plugin API, helpers and services +type Context struct { + *model.CommandArgs + context *plugin.Context + api plugin.API + helpers plugin.Helpers + manifest *model.Manifest + store *store.Store +} + +func NewContext(args *model.CommandArgs, context *plugin.Context, api plugin.API, helpers plugin.Helpers, manifest *model.Manifest, store *store.Store) *Context { + return &Context{ + args, + context, + api, + helpers, + manifest, + store, + } +} + +type HandlerFunc func(context *Context, args ...string) (*model.CommandResponse, *model.AppError) + +type Handler struct { + handlers map[string]HandlerFunc + defaultHandler HandlerFunc +} + +func (ch Handler) Handle(context *Context, args ...string) (*model.CommandResponse, *model.AppError) { + for n := len(args); n > 0; n-- { + h := ch.handlers[strings.Join(args[:n], "/")] + if h != nil { + return h(context, args[n:]...) + } + } + return ch.defaultHandler(context, args...) +} diff --git a/server/command/twitter.go b/server/command/twitter.go new file mode 100644 index 0000000..5295928 --- /dev/null +++ b/server/command/twitter.go @@ -0,0 +1,96 @@ +package command + +import ( + "fmt" + + "github.com/mattermost/mattermost-server/v5/model" + + "github.com/mattermost/mattermost-plugin-twitter/server/serializers" + "github.com/mattermost/mattermost-plugin-twitter/server/util" +) + +const ( + invalidCommand = "Invalid command parameters. Please use `/twitter help` for more information." + + helpText = "###### Twitter - Slash Command Help\n\n" + + "* `/twitter connect` - Connect to your twitter account.\n" + + "* `/twitter disconnect` - Disconnect your twitter account.\n" +) + +func GetCommand(iconData string) *model.Command { + return &model.Command{ + Trigger: "twitter", + DisplayName: "Twitter", + AutoComplete: true, + AutoCompleteDesc: "Available commands: connect, disconnect, help.", + AutoCompleteHint: "[command]", + AutocompleteData: getAutoCompleteData(), + AutocompleteIconData: iconData, + } +} + +func getAutoCompleteData() *model.AutocompleteData { + twitter := model.NewAutocompleteData("twitter", "[command]", "Available commands: connect, disconnect, help.") + + connect := model.NewAutocompleteData("connect", "", "Connect to your twitter account.") + twitter.AddCommand(connect) + + disconnect := model.NewAutocompleteData("disconnect", "", "Disconnect your twitter account.") + twitter.AddCommand(disconnect) + + help := model.NewAutocompleteData("help", "", "Show twitter slash command help") + twitter.AddCommand(help) + + return twitter +} + +var TwitterCommandHandler = Handler{ + handlers: map[string]HandlerFunc{ + "connect": twitterConnect, + "disconnect": twitterDisconnect, + "help": twitterHelpCommand, + }, + defaultHandler: func(context *Context, args ...string) (*model.CommandResponse, *model.AppError) { + return util.SendEphemeralCommandResponse(invalidCommand) + }, +} + +func twitterConnect(ctx *Context, args ...string) (*model.CommandResponse, *model.AppError) { + // If the user is already connected to twitter. + if twUser, err := ctx.store.GetTwitterUser(ctx.UserId); err == nil && twUser != nil { + return util.SendEphemeralCommandResponse(fmt.Sprintf("You are already connected as twitter user: %s.\nUse `/twitter disconnect` to disconnect your account.", twUser.Name+" (@"+twUser.Username+")")) + } + + twitterOAuth1Config := util.GetTwitterOAuth1Config(ctx.api, ctx.manifest) + token, secret, err := twitterOAuth1Config.RequestToken() + if err != nil { + ctx.api.LogError("Failed to connect.", "userID", ctx.UserId, "Error", err.Error()) + return util.SendEphemeralCommandResponse("Failed to connect to twitter. If the problem persists, contact your system administrator.") + } + + err = ctx.store.StoreOneTimeSecretJSON(ctx.UserId, &serializers.OAuth1aTemporaryCredentials{Token: token, Secret: secret}) + if err != nil { + ctx.api.LogError("Failed to connect.", "userID", ctx.UserId, "Error", err.Error()) + return util.SendEphemeralCommandResponse("Failed to connect to twitter. If the problem persists, contact your system administrator.") + } + + authURL, err := twitterOAuth1Config.AuthorizationURL(token) + if err != nil { + ctx.api.LogError("Failed to connect.", "userID", ctx.UserId, "Error", err.Error()) + return util.SendEphemeralCommandResponse("Failed to connect to twitter. If the problem persists, contact your system administrator.") + } + + return util.SendEphemeralCommandResponse(fmt.Sprintf("Click [here](%s) to connect to your twitter account.", authURL)) +} + +func twitterDisconnect(ctx *Context, args ...string) (*model.CommandResponse, *model.AppError) { + if err := ctx.store.DeleteTwitterUser(ctx.UserId); err != nil { + ctx.api.LogError("Failed to disconnect user.", "userID", ctx.UserId, "Error", err.Error()) + return util.SendEphemeralCommandResponse("Failed to disconnect your twitter account. If the problem persists, contact your system administrator.") + } + return util.SendEphemeralCommandResponse("Successfully disconnected from your twitter account.") +} + +func twitterHelpCommand(_ *Context, _ ...string) (*model.CommandResponse, *model.AppError) { + return util.SendEphemeralCommandResponse(helpText) +} diff --git a/server/config/main.go b/server/config/main.go new file mode 100644 index 0000000..4793fa8 --- /dev/null +++ b/server/config/main.go @@ -0,0 +1,70 @@ +package config + +import ( + "encoding/json" + + "github.com/pkg/errors" + "go.uber.org/atomic" +) + +var ( + config atomic.Value +) + +// Configuration captures the plugin's external configuration as exposed in the Mattermost server +// configuration, as well as values computed from the configuration. Any public fields will be +// deserialized from the Mattermost server configuration in OnConfigurationChange. +// +// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin +// configuration can change at any time, access to the configuration must be synchronized. The +// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire +// struct whenever it changes. You may replace this with whatever strategy you choose. +// +// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep +// copy appropriate for your types. +type Configuration struct { + OAuthClientID string + OAuthClientSecret string + EncryptionKey string +} + +// Clone shallow copies the configuration. Your implementation may require a deep copy if +// your configuration has reference types. +func (c *Configuration) Clone() *Configuration { + var clone = *c + return &clone +} + +// GetConfig retrieves the active configuration. +func GetConfig() *Configuration { + return config.Load().(*Configuration) +} + +// SetConfig replaces the active configuration. +func SetConfig(c *Configuration) { + config.Store(c) +} + +// IsValid checks if all needed fields are set. +func (c *Configuration) IsValid() error { + if c.OAuthClientID == "" { + return errors.New("must have a twitter oauth client id") + } + + if c.OAuthClientSecret == "" { + return errors.New("must have a twitter oauth client secret") + } + + if c.EncryptionKey == "" { + return errors.New("must have an encryption key") + } + + return nil +} + +func (c *Configuration) Serialize() map[string]interface{} { + out := make(map[string]interface{}) + b, _ := json.Marshal(c) + _ = json.Unmarshal(b, &out) + return out +} diff --git a/server/configuration.go b/server/configuration.go deleted file mode 100644 index 05aaf5b..0000000 --- a/server/configuration.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "reflect" - - "github.com/pkg/errors" -) - -// configuration captures the plugin's external configuration as exposed in the Mattermost server -// configuration, as well as values computed from the configuration. Any public fields will be -// deserialized from the Mattermost server configuration in OnConfigurationChange. -// -// As plugins are inherently concurrent (hooks being called asynchronously), and the plugin -// configuration can change at any time, access to the configuration must be synchronized. The -// strategy used in this plugin is to guard a pointer to the configuration, and clone the entire -// struct whenever it changes. You may replace this with whatever strategy you choose. -// -// If you add non-reference types to your configuration struct, be sure to rewrite Clone as a deep -// copy appropriate for your types. -type configuration struct { -} - -// Clone shallow copies the configuration. Your implementation may require a deep copy if -// your configuration has reference types. -func (c *configuration) Clone() *configuration { - var clone = *c - return &clone -} - -// getConfiguration retrieves the active configuration under lock, making it safe to use -// concurrently. The active configuration may change underneath the client of this method, but -// the struct returned by this API call is considered immutable. -func (p *Plugin) getConfiguration() *configuration { - p.configurationLock.RLock() - defer p.configurationLock.RUnlock() - - if p.configuration == nil { - return &configuration{} - } - - return p.configuration -} - -// setConfiguration replaces the active configuration under lock. -// -// Do not call setConfiguration while holding the configurationLock, as sync.Mutex is not -// reentrant. In particular, avoid using the plugin API entirely, as this may in turn trigger a -// hook back into the plugin. If that hook attempts to acquire this lock, a deadlock may occur. -// -// This method panics if setConfiguration is called with the existing configuration. This almost -// certainly means that the configuration was modified without being cloned and may result in -// an unsafe access. -func (p *Plugin) setConfiguration(configuration *configuration) { - p.configurationLock.Lock() - defer p.configurationLock.Unlock() - - if configuration != nil && p.configuration == configuration { - // Ignore assignment if the configuration struct is empty. Go will optimize the - // allocation for same to point at the same memory address, breaking the check - // above. - if reflect.ValueOf(*configuration).NumField() == 0 { - return - } - - panic("setConfiguration called with the existing configuration") - } - - p.configuration = configuration -} - -// OnConfigurationChange is invoked when configuration changes may have been made. -func (p *Plugin) OnConfigurationChange() error { - var configuration = new(configuration) - - // Load the public configuration fields from the Mattermost server configuration. - if err := p.API.LoadPluginConfiguration(configuration); err != nil { - return errors.Wrap(err, "failed to load plugin configuration") - } - - p.setConfiguration(configuration) - - return nil -} diff --git a/server/constant/constants.go b/server/constant/constants.go new file mode 100644 index 0000000..3bcbccb --- /dev/null +++ b/server/constant/constants.go @@ -0,0 +1,17 @@ +package constant + +const ( + // TODO: use manifest.id instead + PluginName = "com.mattermost.twitter" + + URLPluginBase = "/plugins/" + PluginName + URLStaticBase = URLPluginBase + "/static" + + HeaderMattermostUserID = "Mattermost-User-Id" + + BotUsername = "twitter" + BotDisplayName = "Twitter" + BotIconURL = URLStaticBase + "/twitter.png" + + PathTwitterOAuth1Callback = "/twitter/callback" +) diff --git a/server/controller/main.go b/server/controller/main.go new file mode 100644 index 0000000..c58a16a --- /dev/null +++ b/server/controller/main.go @@ -0,0 +1,87 @@ +package controller + +import ( + "net/http" + "path/filepath" + "runtime/debug" + + "github.com/gorilla/mux" + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin" + + "github.com/mattermost/mattermost-plugin-twitter/server/constant" + "github.com/mattermost/mattermost-plugin-twitter/server/store" +) + +type Controller struct { + api plugin.API + helpers plugin.Helpers + manifest *model.Manifest + store *store.Store +} + +func NewController(api plugin.API, helpers plugin.Helpers, manifest *model.Manifest, store *store.Store) *Controller { + return &Controller{ + api, + helpers, + manifest, + store, + } +} + +// InitAPI initializes the REST API +func (c *Controller) InitAPI() *mux.Router { + r := mux.NewRouter() + r.Use(c.withRecovery) + + c.handleStaticFiles(r) + s := r.PathPrefix("/api/v1").Subrouter() + + // Add the custom plugin routes here + s.HandleFunc(constant.PathTwitterOAuth1Callback, handleAuthRequired(c.twitterLoginCallback)).Methods(http.MethodGet) + + // 404 handler + r.Handle("{anything:.*}", http.NotFoundHandler()) + return r +} + +// From: https://github.com/mattermost/mattermost-plugin-github/blob/42185ff874963bed1efd8bc84c81462184d7cca8/server/plugin/api.go#L135 +func (c *Controller) withRecovery(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if x := recover(); x != nil { + c.api.LogError("Recovered from a panic", + "url", r.URL.String(), + "error", x, + "stack", string(debug.Stack())) + } + }() + + next.ServeHTTP(w, r) + }) +} + +// handleStaticFiles handles the static files under the assets directory. +func (c *Controller) handleStaticFiles(r *mux.Router) { + bundlePath, err := c.api.GetBundlePath() + if err != nil { + c.api.LogWarn("Failed to get bundle path.", "Error", err.Error()) + return + } + + // This will serve static files from the 'assets' directory under '/static/' + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(filepath.Join(bundlePath, "assets"))))) +} + +// handleAuthRequired verifies if provided request is performed by a logged-in Mattermost user. +func handleAuthRequired(handleFunc func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + userID := r.Header.Get(constant.HeaderMattermostUserID) + if userID == "" { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + handleFunc(w, r) + } +} diff --git a/server/controller/oauth1.go b/server/controller/oauth1.go new file mode 100644 index 0000000..035db5f --- /dev/null +++ b/server/controller/oauth1.go @@ -0,0 +1,91 @@ +package controller + +import ( + "fmt" + "net/http" + + "github.com/dghubble/go-twitter/twitter" + "github.com/dghubble/oauth1" + "github.com/mattermost/mattermost-server/v5/model" + + "github.com/mattermost/mattermost-plugin-twitter/server/serializers" + "github.com/mattermost/mattermost-plugin-twitter/server/util" +) + +func (c *Controller) twitterLoginCallback(w http.ResponseWriter, r *http.Request) { + requestToken, verifier, err := oauth1.ParseAuthorizationCallback(r) + if err != nil { + c.api.LogError("twitterLoginCallback: Failed to parse authorisation callback.", "Error", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + mmUserID := r.Header.Get("Mattermost-User-ID") + if mmUserID == "" { + c.api.LogError("twitterLoginCallback: Failed to get mattermost userID.") + http.Error(w, "not authorized", http.StatusUnauthorized) + return + } + + mmUser, appErr := c.api.GetUser(mmUserID) + if appErr != nil { + c.api.LogError("twitterLoginCallback: Failed to get mattermost user.", "Error", appErr.Error()) + http.Error(w, appErr.Error(), http.StatusInternalServerError) + return + } + + var oauthTmpCredentials serializers.OAuth1aTemporaryCredentials + if storeErr := c.store.LoadOneTimeSecretJSON(mmUserID, &oauthTmpCredentials); storeErr != nil || len(oauthTmpCredentials.Token) == 0 { + c.api.LogError(fmt.Sprintf("twitterLoginCallback: Failed to load oauth one-time secret. Error: %v", storeErr)) + http.Error(w, fmt.Sprintf("temporary credentials for %s not found or expired, try to connect again", mmUserID), http.StatusInternalServerError) + return + } + + if oauthTmpCredentials.Token != requestToken { + c.api.LogError("twitterLoginCallback: saved OAuth credentials and request token do not match.") + http.Error(w, "request token mismatch", http.StatusBadRequest) + return + } + + twitterOAuth1Config := util.GetTwitterOAuth1Config(c.api, c.manifest) + + // Twitter ignores the oauth_signature on the access token request. The user + // to which the request (temporary) token corresponds is already known on the + // server. The request for a request token earlier was validated signed by + // the consumer. Consumer applications can avoid keeping request token state + // between authorization granting and callback handling. + accessToken, accessSecret, err := twitterOAuth1Config.AccessToken(requestToken, "", verifier) + if err != nil { + c.api.LogError("twitterLoginCallback: Failed to get AccessToken from request.", "Error", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Twitter client + client := util.GetTwitterClient(accessToken, accessSecret) + twUser, resp, err := client.Accounts.VerifyCredentials(&twitter.AccountVerifyParams{}) + if err != nil { + c.api.LogError("twitterLoginCallback: Failed to verify twitter credentials for connected user.", "Error", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if resp != nil { + defer resp.Body.Close() + } + + if err := c.store.SaveTwitterUser(mmUserID, &serializers.TwitterUser{ + Name: twUser.Name, + Username: twUser.ScreenName, + AccessToken: accessToken, + AccessSecret: accessSecret, + }); err != nil { + c.api.LogError("twitterLoginCallback: Failed to save twitter client to KVStore.", "Error", err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + c.renderTemplate(w, "oauth1-complete.html", "text/html", map[string]string{ + "TwitterDisplayName": twUser.Name + " (@" + twUser.ScreenName + ")", + "MattermostDisplayName": mmUser.GetDisplayName(model.SHOW_NICKNAME_FULLNAME), + }) +} diff --git a/server/controller/utils.go b/server/controller/utils.go new file mode 100644 index 0000000..453720d --- /dev/null +++ b/server/controller/utils.go @@ -0,0 +1,31 @@ +package controller + +import ( + "html/template" + "net/http" + "path" + "path/filepath" +) + +func (c *Controller) renderTemplate(w http.ResponseWriter, templateName, contentType string, values interface{}) { + bundlePath, err := c.api.GetBundlePath() + if err != nil { + c.api.LogWarn("Failed to get bundle path.", "Error", err.Error()) + return + } + + templateDir := filepath.Join(bundlePath, "assets", "templates") + tmplPath := path.Join(templateDir, templateName) + + tmpl, err := template.ParseFiles(tmplPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", contentType) + if err = tmpl.Execute(w, values); err != nil { + http.Error(w, "failed to write response: "+err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/server/manifest.go b/server/manifest.go index 011a692..b4c3b69 100644 --- a/server/manifest.go +++ b/server/manifest.go @@ -12,11 +12,11 @@ var manifest *model.Manifest const manifestStr = ` { - "id": "com.mattermost.plugin-starter-template", - "name": "Plugin Starter Template", - "description": "This plugin serves as a starting point for writing a Mattermost plugin.", + "id": "com.mattermost.twitter", + "name": "Twitter", + "description": "A Matermost plugin to connect to Twitter.", "version": "0.1.0", - "min_server_version": "5.12.0", + "min_server_version": "5.27.0", "server": { "executables": { "linux-amd64": "server/dist/plugin-linux-amd64", @@ -25,13 +25,35 @@ const manifestStr = ` }, "executable": "" }, - "webapp": { - "bundle_path": "webapp/dist/main.js" - }, "settings_schema": { "header": "", "footer": "", - "settings": [] + "settings": [ + { + "key": "OAuthClientID", + "display_name": "OAuth Client ID:", + "type": "text", + "help_text": "The client ID for the OAuth app registered with Twitter.", + "placeholder": "", + "default": "WkEzuWVPGHSmO4vm0yvdF8bq4" + }, + { + "key": "OAuthClientSecret", + "display_name": "OAuth Client Secret:", + "type": "text", + "help_text": "The client secret for the OAuth app registered with Twitter.", + "placeholder": "", + "default": "LwAgBVckO0EQlI2zka55ZFAN16xMRY8T0zvzVI4KbVlCK6tBN1" + }, + { + "key": "EncryptionKey", + "display_name": "At Rest Encryption Key", + "type": "generated", + "help_text": "The AES encryption key used to encrypt stored access tokens.", + "placeholder": "", + "default": null + } + ] } } ` diff --git a/server/plugin.go b/server/plugin.go index 29f249a..f7de2f5 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -3,26 +3,114 @@ package main import ( "fmt" "net/http" - "sync" + "runtime/debug" + "strings" + "github.com/gorilla/mux" + cmd2 "github.com/mattermost/mattermost-plugin-api/experimental/command" + "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin" + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-plugin-twitter/server/command" + "github.com/mattermost/mattermost-plugin-twitter/server/config" + "github.com/mattermost/mattermost-plugin-twitter/server/controller" + "github.com/mattermost/mattermost-plugin-twitter/server/store" + "github.com/mattermost/mattermost-plugin-twitter/server/util" ) // Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes. +// See https://developers.mattermost.com/extend/plugins/server/reference/ type Plugin struct { plugin.MattermostPlugin - // configurationLock synchronizes access to the configuration. - configurationLock sync.RWMutex + router *mux.Router + store *store.Store +} + +func (p *Plugin) OnActivate() error { + if err := p.registerCommand(); err != nil { + p.API.LogError(err.Error()) + return err + } - // configuration is the active plugin configuration. Consult getConfiguration and - // setConfiguration for usage. - configuration *configuration + p.store = store.NewStore(p.API, p.Helpers) + p.router = controller.NewController(p.API, p.Helpers, manifest, p.store).InitAPI() + return nil } -// ServeHTTP demonstrates a plugin that handles HTTP requests by greeting the world. -func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "Hello, world!") +// OnConfigurationChange is invoked when configuration changes may have been made. +func (p *Plugin) OnConfigurationChange() error { + var configuration config.Configuration + + // Load the public configuration fields from the Mattermost server configuration. + if err := p.API.LoadPluginConfiguration(&configuration); err != nil { + return errors.Wrap(err, "failed to load plugin configuration") + } + + if err := configuration.IsValid(); err != nil { + return errors.Wrap(err, "failed to validate plugin configuration") + } + + config.SetConfig(&configuration) + return nil } -// See https://developers.mattermost.com/extend/plugins/server/reference/ +func (p *Plugin) registerCommand() error { + iconData, err := cmd2.GetIconData(p.API, "assets/logo.svg") + if err != nil { + return errors.Wrap(err, "failed to get icon data") + } + + cmd := command.GetCommand(iconData) + if err := p.API.RegisterCommand(cmd); err != nil { + return errors.Wrap(err, "failed to register slash command: "+cmd.Trigger) + } + + return nil +} + +func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { + defer func() { + if x := recover(); x != nil { + p.API.LogError("Recovered from a panic while executing slash command.", + "commandArgs", fmt.Sprintf("%v", args), + "error", x, + "stack", string(debug.Stack())) + } + }() + + split, argErr := util.SplitArgs(args.Command) + if argErr != nil { + return util.SendEphemeralCommandResponse(argErr.Error()) + } + + cmdName := split[0][1:] + var params []string + + if len(split) > 1 { + params = split[1:] + } + + cmd := command.GetCommand("") + if cmd.Trigger != cmdName { + return util.SendEphemeralCommandResponse("Unknown command: [" + cmdName + "] encountered") + } + + p.API.LogDebug("Executing command: " + cmdName + " with params: [" + strings.Join(params, ", ") + "]") + cmdContext := command.NewContext(args, c, p.API, p.Helpers, manifest, p.store) + return command.TwitterCommandHandler.Handle(cmdContext, params...) +} + +// ServeHTTP handles HTTP requests for the plugin. +func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) { + p.API.LogDebug("New request:", "Host", r.Host, "RequestURI", r.RequestURI, "Method", r.Method) + + if err := config.GetConfig().IsValid(); err != nil { + p.API.LogError("This plugin is not configured.", "Error", err.Error()) + http.Error(w, "This plugin is not configured.", http.StatusNotImplemented) + return + } + + p.router.ServeHTTP(w, r) +} diff --git a/server/serializers/oauth1.go b/server/serializers/oauth1.go new file mode 100644 index 0000000..2abb26d --- /dev/null +++ b/server/serializers/oauth1.go @@ -0,0 +1,6 @@ +package serializers + +type OAuth1aTemporaryCredentials struct { + Token string + Secret string +} diff --git a/server/serializers/twitter-user.go b/server/serializers/twitter-user.go new file mode 100644 index 0000000..e8e548e --- /dev/null +++ b/server/serializers/twitter-user.go @@ -0,0 +1,8 @@ +package serializers + +type TwitterUser struct { + Name string + Username string + AccessToken string + AccessSecret string +} diff --git a/server/store/main.go b/server/store/main.go new file mode 100644 index 0000000..ea44b94 --- /dev/null +++ b/server/store/main.go @@ -0,0 +1,67 @@ +// Package store ... +// Loosely adapted from: https://github.com/mattermost/mattermost-plugin-mscalendar/blob/7d80765da31ac3483354b99197f099d805c3b4a9/server/utils/kvstore/plugin_store.go +package store + +import ( + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin" + + "github.com/pkg/errors" +) + +var ErrNotFound = errors.New("not found") + +type Store struct { + api plugin.API + helpers plugin.Helpers +} + +func NewStore(api plugin.API, helpers plugin.Helpers) *Store { + return &Store{ + api, + helpers, + } +} + +func (s Store) Load(key string) ([]byte, error) { + data, appErr := s.api.KVGet(key) + if appErr != nil { + return nil, errors.WithMessage(appErr, "failed plugin KVGet") + } + if data == nil { + return nil, ErrNotFound + } + return data, nil +} + +func (s Store) Store(key string, data []byte) error { + var appErr *model.AppError + if appErr = s.api.KVSet(key, data); appErr != nil { + return errors.WithMessagef(appErr, "failed plugin KVSet %q", key) + } + return nil +} + +func (s Store) StoreTTL(key string, data []byte, ttlSeconds int64) error { + appErr := s.api.KVSetWithExpiry(key, data, ttlSeconds) + if appErr != nil { + return errors.WithMessagef(appErr, "failed plugin KVSet (ttl: %vs) %q", ttlSeconds, key) + } + return nil +} + +func (s Store) StoreWithOptions(key string, value []byte, opts model.PluginKVSetOptions) (bool, error) { + success, appErr := s.api.KVSetWithOptions(key, value, opts) + if appErr != nil { + return false, errors.WithMessagef(appErr, "failed plugin KVSet (ttl: %vs) %q", opts.ExpireInSeconds, key) + } + return success, nil +} + +func (s Store) Delete(key string) error { + appErr := s.api.KVDelete(key) + if appErr != nil { + return errors.WithMessagef(appErr, "failed plugin KVdelete %q", key) + } + return nil +} diff --git a/server/store/ots.go b/server/store/ots.go new file mode 100644 index 0000000..123740b --- /dev/null +++ b/server/store/ots.go @@ -0,0 +1,54 @@ +// Package store ... +// Loosely adapted from: https://github.com/mattermost/mattermost-plugin-mscalendar/blob/7d80765da31ac3483354b99197f099d805c3b4a9/server/utils/kvstore/ots.go +package store + +import ( + "encoding/json" + + "github.com/mattermost/mattermost-plugin-twitter/server/util" +) + +const ( + prefixOneTimeSecret = "ots_" // + unique key that will be deleted after the first verification + + // Expire in 15 minutes + otsExpiration = 15 * 60 +) + +func (s *Store) StoreOneTimeSecret(token, secret string) error { + return s.StoreTTL(util.HashKey(prefixOneTimeSecret, token), []byte(secret), otsExpiration) +} + +func (s *Store) LoadOneTimeSecret(key string) (data []byte, returnErr error) { + data, err := s.Load(util.HashKey(prefixOneTimeSecret, key)) + if len(data) != 0 { + _ = s.Delete(util.HashKey(prefixOneTimeSecret, key)) + } + return data, err +} + +func (s *Store) StoreOneTimeSecretJSON(token string, v interface{}) error { + data, err := json.Marshal(v) + if err != nil { + return err + } + return s.StoreTTL(util.HashKey(prefixOneTimeSecret, token), data, otsExpiration) +} + +func (s *Store) LoadOneTimeSecretJSON(key string, v interface{}) (returnErr error) { + data, err := s.Load(util.HashKey(prefixOneTimeSecret, key)) + if err != nil { + return err + } + + // If the key expired, appErr is nil, but the data is also nil + if len(data) == 0 { + return ErrNotFound + + // TODO: Remove this + // nil, errors.Wrapf(kvstore.ErrNotFound, "temporary credentials for %s not found or expired, try to connect again"+mmUserId) + } + + _ = s.Delete(util.HashKey(prefixOneTimeSecret, key)) + return json.Unmarshal(data, v) +} diff --git a/server/store/twitter-user.go b/server/store/twitter-user.go new file mode 100644 index 0000000..350879d --- /dev/null +++ b/server/store/twitter-user.go @@ -0,0 +1,27 @@ +package store + +import ( + "github.com/mattermost/mattermost-plugin-twitter/server/serializers" + "github.com/mattermost/mattermost-plugin-twitter/server/util" +) + +const ( + twitterUserPrefix = "twitter-user-" +) + +func (s Store) SaveTwitterUser(mmUserID string, user *serializers.TwitterUser) error { + return s.StoreJSON(util.HashKey(twitterUserPrefix, mmUserID), user) +} + +func (s Store) GetTwitterUser(mmUserID string) (*serializers.TwitterUser, error) { + var user serializers.TwitterUser + err := s.LoadJSON(util.HashKey(twitterUserPrefix, mmUserID), &user) + if err != nil { + return nil, err + } + return &user, nil +} + +func (s Store) DeleteTwitterUser(mmUserID string) error { + return s.Delete(util.HashKey(twitterUserPrefix, mmUserID)) +} diff --git a/server/store/utils.go b/server/store/utils.go new file mode 100644 index 0000000..313a2b2 --- /dev/null +++ b/server/store/utils.go @@ -0,0 +1,116 @@ +// Package store ... +// Loosely adapted from: https://github.com/mattermost/mattermost-plugin-mscalendar/blob/7d80765da31ac3483354b99197f099d805c3b4a9/server/utils/kvstore/kvstore.go +package store + +import ( + "bytes" + "encoding/json" + "time" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/pkg/errors" +) + +const ( + atomicRetryLimit = 5 + atomicRetryWait = 30 * time.Millisecond +) + +// Ensure makes sure the initial value for a key is set to the value provided, if it does not already exists +// Returns the value set for the key in kv-store and error +func (s *Store) Ensure(key string, newValue []byte) ([]byte, error) { + value, err := s.Load(key) + switch err { + case nil: + return value, nil + case ErrNotFound: + break + default: + return nil, err + } + + err = s.Store(key, newValue) + if err != nil { + return nil, err + } + + // Load again in case we lost the race to another server + value, err = s.Load(key) + if err != nil { + return newValue, nil + } + return value, nil +} + +// LoadJSON loads a json value stored in the KVStore using StoreJSON +// unmarshalling it to an interface using json.Unmarshal +func (s *Store) LoadJSON(key string, v interface{}) (returnErr error) { + data, err := s.Load(key) + if err != nil { + return err + } + return json.Unmarshal(data, v) +} + +// StoreJSON stores a json value from an interface to KVStore after marshaling it using json.Marshal +func (s *Store) StoreJSON(key string, v interface{}) (returnErr error) { + data, err := json.Marshal(v) + if err != nil { + return err + } + return s.Store(key, data) +} + +// AtomicModifyWithOptions modifies the value for a key in KVStore, only if the initial value is not changed while attempting to modify it. +// To avoid race conditions, we retry the modification multiple times after waiting for a fixed wait interval. +// input: kv store key and a modify function which takes initial value and returns final value with PluginKVSetOptions and error. +// returns: nil for a successful update and error if the update is unsuccessful or the retry limit reached. +func (s *Store) AtomicModifyWithOptions(key string, modify func(initialValue []byte) ([]byte, *model.PluginKVSetOptions, error)) error { + currentAttempt := 0 + for { + initialBytes, appErr := s.Load(key) + if appErr != nil && appErr != ErrNotFound { + return errors.Wrap(appErr, "unable to read initial value") + } + + newValue, opts, err := modify(initialBytes) + if err != nil { + return errors.Wrap(err, "modification error") + } + + // No modifications have been done. No reason to hit the plugin API. + if bytes.Equal(initialBytes, newValue) { + return nil + } + + if opts == nil { + opts = &model.PluginKVSetOptions{} + } + opts.Atomic = true + opts.OldValue = initialBytes + success, setError := s.StoreWithOptions(key, newValue, *opts) + if setError != nil { + return errors.Wrap(setError, "problem writing value") + } + if success { + return nil + } + + currentAttempt++ + if currentAttempt >= atomicRetryLimit { + return errors.New("reached write attempt limit") + } + + time.Sleep(atomicRetryWait) + } +} + +// AtomicModify calls AtomicModifyWithOptions with nil PluginKVSetOptions +// to atomically modify a value in KVStore and set it for an indefinite time +// See AtomicModifyWithOptions for more info +func (s *Store) AtomicModify(key string, modify func(initialValue []byte) ([]byte, error)) error { + return s.AtomicModifyWithOptions(key, func(initialValue []byte) ([]byte, *model.PluginKVSetOptions, error) { + b, err := modify(initialValue) + return b, nil, err + }) +} diff --git a/server/util/command.go b/server/util/command.go new file mode 100644 index 0000000..aeb3350 --- /dev/null +++ b/server/util/command.go @@ -0,0 +1,77 @@ +package util + +import ( + "regexp" + "strings" + + "github.com/mattermost/mattermost-server/v5/model" + "github.com/pkg/errors" + + "github.com/mattermost/mattermost-plugin-twitter/server/constant" +) + +// Min - since math.Min is for floats and casting to and from floats is dangerous. +func Min(a, b int) int { + if a < b { + return a + } + return b +} + +// SplitArgs is used to split a string to an array of arguments with separators: "(quotes) and spaces +// We cant use strings.split as it includes empty string if deliminator is the last character in input string +func SplitArgs(s string) ([]string, error) { + indexes := regexp.MustCompile("\"").FindAllStringIndex(s, -1) + if len(indexes)%2 != 0 { + return []string{}, errors.New("quotes not closed") + } + + indexes = append([][]int{{0, 0}}, indexes...) + + if indexes[len(indexes)-1][1] < len(s) { + indexes = append(indexes, [][]int{{len(s), 0}}...) + } + + var args []string + for i := 0; i < len(indexes)-1; i++ { + start := indexes[i][1] + end := Min(len(s), indexes[i+1][0]) + + if i%2 == 0 { + args = append(args, strings.Split(strings.Trim(s[start:end], " "), " ")...) + } else { + args = append(args, s[start:end]) + } + } + + cleanedArgs := make([]string, len(args)) + count := 0 + + for _, arg := range args { + if arg != "" { + cleanedArgs[count] = strings.TrimSpace(arg) + count++ + } + } + + return cleanedArgs[0:count], nil +} + +// SendEphemeralCommandResponse can be used to return an ephemeral message as the response for a slash command +func SendEphemeralCommandResponse(message string) (*model.CommandResponse, *model.AppError) { + return &model.CommandResponse{ + Type: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, + Text: message, + Username: constant.BotUsername, + IconURL: constant.BotIconURL, + }, nil +} + +// BaseCommandResponse returns the base slash command response to post in channel +func BaseCommandResponse() *model.CommandResponse { + return &model.CommandResponse{ + Type: model.COMMAND_RESPONSE_TYPE_IN_CHANNEL, + Username: constant.BotUsername, + IconURL: constant.BotIconURL, + } +} diff --git a/server/util/hash.go b/server/util/hash.go new file mode 100644 index 0000000..d8c0a91 --- /dev/null +++ b/server/util/hash.go @@ -0,0 +1,19 @@ +package util + +import ( + "crypto/md5" + "fmt" +) + +// HashKey returns the kvstore kev by appending prefix with the hash of the input key +// From: https://github.com/mattermost/mattermost-plugin-mscalendar/blob/26fe3c5ea965a435e76dfc5b23e7f66fa9e9b592/server/utils/kvstore/hashed_key.go#L47 +// TODO: use a more secure hash primitive +func HashKey(prefix, key string) string { + if key == "" { + return prefix + } + + h := md5.New() + _, _ = h.Write([]byte(key)) + return fmt.Sprintf("%s%x", prefix, h.Sum(nil)) +} diff --git a/server/util/post.go b/server/util/post.go new file mode 100644 index 0000000..4a23ca1 --- /dev/null +++ b/server/util/post.go @@ -0,0 +1,20 @@ +package util + +import ( + "github.com/mattermost/mattermost-server/v5/model" + + "github.com/mattermost/mattermost-plugin-twitter/server/constant" +) + +func EphemeralPost(channelID, message string) *model.Post { + post := &model.Post{ + ChannelId: channelID, + Message: message, + } + post.SetProps(model.StringInterface{ + "from_webhook": "true", + "override_username": constant.BotUsername, + "override_icon_url": constant.BotIconURL, + }) + return post +} diff --git a/server/util/utils.go b/server/util/utils.go new file mode 100644 index 0000000..c2ddd1e --- /dev/null +++ b/server/util/utils.go @@ -0,0 +1,51 @@ +package util + +import ( + "strings" + + "github.com/dghubble/go-twitter/twitter" + "github.com/dghubble/oauth1" + twAuth "github.com/dghubble/oauth1/twitter" + "github.com/mattermost/mattermost-server/v5/model" + "github.com/mattermost/mattermost-server/v5/plugin" + + "github.com/mattermost/mattermost-plugin-twitter/server/config" + "github.com/mattermost/mattermost-plugin-twitter/server/constant" +) + +func GetSiteURL(api plugin.API) string { + return *api.GetConfig().ServiceSettings.SiteURL +} + +func GetPluginURLPath(manifest *model.Manifest) string { + return "/plugins/" + manifest.Id +} + +func GetPluginURL(api plugin.API, manifest *model.Manifest) string { + return strings.TrimRight(GetSiteURL(api), "/") + GetPluginURLPath(manifest) +} + +func GetPluginAPIURL(api plugin.API, manifest *model.Manifest) string { + return GetPluginURL(api, manifest) + "/api/v1" +} + +func GetTwitterOAuth1Config(api plugin.API, manifest *model.Manifest) oauth1.Config { + conf := config.GetConfig() + + return oauth1.Config{ + ConsumerKey: conf.OAuthClientID, + ConsumerSecret: conf.OAuthClientSecret, + CallbackURL: GetPluginAPIURL(api, manifest) + constant.PathTwitterOAuth1Callback, + Endpoint: twAuth.AuthorizeEndpoint, + } +} + +func GetTwitterClient(accessToken, accessSecret string) *twitter.Client { + conf := config.GetConfig() + oauth1Config := oauth1.NewConfig(conf.OAuthClientID, conf.OAuthClientSecret) + token := oauth1.NewToken(accessToken, accessSecret) + httpClient := oauth1Config.Client(oauth1.NoContext, token) + + // Twitter client + return twitter.NewClient(httpClient) +} From 9d2e325bbfdc02b8209c765d18e89a5cfdd23923 Mon Sep 17 00:00:00 2001 From: Chetanya Kandhari Date: Wed, 28 Oct 2020 22:22:25 +0530 Subject: [PATCH 2/8] Clean up assets and update logs --- assets/logo.svg | 6 ++++- assets/templates/oauth1-complete.html | 38 ++------------------------- plugin.json | 3 +++ server/command/twitter.go | 6 ++--- 4 files changed, 13 insertions(+), 40 deletions(-) diff --git a/assets/logo.svg b/assets/logo.svg index 2832e7b..0cd7667 100755 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1 +1,5 @@ -Twitter_Logo_Blue \ No newline at end of file + + + + + diff --git a/assets/templates/oauth1-complete.html b/assets/templates/oauth1-complete.html index 3850c5e..1c6b42b 100644 --- a/assets/templates/oauth1-complete.html +++ b/assets/templates/oauth1-complete.html @@ -11,50 +11,17 @@ padding: 50px; } - .btn { - -webkit-transition: all 0.15s ease; - -webkit-transition-delay: 0s; - -moz-transition: all 0.15s ease; - -o-transition: all 0.15s ease; - transition: all 0.15s ease; - padding-right: 1em; - padding-left: 1em; - font-size: inherit; - border: none; - height: 2.4em; - border-radius: 4px; - cursor: pointer; - } - - .btn-primary { - color: rgb(255, 255, 255); - background: rgb(0, 82, 204); - } - - .btn-primary:hover, - .btn-primary:active { - background: rgb(0, 101, 255); - } - - .btn-link { - color: #505f79; - background: #f4f5f7; - } - - .btn-link:hover, - .btn-link:active { - background: #ebecf0; - } - .accounts-container { padding: 1.6em 0 0.8em; opacity: .6; } + .icon--check { margin-right: 4px; } + mattermost-plugin-twitter
@@ -69,7 +36,6 @@

Twitter account: {{ .TwitterDisplayName }}

You can use `/twitter disconnect` to disconnect your account. It is now safe to close this browser window. - {{/*TODO: Remove this option to disconnect Disconnect*/}} diff --git a/plugin.json b/plugin.json index 383e382..5a36937 100644 --- a/plugin.json +++ b/plugin.json @@ -11,6 +11,9 @@ "windows-amd64": "server/dist/plugin-windows-amd64.exe" } }, + "webapp": { + "bundle_path": "webapp/dist/main.js" + }, "settings_schema": { "header": "", "footer": "", diff --git a/server/command/twitter.go b/server/command/twitter.go index 5295928..9c3d28d 100644 --- a/server/command/twitter.go +++ b/server/command/twitter.go @@ -64,19 +64,19 @@ func twitterConnect(ctx *Context, args ...string) (*model.CommandResponse, *mode twitterOAuth1Config := util.GetTwitterOAuth1Config(ctx.api, ctx.manifest) token, secret, err := twitterOAuth1Config.RequestToken() if err != nil { - ctx.api.LogError("Failed to connect.", "userID", ctx.UserId, "Error", err.Error()) + ctx.api.LogError("Failed to connect to twitter. Unable to obtain Request token and secret (temporary credentials).", "userID", ctx.UserId, "Error", err.Error()) return util.SendEphemeralCommandResponse("Failed to connect to twitter. If the problem persists, contact your system administrator.") } err = ctx.store.StoreOneTimeSecretJSON(ctx.UserId, &serializers.OAuth1aTemporaryCredentials{Token: token, Secret: secret}) if err != nil { - ctx.api.LogError("Failed to connect.", "userID", ctx.UserId, "Error", err.Error()) + ctx.api.LogError("Failed to connect to twitter. Unable to store temporary credentials to KVStore.", "userID", ctx.UserId, "Error", err.Error()) return util.SendEphemeralCommandResponse("Failed to connect to twitter. If the problem persists, contact your system administrator.") } authURL, err := twitterOAuth1Config.AuthorizationURL(token) if err != nil { - ctx.api.LogError("Failed to connect.", "userID", ctx.UserId, "Error", err.Error()) + ctx.api.LogError("Failed to connect to twitter. Unable to obtain authorization URL.", "userID", ctx.UserId, "Error", err.Error()) return util.SendEphemeralCommandResponse("Failed to connect to twitter. If the problem persists, contact your system administrator.") } From b83c175e3c4fb1fb72a7f707188ff933eb6ff681 Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Tue, 10 Nov 2020 16:36:18 +0100 Subject: [PATCH 3/8] go mod tidy --- go.sum | 1 + 1 file changed, 1 insertion(+) diff --git a/go.sum b/go.sum index f23ec60..7fbb6d8 100644 --- a/go.sum +++ b/go.sum @@ -219,6 +219,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= From 1ec099db8bd7ff11ceacbc5934c3964ded7afcdf Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Tue, 10 Nov 2020 16:39:18 +0100 Subject: [PATCH 4/8] Trigger CI --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 826b555..f3a48a4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ https://developer.twitter.com/en/portal/projects//apps//auth - Set the Website URL to `your-mattermost-url`. - ## Development To avoid having to manually install your plugin, build and deploy your plugin using one of the following options. From 81688452a59bfa46388395912038e3f6d7ad41f1 Mon Sep 17 00:00:00 2001 From: Ben Schumacher Date: Tue, 10 Nov 2020 16:42:33 +0100 Subject: [PATCH 5/8] For real now --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index f3a48a4..835c1f2 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ https://developer.twitter.com/en/portal/projects//apps//auth - Set the callbackURL to `/plugins/com.mattermost.twitter/twitter/callback`. - Set the Website URL to `your-mattermost-url`. - ## Development To avoid having to manually install your plugin, build and deploy your plugin using one of the following options. From 8458f469c99337fe08b6c885d6db018d81d55474 Mon Sep 17 00:00:00 2001 From: Chetanya Kandhari Date: Tue, 10 Nov 2020 23:25:57 +0530 Subject: [PATCH 6/8] Feedback changes --- README.md | 31 +--------------------------- go.mod | 9 -------- go.sum | 18 ---------------- plugin.json | 6 ------ server/{controller => api}/main.go | 8 +++++-- server/{controller => api}/oauth1.go | 16 +++++++------- server/{controller => api}/utils.go | 2 +- server/command/twitter.go | 3 +-- server/constant/constants.go | 2 -- server/plugin.go | 4 ++-- server/plugin_test.go | 28 ------------------------- server/serializers/twitter-user.go | 8 +++++++ server/store/ots.go | 3 --- server/store/twitter-user.go | 3 +++ 14 files changed, 31 insertions(+), 110 deletions(-) rename server/{controller => api}/main.go (95%) rename server/{controller => api}/oauth1.go (90%) rename server/{controller => api}/utils.go (97%) delete mode 100644 server/plugin_test.go diff --git a/README.md b/README.md index 835c1f2..8aff89d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Plugin Starter Template [![CircleCI branch](https://img.shields.io/circleci/project/github/mattermost/mattermost-plugin-starter-template/master.svg)](https://circleci.com/gh/mattermost/mattermost-plugin-starter-template) +# Plugin Starter Template [![CircleCI branch](https://img.shields.io/circleci/project/github/mattermost/mattermost-plugin-twitter/master.svg)](https://circleci.com/gh/mattermost/mattermost-plugin-starter-template) A Mattermost plugin to connect to twitter. @@ -87,32 +87,3 @@ export MM_SERVICESETTINGS_SITEURL=http://localhost:8065 export MM_ADMIN_TOKEN=j44acwd8obn78cdcx7koid4jkr make deploy ``` - -## Q&A - -### How do I make a server-only or web app-only plugin? - -Simply delete the `server` or `webapp` folders and remove the corresponding sections from `plugin.json`. The build scripts will skip the missing portions automatically. - -### How do I include assets in the plugin bundle? - -Place them into the `assets` directory. To use an asset at runtime, build the path to your asset and open as a regular file: - -```go -bundlePath, err := p.API.GetBundlePath() -if err != nil { - return errors.Wrap(err, "failed to get bundle path") -} - -profileImage, err := ioutil.ReadFile(filepath.Join(bundlePath, "assets", "profile_image.png")) -if err != nil { - return errors.Wrap(err, "failed to read profile image") -} - -if appErr := p.API.SetProfileImage(userID, profileImage); appErr != nil { - return errors.Wrap(err, "failed to set profile image") -} -``` - -### How do I build the plugin with unminified JavaScript? -Setting the `MM_DEBUG` environment variable will invoke the debug builds. The simplist way to do this is to simply include this variable in your calls to `make` (e.g. `make dist MM_DEBUG=1`). diff --git a/go.mod b/go.mod index 15e9d60..267e404 100644 --- a/go.mod +++ b/go.mod @@ -5,18 +5,9 @@ go 1.12 require ( github.com/dghubble/go-twitter v0.0.0-20201011215211-4b180d0cc78d github.com/dghubble/oauth1 v0.6.0 - github.com/google/go-cmp v0.5.1 // indirect github.com/gorilla/mux v1.7.4 github.com/mattermost/mattermost-plugin-api v0.0.12 github.com/mattermost/mattermost-server/v5 v5.26.2 github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.6.1 go.uber.org/atomic v1.6.0 - golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect - golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 // indirect - golang.org/x/tools v0.0.0-20200825202427-b303f430e36d // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 // indirect - google.golang.org/grpc v1.31.0 // indirect - honnef.co/go/tools v0.0.1-2020.1.4 // indirect ) diff --git a/go.sum b/go.sum index 7fbb6d8..83e4727 100644 --- a/go.sum +++ b/go.sum @@ -216,8 +216,6 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -618,7 +616,6 @@ github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDf github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -703,8 +700,6 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -718,7 +713,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -753,8 +747,6 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -792,14 +784,10 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f h1:JcoF/bowzCDI+MXu1yLqQGNO3ibqWsWq+Sk7pOT218w= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181220000619-583d854617af/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -828,8 +816,6 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5 h1:a/Sqq5B3dGnmxhuJZIHFsIxhEkqElErr5TaU6IqBAj0= google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987 h1:PDIOdWxZ8eRizhKa1AAvY53xsvLB1cWorMjslvY3VA8= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= @@ -844,8 +830,6 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -902,8 +886,6 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= diff --git a/plugin.json b/plugin.json index 5a36937..f997517 100644 --- a/plugin.json +++ b/plugin.json @@ -29,12 +29,6 @@ "display_name": "OAuth Client Secret:", "type": "text", "help_text": "The client secret for the OAuth app registered with Twitter." - }, - { - "key": "EncryptionKey", - "display_name": "At Rest Encryption Key", - "type": "generated", - "help_text": "The AES encryption key used to encrypt stored access tokens." } ] } diff --git a/server/controller/main.go b/server/api/main.go similarity index 95% rename from server/controller/main.go rename to server/api/main.go index c58a16a..e5862d3 100644 --- a/server/controller/main.go +++ b/server/api/main.go @@ -1,4 +1,4 @@ -package controller +package api import ( "net/http" @@ -13,6 +13,10 @@ import ( "github.com/mattermost/mattermost-plugin-twitter/server/store" ) +const ( + HeaderMattermostUserID = "Mattermost-User-Id" +) + type Controller struct { api plugin.API helpers plugin.Helpers @@ -76,7 +80,7 @@ func (c *Controller) handleStaticFiles(r *mux.Router) { // handleAuthRequired verifies if provided request is performed by a logged-in Mattermost user. func handleAuthRequired(handleFunc func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - userID := r.Header.Get(constant.HeaderMattermostUserID) + userID := r.Header.Get(HeaderMattermostUserID) if userID == "" { http.Error(w, "Unauthorized", http.StatusUnauthorized) return diff --git a/server/controller/oauth1.go b/server/api/oauth1.go similarity index 90% rename from server/controller/oauth1.go rename to server/api/oauth1.go index 035db5f..d9822e0 100644 --- a/server/controller/oauth1.go +++ b/server/api/oauth1.go @@ -1,4 +1,4 @@ -package controller +package api import ( "fmt" @@ -63,7 +63,7 @@ func (c *Controller) twitterLoginCallback(w http.ResponseWriter, r *http.Request // Twitter client client := util.GetTwitterClient(accessToken, accessSecret) - twUser, resp, err := client.Accounts.VerifyCredentials(&twitter.AccountVerifyParams{}) + twAccount, resp, err := client.Accounts.VerifyCredentials(&twitter.AccountVerifyParams{}) if err != nil { c.api.LogError("twitterLoginCallback: Failed to verify twitter credentials for connected user.", "Error", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -73,19 +73,21 @@ func (c *Controller) twitterLoginCallback(w http.ResponseWriter, r *http.Request defer resp.Body.Close() } - if err := c.store.SaveTwitterUser(mmUserID, &serializers.TwitterUser{ - Name: twUser.Name, - Username: twUser.ScreenName, + twUser := &serializers.TwitterUser{ + Name: twAccount.Name, + Username: twAccount.ScreenName, AccessToken: accessToken, AccessSecret: accessSecret, - }); err != nil { + } + + if err := c.store.SaveTwitterUser(mmUserID, twUser); err != nil { c.api.LogError("twitterLoginCallback: Failed to save twitter client to KVStore.", "Error", err.Error()) http.Error(w, err.Error(), http.StatusInternalServerError) return } c.renderTemplate(w, "oauth1-complete.html", "text/html", map[string]string{ - "TwitterDisplayName": twUser.Name + " (@" + twUser.ScreenName + ")", + "TwitterDisplayName": twUser.GetDisplayName(), "MattermostDisplayName": mmUser.GetDisplayName(model.SHOW_NICKNAME_FULLNAME), }) } diff --git a/server/controller/utils.go b/server/api/utils.go similarity index 97% rename from server/controller/utils.go rename to server/api/utils.go index 453720d..4bf18cf 100644 --- a/server/controller/utils.go +++ b/server/api/utils.go @@ -1,4 +1,4 @@ -package controller +package api import ( "html/template" diff --git a/server/command/twitter.go b/server/command/twitter.go index 9c3d28d..b6d423e 100644 --- a/server/command/twitter.go +++ b/server/command/twitter.go @@ -20,7 +20,6 @@ const ( func GetCommand(iconData string) *model.Command { return &model.Command{ Trigger: "twitter", - DisplayName: "Twitter", AutoComplete: true, AutoCompleteDesc: "Available commands: connect, disconnect, help.", AutoCompleteHint: "[command]", @@ -58,7 +57,7 @@ var TwitterCommandHandler = Handler{ func twitterConnect(ctx *Context, args ...string) (*model.CommandResponse, *model.AppError) { // If the user is already connected to twitter. if twUser, err := ctx.store.GetTwitterUser(ctx.UserId); err == nil && twUser != nil { - return util.SendEphemeralCommandResponse(fmt.Sprintf("You are already connected as twitter user: %s.\nUse `/twitter disconnect` to disconnect your account.", twUser.Name+" (@"+twUser.Username+")")) + return util.SendEphemeralCommandResponse(fmt.Sprintf("You are already connected as twitter user: %s.\nUse `/twitter disconnect` to disconnect your account.", twUser.GetDisplayName())) } twitterOAuth1Config := util.GetTwitterOAuth1Config(ctx.api, ctx.manifest) diff --git a/server/constant/constants.go b/server/constant/constants.go index 3bcbccb..96d9d59 100644 --- a/server/constant/constants.go +++ b/server/constant/constants.go @@ -7,8 +7,6 @@ const ( URLPluginBase = "/plugins/" + PluginName URLStaticBase = URLPluginBase + "/static" - HeaderMattermostUserID = "Mattermost-User-Id" - BotUsername = "twitter" BotDisplayName = "Twitter" BotIconURL = URLStaticBase + "/twitter.png" diff --git a/server/plugin.go b/server/plugin.go index f7de2f5..1fb63bd 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -12,9 +12,9 @@ import ( "github.com/mattermost/mattermost-server/v5/plugin" "github.com/pkg/errors" + "github.com/mattermost/mattermost-plugin-twitter/server/api" "github.com/mattermost/mattermost-plugin-twitter/server/command" "github.com/mattermost/mattermost-plugin-twitter/server/config" - "github.com/mattermost/mattermost-plugin-twitter/server/controller" "github.com/mattermost/mattermost-plugin-twitter/server/store" "github.com/mattermost/mattermost-plugin-twitter/server/util" ) @@ -35,7 +35,7 @@ func (p *Plugin) OnActivate() error { } p.store = store.NewStore(p.API, p.Helpers) - p.router = controller.NewController(p.API, p.Helpers, manifest, p.store).InitAPI() + p.router = api.NewController(p.API, p.Helpers, manifest, p.store).InitAPI() return nil } diff --git a/server/plugin_test.go b/server/plugin_test.go deleted file mode 100644 index 1d3a474..0000000 --- a/server/plugin_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestServeHTTP(t *testing.T) { - assert := assert.New(t) - plugin := Plugin{} - w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/", nil) - - plugin.ServeHTTP(nil, w, r) - - result := w.Result() - assert.NotNil(result) - defer result.Body.Close() - bodyBytes, err := ioutil.ReadAll(result.Body) - assert.Nil(err) - bodyString := string(bodyBytes) - - assert.Equal("Hello, world!", bodyString) -} diff --git a/server/serializers/twitter-user.go b/server/serializers/twitter-user.go index e8e548e..88b765c 100644 --- a/server/serializers/twitter-user.go +++ b/server/serializers/twitter-user.go @@ -1,8 +1,16 @@ package serializers +import ( + "fmt" +) + type TwitterUser struct { Name string Username string AccessToken string AccessSecret string } + +func (u *TwitterUser) GetDisplayName() string { + return fmt.Sprintf("%s (@%s)", u.Name, u.Username) +} diff --git a/server/store/ots.go b/server/store/ots.go index 123740b..dbb23d1 100644 --- a/server/store/ots.go +++ b/server/store/ots.go @@ -44,9 +44,6 @@ func (s *Store) LoadOneTimeSecretJSON(key string, v interface{}) (returnErr erro // If the key expired, appErr is nil, but the data is also nil if len(data) == 0 { return ErrNotFound - - // TODO: Remove this - // nil, errors.Wrapf(kvstore.ErrNotFound, "temporary credentials for %s not found or expired, try to connect again"+mmUserId) } _ = s.Delete(util.HashKey(prefixOneTimeSecret, key)) diff --git a/server/store/twitter-user.go b/server/store/twitter-user.go index 350879d..1830de3 100644 --- a/server/store/twitter-user.go +++ b/server/store/twitter-user.go @@ -17,6 +17,9 @@ func (s Store) GetTwitterUser(mmUserID string) (*serializers.TwitterUser, error) var user serializers.TwitterUser err := s.LoadJSON(util.HashKey(twitterUserPrefix, mmUserID), &user) if err != nil { + if err != ErrNotFound { + s.api.LogError("Failed to get connected twitter user.", "userID", mmUserID, "error", err.Error()) + } return nil, err } return &user, nil From 7d9bfcc56cfbf36deb94180037be07f2710e4267 Mon Sep 17 00:00:00 2001 From: Chetanya Kandhari Date: Sat, 17 Jul 2021 17:59:05 +0530 Subject: [PATCH 7/8] Update .gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index a6671c8..16f09f6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,3 @@ dist/ # Jetbrains .idea/ - -vendor From d9e22c4bb9de2a2a1693b2753c7e576d9c681171 Mon Sep 17 00:00:00 2001 From: Chetanya Kandhari Date: Sat, 17 Jul 2021 17:59:44 +0530 Subject: [PATCH 8/8] Update plugin.json --- plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.json b/plugin.json index f997517..aa41006 100644 --- a/plugin.json +++ b/plugin.json @@ -3,7 +3,7 @@ "name": "Twitter", "description": "A Matermost plugin to connect to Twitter.", "version": "0.1.0", - "min_server_version": "5.27.0", + "min_server_version": "5.12.0", "server": { "executables": { "linux-amd64": "server/dist/plugin-linux-amd64",