This commit is contained in:
parent
09c4f13f2c
commit
2d962d18d2
16 changed files with 520 additions and 89 deletions
|
@ -6,9 +6,8 @@ linters-settings:
|
|||
gofmt:
|
||||
simplify: true
|
||||
goimports:
|
||||
local-prefixes: github.com/mattermost/mattermost-starter-template
|
||||
local-prefixes: github.com/mattermost/mattermost-plugin-attachments-remover
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
|
|
2
Makefile
2
Makefile
|
@ -164,7 +164,7 @@ apply:
|
|||
## Install go tools
|
||||
install-go-tools:
|
||||
@echo Installing go tools
|
||||
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.1
|
||||
$(GO) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.59.1
|
||||
$(GO) install gotest.tools/gotestsum@v1.7.0
|
||||
|
||||
## Runs eslint and golangci-lint
|
||||
|
|
76
README.md
76
README.md
|
@ -1,54 +1,8 @@
|
|||
# Plugin Starter Template [](https://circleci.com/gh/mattermost/mattermost-plugin-starter-template)
|
||||
# Plugin Starter Template [](https://circleci.com/gh/mattermost/mattermost-plugin-attachments-remover)
|
||||
|
||||
This plugin serves as a starting point for writing a Mattermost plugin. Feel free to base your own plugin off this repository.
|
||||
## Features
|
||||
|
||||
To learn more about plugins, see [our plugin documentation](https://developers.mattermost.com/extend/plugins/).
|
||||
|
||||
This template requires node v16 and npm v8. You can download and install nvm to manage your node versions by following the instructions [here](https://github.com/nvm-sh/nvm). Once you've setup the project simply run `nvm i` within the root folder to use the suggested version of node.
|
||||
|
||||
## 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 the following files:
|
||||
1. `plugin.json` with your `id`, `name`, and `description`:
|
||||
```json
|
||||
{
|
||||
"id": "com.example.my-plugin",
|
||||
"name": "My Plugin",
|
||||
"description": "A plugin to enhance Mattermost."
|
||||
}
|
||||
```
|
||||
|
||||
2. `go.mod` with your Go module path, following the `<hosting-site>/<repository>/<module>` convention:
|
||||
```
|
||||
module github.com/example/my-plugin
|
||||
```
|
||||
|
||||
3. `.golangci.yml` with your Go module path:
|
||||
```yml
|
||||
linters-settings:
|
||||
# [...]
|
||||
goimports:
|
||||
local-prefixes: github.com/example/my-plugin
|
||||
```
|
||||
|
||||
Build your plugin:
|
||||
```
|
||||
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
|
||||
```
|
||||
- Allows users and sysadmins to delete attachments from posts via a context menu option
|
||||
|
||||
## Development
|
||||
|
||||
|
@ -161,29 +115,5 @@ To trigger a release, follow these steps:
|
|||
|
||||
## 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`).
|
||||
|
|
11
go.mod
11
go.mod
|
@ -1,27 +1,33 @@
|
|||
module github.com/mattermost/mattermost-plugin-starter-template
|
||||
module github.com/mattermost/mattermost-plugin-attachments-remover
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/jmoiron/sqlx v1.4.0
|
||||
github.com/mattermost/mattermost/server/public v0.0.14
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.8.4
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.2 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.0 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
|
||||
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
|
||||
|
@ -35,6 +41,7 @@ require (
|
|||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tinylib/msgp v1.1.9 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
|
|
24
go.sum
24
go.sum
|
@ -6,8 +6,12 @@ dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl
|
|||
dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
|
||||
dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
|
@ -36,8 +40,8 @@ github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aev
|
|||
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
|
@ -63,6 +67,8 @@ github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
|
|||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
|
@ -76,6 +82,8 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg
|
|||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
|
@ -86,6 +94,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
|
@ -107,6 +119,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k
|
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
|
@ -160,11 +174,16 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1l
|
|||
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
|
||||
github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE=
|
||||
github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
|
||||
github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
|
@ -225,6 +244,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
|
|
10
plugin.json
10
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.",
|
||||
"homepage_url": "https://github.com/mattermost/mattermost-plugin-starter-template",
|
||||
"support_url": "https://github.com/mattermost/mattermost-plugin-starter-template/issues",
|
||||
"id": "com.mattermost.attachments-remover",
|
||||
"name": "Attachments manager",
|
||||
"description": "This plugin allows you more ways to manage attachments in Mattermost.",
|
||||
"homepage_url": "https://github.com/mattermost/mattermost-plugin-attachments-remover",
|
||||
"support_url": "https://github.com/mattermost/mattermost-plugin-attachments-remover/issues",
|
||||
"icon_path": "assets/starter-template-icon.svg",
|
||||
"min_server_version": "6.2.1",
|
||||
"server": {
|
||||
|
|
93
server/api.go
Normal file
93
server/api.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
plugin *Plugin
|
||||
router *mux.Router
|
||||
}
|
||||
|
||||
func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
a.router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (a *API) handlerRemoveAttachments(w http.ResponseWriter, r *http.Request) {
|
||||
// Get post_id from the query parameters
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
postID := r.FormValue("post_id")
|
||||
|
||||
// Check if the user is the post author or a system admin
|
||||
userID := r.Header.Get("Mattermost-User-ID")
|
||||
post, appErr := a.plugin.API.GetPost(postID)
|
||||
if appErr != nil {
|
||||
http.Error(w, appErr.Error(), appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
if errReason := a.plugin.userHasRemovePermissionsToPost(userID, post.ChannelId, postID); errReason != "" {
|
||||
http.Error(w, errReason, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Soft-delete the attachments
|
||||
for _, fileID := range post.FileIds {
|
||||
if err := a.plugin.SQLStore.DetatchAttachmentFromChannel(fileID); err != nil {
|
||||
a.plugin.API.LogError("error detaching attachment from channel", "err", err)
|
||||
http.Error(w, "Internal server error, check logs", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
originalFileIDs := post.FileIds
|
||||
|
||||
// Edit the post to remove the attachments
|
||||
post.FileIds = []string{}
|
||||
newPost, appErr := a.plugin.API.UpdatePost(post)
|
||||
if appErr != nil {
|
||||
http.Error(w, appErr.Error(), appErr.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Attach the original file IDs to the original post so history is not lost
|
||||
if err := a.plugin.SQLStore.AttachFileIDsToPost(newPost.OriginalId, originalFileIDs); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// setupAPI sets up the API for the plugin.
|
||||
func setupAPI(plugin *Plugin) (*API, error) {
|
||||
api := &API{
|
||||
plugin: plugin,
|
||||
router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
group := api.router.PathPrefix("/api/v1").Subrouter()
|
||||
group.Use(authorizationRequiredMiddleware)
|
||||
group.HandleFunc("/remove_attachments", api.handlerRemoveAttachments)
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func authorizationRequiredMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userID := r.Header.Get("Mattermost-User-ID")
|
||||
if userID != "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Not authorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
66
server/commands.go
Normal file
66
server/commands.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
)
|
||||
|
||||
func (p *Plugin) getCommand() *model.Command {
|
||||
return &model.Command{
|
||||
Trigger: "removeattachments",
|
||||
DisplayName: "Remove Attachments",
|
||||
Description: "Remove all attachments from a post",
|
||||
AutoComplete: false, // Hide from autocomplete
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) createCommandResponse(message string) *model.CommandResponse {
|
||||
return &model.CommandResponse{
|
||||
Text: message,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) createErrorCommandResponse(errorMessage string) *model.CommandResponse {
|
||||
return &model.CommandResponse{
|
||||
Text: "Can't remove attachments: " + errorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
|
||||
commandSplit := strings.Split(args.Command, " ")
|
||||
|
||||
// Do not provide command output since it's going to be triggered from the frontend
|
||||
if len(commandSplit) != 2 {
|
||||
return p.createErrorCommandResponse("Invalid number of arguments, use `/removeattachments [postID]`."), nil
|
||||
}
|
||||
|
||||
postID := commandSplit[1]
|
||||
|
||||
// Check if the post ID is a valid ID
|
||||
if !model.IsValidId(postID) {
|
||||
return p.createErrorCommandResponse("Invalid post ID"), nil
|
||||
}
|
||||
|
||||
// Check if the user has permissions to remove attachments from the post
|
||||
if errReason := p.userHasRemovePermissionsToPost(args.UserId, args.ChannelId, postID); errReason != "" {
|
||||
return p.createErrorCommandResponse(errReason), nil
|
||||
}
|
||||
|
||||
// Create an interactive dialog to confirm the action
|
||||
if err := p.API.OpenInteractiveDialog(model.OpenDialogRequest{
|
||||
TriggerId: args.TriggerId,
|
||||
URL: "/plugins/" + manifest.Id + "/api/v1/remove_attachments?post_id=" + postID,
|
||||
Dialog: model.Dialog{
|
||||
Title: "Remove Attachments",
|
||||
IntroductionText: "Are you sure you want to remove all attachments from this post?",
|
||||
SubmitLabel: "Remove",
|
||||
},
|
||||
}); err != nil {
|
||||
return p.createCommandResponse(err.Error()), nil
|
||||
}
|
||||
|
||||
// Return nothing, let the dialog/api handle the response
|
||||
return &model.CommandResponse{}, nil
|
||||
}
|
37
server/permissions.go
Normal file
37
server/permissions.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import "github.com/mattermost/mattermost/server/public/model"
|
||||
|
||||
// userHasRemovePermissionsToPost checks if the user has permissions to remove attachments from a post
|
||||
// based on the post ID, the user ID, and the channel ID.
|
||||
// Returns an error message if the user does not have permissions, or an empty string if the user has permissions.
|
||||
func (p *Plugin) userHasRemovePermissionsToPost(userID, channelID, postID string) string {
|
||||
// Check if the post exists
|
||||
post, appErr := p.API.GetPost(postID)
|
||||
if appErr != nil {
|
||||
return "Post does not exist"
|
||||
}
|
||||
|
||||
// Check if the post has attachments
|
||||
if len(post.FileIds) == 0 {
|
||||
return "Post has no attachments"
|
||||
}
|
||||
|
||||
// Check if the user is the post author or has permissions to edit others posts
|
||||
user, appErr := p.API.GetUser(userID)
|
||||
if appErr != nil {
|
||||
return "Internal error, check with your system administrator for assistance"
|
||||
}
|
||||
|
||||
if post.UserId != user.Id && !p.API.HasPermissionToChannel(userID, channelID, model.PermissionEditOthersPosts) {
|
||||
return "Not authorized"
|
||||
}
|
||||
|
||||
// Check if the post is editable at this point in time
|
||||
config := p.API.GetConfig()
|
||||
if config.ServiceSettings.PostEditTimeLimit != nil && *config.ServiceSettings.PostEditTimeLimit > 0 && model.GetMillis() > post.CreateAt+int64(*config.ServiceSettings.PostEditTimeLimit*1000) {
|
||||
return "Post is too old to edit"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
|
@ -6,6 +6,9 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/plugin"
|
||||
"github.com/mattermost/mattermost/server/public/pluginapi"
|
||||
|
||||
"github.com/mattermost/mattermost-plugin-attachments-remover/server/sqlstore"
|
||||
)
|
||||
|
||||
// Plugin implements the interface expected by the Mattermost server to communicate between the server and plugin processes.
|
||||
|
@ -18,11 +21,39 @@ type Plugin struct {
|
|||
// configuration is the active plugin configuration. Consult getConfiguration and
|
||||
// setConfiguration for usage.
|
||||
configuration *configuration
|
||||
|
||||
// api is the instance of the API struct that handles the plugin's API endpoints.
|
||||
api *API
|
||||
|
||||
// sqlStore is the instance of the SQLStore struct that handles the plugin's database interactions.
|
||||
SQLStore *sqlstore.SQLStore
|
||||
}
|
||||
|
||||
// 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!")
|
||||
p.api.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// See https://developers.mattermost.com/extend/plugins/server/reference/
|
||||
func (p *Plugin) OnActivate() error {
|
||||
var err error
|
||||
p.api, err = setupAPI(p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error setting up the API: %w", err)
|
||||
}
|
||||
|
||||
// Setup direct SQL Store access via the plugin api
|
||||
papi := pluginapi.NewClient(p.API, p.Driver)
|
||||
SQLStore, err := sqlstore.New(papi.Store, &papi.Log)
|
||||
if err != nil {
|
||||
p.API.LogError("cannot create SQLStore", "err", err)
|
||||
return err
|
||||
}
|
||||
p.SQLStore = SQLStore
|
||||
|
||||
// Register command
|
||||
if err := p.API.RegisterCommand(p.getCommand()); err != nil {
|
||||
p.API.LogError("failed to register command", "err", err)
|
||||
return fmt.Errorf("failed to register command: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
53
server/sqlstore/file.go
Normal file
53
server/sqlstore/file.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
const (
|
||||
fileInfoTable = "FileInfo"
|
||||
)
|
||||
|
||||
func (s *SQLStore) DetatchAttachmentFromChannel(fileID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
update := map[string]interface{}{
|
||||
"ChannelId": "",
|
||||
"PostId": "",
|
||||
"DeleteAt": model.GetMillis(),
|
||||
}
|
||||
|
||||
query := s.masterBuilder.
|
||||
Update(fileInfoTable).
|
||||
SetMap(update).
|
||||
Where(sq.Eq{"Id": fileID})
|
||||
|
||||
tx, err := s.master.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error starting transaction: %w", err)
|
||||
}
|
||||
|
||||
q, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating query: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
s.logger.Error("error detaching attachment from channel", "fileId", fileID, "err", err)
|
||||
return tx.Rollback()
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("error committing transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
51
server/sqlstore/post.go
Normal file
51
server/sqlstore/post.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package sqlstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
const (
|
||||
postsTable = "Posts"
|
||||
)
|
||||
|
||||
func (s *SQLStore) AttachFileIDsToPost(postID string, fileIDs []string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
fileIDsJSON, err := json.Marshal(fileIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling fileIds: %w", err)
|
||||
}
|
||||
|
||||
query := s.masterBuilder.
|
||||
Update(postsTable).
|
||||
Set("FileIds", fileIDsJSON).
|
||||
Where(sq.Eq{"Id": postID})
|
||||
|
||||
tx, err := s.master.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error starting transaction: %w", err)
|
||||
}
|
||||
|
||||
q, args, err := query.ToSql()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating query: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, q, args...)
|
||||
if err != nil {
|
||||
s.logger.Error("error attaching file to post", "postId", postID, "err", err)
|
||||
return tx.Rollback()
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("error committing transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
79
server/sqlstore/store.go
Normal file
79
server/sqlstore/store.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package sqlstore
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
type Source interface {
|
||||
GetMasterDB() (*sql.DB, error)
|
||||
GetReplicaDB() (*sql.DB, error)
|
||||
DriverName() string
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Error(message string, keyValuePairs ...interface{})
|
||||
Warn(message string, keyValuePairs ...interface{})
|
||||
Info(message string, keyValuePairs ...interface{})
|
||||
Debug(message string, keyValuePairs ...interface{})
|
||||
}
|
||||
|
||||
type SQLStore struct {
|
||||
src Source
|
||||
master *sqlx.DB
|
||||
replica *sqlx.DB
|
||||
masterBuilder sq.StatementBuilderType
|
||||
replicaBuilder sq.StatementBuilderType
|
||||
logger Logger
|
||||
}
|
||||
|
||||
// New constructs a new instance of SQLStore.
|
||||
func New(src Source, logger Logger) (*SQLStore, error) {
|
||||
var master, replica *sqlx.DB
|
||||
|
||||
masterDB, err := src.GetMasterDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
master = sqlx.NewDb(masterDB, src.DriverName())
|
||||
|
||||
replicaDB, err := src.GetReplicaDB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
replica = sqlx.NewDb(replicaDB, src.DriverName())
|
||||
|
||||
masterBuilder := sq.StatementBuilder.PlaceholderFormat(sq.Question)
|
||||
if src.DriverName() == model.DatabaseDriverPostgres {
|
||||
masterBuilder = masterBuilder.PlaceholderFormat(sq.Dollar)
|
||||
}
|
||||
|
||||
if src.DriverName() == model.DatabaseDriverMysql {
|
||||
master.MapperFunc(func(s string) string { return s })
|
||||
}
|
||||
|
||||
masterBuilder = masterBuilder.RunWith(master)
|
||||
|
||||
replicaBuilder := sq.StatementBuilder.PlaceholderFormat(sq.Question)
|
||||
if src.DriverName() == model.DatabaseDriverPostgres {
|
||||
replicaBuilder = replicaBuilder.PlaceholderFormat(sq.Dollar)
|
||||
}
|
||||
|
||||
if src.DriverName() == model.DatabaseDriverMysql {
|
||||
replica.MapperFunc(func(s string) string { return s })
|
||||
}
|
||||
|
||||
replicaBuilder = replicaBuilder.RunWith(replica)
|
||||
|
||||
return &SQLStore{
|
||||
src,
|
||||
master,
|
||||
replica,
|
||||
masterBuilder,
|
||||
replicaBuilder,
|
||||
logger,
|
||||
}, nil
|
||||
}
|
45
webapp/src/actions.tsx
Normal file
45
webapp/src/actions.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import {AnyAction, Dispatch} from 'redux';
|
||||
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {GetStateFunc} from 'mattermost-redux/types/actions';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {IntegrationTypes} from 'mattermost-redux/action_types';
|
||||
|
||||
export function setTriggerId(triggerId: string) {
|
||||
return {
|
||||
type: IntegrationTypes.RECEIVED_DIALOG_TRIGGER_ID,
|
||||
data: triggerId,
|
||||
};
|
||||
}
|
||||
|
||||
export function triggerRemoveAttachmentsCommand(postID: string) {
|
||||
return (dispatch: Dispatch<AnyAction>, getState: GetStateFunc) => {
|
||||
const command = '/removeattachments ' + postID;
|
||||
clientExecuteCommand(dispatch, getState, command);
|
||||
|
||||
return {data: true};
|
||||
};
|
||||
}
|
||||
|
||||
export async function clientExecuteCommand(dispatch: Dispatch<AnyAction>, getState: GetStateFunc, command: string) {
|
||||
let currentChannel = getCurrentChannel(getState());
|
||||
const currentTeamId = getCurrentTeamId(getState());
|
||||
|
||||
// Default to town square if there is no current channel (i.e., if Mattermost has not yet loaded)
|
||||
if (!currentChannel) {
|
||||
currentChannel = await Client4.getChannelByName(currentTeamId, 'town-square');
|
||||
}
|
||||
|
||||
const args = {
|
||||
channel_id: currentChannel?.id,
|
||||
team_id: currentTeamId,
|
||||
};
|
||||
|
||||
try {
|
||||
//@ts-ignore Typing in mattermost-redux is wrong
|
||||
const data = await Client4.executeCommand(command, args);
|
||||
dispatch(setTriggerId(data?.trigger_id));
|
||||
} catch (error) {
|
||||
console.error(error); //eslint-disable-line no-console
|
||||
}
|
||||
}
|
|
@ -1,15 +1,31 @@
|
|||
import {Store, Action} from 'redux';
|
||||
|
||||
import {GlobalState} from '@mattermost/types/lib/store';
|
||||
import {GlobalState} from 'mattermost-redux/types/store';
|
||||
import {getPost} from 'mattermost-redux/selectors/entities/posts';
|
||||
|
||||
import manifest from '@/manifest';
|
||||
|
||||
import {PluginRegistry} from '@/types/mattermost-webapp';
|
||||
|
||||
import {triggerRemoveAttachmentsCommand} from './actions';
|
||||
|
||||
export default class Plugin {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||
public async initialize(registry: PluginRegistry, store: Store<GlobalState, Action<Record<string, unknown>>>) {
|
||||
// @see https://developers.mattermost.com/extend/plugins/webapp/reference/
|
||||
registry.registerPostDropdownMenuAction(
|
||||
'Remove attachments',
|
||||
async (postID) => {
|
||||
store.dispatch(triggerRemoveAttachmentsCommand(postID) as any);
|
||||
},
|
||||
(postID) => {
|
||||
const state = store.getState();
|
||||
const post = getPost(state, postID);
|
||||
|
||||
// Don't show up if the post has no attachments. Permissions are checked server-side.
|
||||
return typeof post.file_ids?.length !== 'undefined' && post.file_ids?.length > 0;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
type PostMenuAction = (postID: string) => void;
|
||||
type PostMenuFilter = (postID: string) => boolean;
|
||||
|
||||
export interface PluginRegistry {
|
||||
registerPostTypeComponent(typeName: string, component: React.ElementType)
|
||||
registerPostDropdownMenuAction(text: string, action?: PostMenuAction, filter?: PostMenuFilter)
|
||||
|
||||
// Add more if needed from https://developers.mattermost.com/extend/plugins/webapp/reference
|
||||
}
|
||||
|
|
Reference in a new issue