added command with interactive dialog
Some checks failed
ci / plugin-ci (push) Has been cancelled

This commit is contained in:
Felipe M 2024-08-08 15:52:29 +02:00
parent 09c4f13f2c
commit 2d962d18d2
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8
16 changed files with 520 additions and 89 deletions

View file

@ -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

View file

@ -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

View file

@ -1,54 +1,8 @@
# 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-attachments-remover/master.svg)](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
View file

@ -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
View file

@ -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=

View file

@ -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
View 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
View 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
View 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 ""
}

View file

@ -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
View 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
View 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
View 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
View 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
}
}

View file

@ -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;
},
);
}
}

View file

@ -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
}