diff --git a/.golangci.yml b/.golangci.yml index 8b75d38..93bc566 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,7 +9,7 @@ linters-settings: gofmt: simplify: true goimports: - local-prefixes: github.com/nathanaelhoun/mattermost-plugin-postmanager + local-prefixes: github.com/nathanaelhoun/mattermost-plugin-clear golint: min-confidence: 0 govet: diff --git a/Makefile b/Makefile index cf12b20..2cf0c6c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ GO ?= $(shell command -v go 2> /dev/null) NPM ?= $(shell command -v npm 2> /dev/null) CURL ?= $(shell command -v curl 2> /dev/null) -DEBUG ?=1 +DEBUG ?= MANIFEST_FILE ?= plugin.json GOPATH ?= $(shell go env GOPATH) GO_TEST_FLAGS ?= -race diff --git a/README.md b/README.md index 1aa01f8..1c15027 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,19 @@ [![CircleCI branch](https://img.shields.io/circleci/project/github/nathanaelhoun/mattermost-plugin-clear/master.svg)](https://circleci.com/gh/nathanaelhoun/mattermost-plugin-clear) -This [mattermost](https://mattermost.org) plugin allow to delete post with a command. +This [Mattermost](https://mattermost.org) plugin allow to delete posts with a /command. ![Plugin screenshot](./screenshot.png) -**Supported Mattermost Server Versions: 5.2+** (command autocomplete with Mattermost 5.24+) +**Supported Mattermost Server Versions: 5.12+** (command autocomplete with Mattermost 5.24+) ## Features -#### Manage posts with commands - `/clear [number-of-post]` Delete the last `[number-of-post]` posts in the current channel -Available options : - _incoming feature_ +### Available options : +* `--delete-pinned-posts` Also delete pinned post (disabled by default) +* `--confirm` Do not show confirmation dialog ## Installation diff --git a/go.mod b/go.mod index d9a21be..900a948 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,6 @@ module github.com/mattermost/mattermost-plugin-starter-template go 1.12 require ( - github.com/mattermost/mattermost-server/v5 v5.24.0-rc3 + github.com/mattermost/mattermost-server/v5 v5.24.1 github.com/pkg/errors v0.9.1 ) diff --git a/go.sum b/go.sum index 8855ebe..b6847e2 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,7 @@ github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2o github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/squirrel v1.2.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PaulARoy/azurestoragecache v0.0.0-20170906084534-3c249a3ba788/go.mod h1:lY1dZd8HBzJ10eqKERHn3CU59tfhzcAVb2c0ZhIWSOk= github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= @@ -279,8 +280,8 @@ github.com/mattermost/gorp v2.0.1-0.20190301154413-3b31e9a39d05+incompatible/go. github.com/mattermost/gosaml2 v0.3.2/go.mod h1:Z429EIOiEi9kbq6yHoApfzlcXpa6dzRDc6pO+Vy2Ksk= github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d h1:2DV7VIlEv6J5R5o6tUcb3ZMKJYeeZuWZL7Rv1m23TgQ= github.com/mattermost/ldap v0.0.0-20191128190019-9f62ba4b8d4d/go.mod h1:HLbgMEI5K131jpxGazJ97AxfPDt31osq36YS1oxFQPQ= -github.com/mattermost/mattermost-server/v5 v5.24.0-rc3 h1:+fQNocygVkOnh0LV+x97qWxKd/Ibydk8ju2PKx1PSXk= -github.com/mattermost/mattermost-server/v5 v5.24.0-rc3/go.mod h1:wmIfEohk+D0xbFFfJPNvvUYaxeqUNKQcPZejT73m9YM= +github.com/mattermost/mattermost-server/v5 v5.24.1 h1:dkEDqjLTtgqlQTc03kEi7N2IgBT9cZmzLs6XPylpMUo= +github.com/mattermost/mattermost-server/v5 v5.24.1/go.mod h1:TVkOfVyk4wGw8j5J2IX3PDCP5R7j20IEP4FAezDK8Wk= github.com/mattermost/rsc v0.0.0-20160330161541-bbaefb05eaa0/go.mod h1:nV5bfVpT//+B1RPD2JvRnxbkLmJEYXmRaaVl15fsXjs= github.com/mattermost/viper v1.0.4/go.mod h1:uc5hKG9lv4/KRwPOt2c1omOyirS/UnuA2TytiZQSFHM= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -312,7 +313,6 @@ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mkraft/gziphandler v1.1.2-0.20200509175700-73dc64f3ad90/go.mod h1:gG8WEPb2aI5MHdmHv83au7bk3molRSZiAjdxYrEMJdQ= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= diff --git a/plugin.json b/plugin.json index 826215b..2a8470e 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "id": "com.github.nathanaelhoun.plugin-clear", "name": "Clear", - "description": "Delete multiple posts with a command.", + "description": "Delete multiple posts with a /command.", "version": "0.2.0", "min_server_version": "5.12.0", "server": { @@ -10,10 +10,5 @@ "darwin-amd64": "server/dist/plugin-darwin-amd64", "windows-amd64": "server/dist/plugin-windows-amd64.exe" } - }, - "settings_schema": { - "header": "", - "footer": "* To report an issue, make a suggestion or a contribution, [check the repository](https://github.com/nathanaelhoun/mattermost-plugin-clear).", - "settings": [] } } \ No newline at end of file diff --git a/screenshot.png b/screenshot.png index d417b76..58451c1 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/server/command.go b/server/command.go index 7a8f0bd..8129f6f 100644 --- a/server/command.go +++ b/server/command.go @@ -12,11 +12,15 @@ import ( const ( commandTrigger = "clear" - commandHelpText = "**Delete posts with commands.**\n" + - "`/clear [number-of-post]` Delete the last `[number-of-post]` posts in the current channel" + - "" + - "Available options :" + - " _incoming feature_" + optionDeletePinnedPost = "delete-pinned-posts" + optionNoConfirmDialog = "confirm" + + commandHelpText = "## Delete posts with /" + commandTrigger + "\n" + + "`/clear [number-of-post]` Delete the last `[number-of-post]` posts in the current channel\n" + + "\n" + + "### Available options :\n" + + " * `--" + optionDeletePinnedPost + "` Also delete pinned post (disabled by default)\n" + + " * `--" + optionNoConfirmDialog + "` Do not show confirmation dialog\n" ) func (p *Plugin) getCommand() *model.Command { @@ -24,22 +28,70 @@ func (p *Plugin) getCommand() *model.Command { Trigger: commandTrigger, AutoComplete: true, AutoCompleteDesc: "Delete posts", - AutoCompleteHint: "[--option / number-of-posts]", + AutoCompleteHint: "[number-of-posts]", AutocompleteData: getAutocompleteData(), } } func getAutocompleteData() *model.AutocompleteData { - command := model.NewAutocompleteData(commandTrigger, "[--option / number-of-posts]", "Delete posts in the current channel") + command := model.NewAutocompleteData(commandTrigger, "[number-of-posts]", "Delete posts in the current channel") - command.AddTextArgument("Delete the last [number-of-post] posts in this channel.", "[number-of-post]", "[0-9]+") + command.AddTextArgument("Delete the last [number-of-post] posts in this channel", "[number-of-post]", "[0-9]+") + command.AddNamedTextArgument(optionDeletePinnedPost, "Also delete pinned posts (disabled by default)", "true", "", false) + command.AddNamedTextArgument(optionNoConfirmDialog, "Do not show confirmation dialog", "true", "", false) return command } +func parseArguments(args *model.CommandArgs) ([]string, map[string]bool, string) { + parameters := []string{} + options := make(map[string]bool) + + nextIsNamedTextArgumentValue := false + namedTextArgumentName := "" + + for position, arg := range strings.Fields(args.Command) { + if position == 0 { + continue // skip '/commandTrigger' + } + + if nextIsNamedTextArgumentValue { + // NamedTextArgument should only be "true" or "false" in this plugin + switch arg { + case "false": + delete(options, namedTextArgumentName) + case "true": + break + default: + return nil, nil, fmt.Sprintf("Invalid value for argument `--%s`, must be `true` or `false`.", namedTextArgumentName) + } + + nextIsNamedTextArgumentValue = false + namedTextArgumentName = "" + continue + } + + if strings.HasPrefix(arg, "--") { + optionName := arg[2:] + options[optionName] = true + nextIsNamedTextArgumentValue = true + namedTextArgumentName = optionName + continue + } + + parameters = append(parameters, arg) + } + + if nextIsNamedTextArgumentValue { + return nil, nil, fmt.Sprintf("Invalid value for argument `--%s`, must be `true` or `false`.", namedTextArgumentName) + } + + return parameters, options, "" +} + func (p *Plugin) verifyCommandDelete(parameters []string, args *model.CommandArgs) (int, *model.AppError) { if len(parameters) < 1 { - p.sendEphemeralPost(args, "Please precise the [number-of-post] you want to delete.") + p.sendEphemeralPost(args, "Please precise the [number-of-post] you want to delete") return 0, nil } @@ -56,22 +108,22 @@ func (p *Plugin) verifyCommandDelete(parameters []string, args *model.CommandArg currentChannel, appErr := p.API.GetChannel(args.ChannelId) if appErr != nil { - // stop the command because if numPostToDelete > currentChannel.TotalMsgCount, the plugin crashes - p.sendEphemeralPost(args, "Error when deleting posts.") + p.sendEphemeralPost(args, "Error when deleting posts") return 0, &model.AppError{ Message: "Unable to get channel statistics", DetailedError: appErr.DetailedError, } } if currentChannel.TotalMsgCount < numPostToDelete64 { - p.sendEphemeralPost(args, "Cannot delete more posts that there is in this channel.") + // stop the command because if numPostToDelete > currentChannel.TotalMsgCount, the plugin crashes + p.sendEphemeralPost(args, "Cannot delete more posts that there is in this channel") return 0, nil } return int(numPostToDelete64), nil } -func (p *Plugin) askConfirmCommandDelete(numPostToDelete int, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { +func (p *Plugin) askConfirmCommandDelete(numPostToDelete int, args *model.CommandArgs, deletePinnedPosts bool) (*model.CommandResponse, *model.AppError) { serverConfig := p.API.GetConfig() dialog := &model.OpenDialogRequest{ @@ -83,6 +135,16 @@ func (p *Plugin) askConfirmCommandDelete(numPostToDelete int, args *model.Comman SubmitLabel: "Confirm", NotifyOnCancel: false, State: strconv.Itoa(numPostToDelete), + Elements: []model.DialogElement{ + { + Type: "bool", + Name: "deletePinnedPosts", + DisplayName: "Delete pinned posts ?", + HelpText: "Pinned posts are keept by default", + Default: strconv.FormatBool(deletePinnedPosts), + Optional: true, + }, + }, }, } @@ -97,19 +159,27 @@ func (p *Plugin) askConfirmCommandDelete(numPostToDelete int, args *model.Comman } func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { - split := strings.Fields(args.Command) + parameters, options, argumentError := parseArguments(args) + if argumentError != "" { + p.sendEphemeralPost(args, argumentError) + return &model.CommandResponse{}, nil + } - if len(split) <= 1 { + if len(parameters) < 1 { p.sendEphemeralPost(args, commandHelpText) return &model.CommandResponse{}, nil } - parameters := split[1:] - - numPostToDelete, err := p.verifyCommandDelete(parameters, args) - if err != nil || numPostToDelete == 0 { - return &model.CommandResponse{}, err + numPostToDelete, appErr := p.verifyCommandDelete(parameters, args) + if appErr != nil || numPostToDelete == 0 { + return &model.CommandResponse{}, appErr } - return p.askConfirmCommandDelete(numPostToDelete, args) + deletePinnedPost := options[optionDeletePinnedPost] + + if options[optionNoConfirmDialog] { + appErr := p.deleteLastPostsInChannel(numPostToDelete, args.ChannelId, args.UserId, deletePinnedPost) + return &model.CommandResponse{}, appErr + } + return p.askConfirmCommandDelete(numPostToDelete, args, deletePinnedPost) } diff --git a/server/http.go b/server/http.go index a5d77c1..dc82acc 100644 --- a/server/http.go +++ b/server/http.go @@ -35,14 +35,19 @@ func (p *Plugin) handleDeletion(w http.ResponseWriter, r *http.Request) { numPostToDelete, err := strconv.Atoi(request.State) if err != nil { - p.API.LogError("Failed to convert string to int. Bad request.", "err", err.Error()) + p.API.LogError("Failed to convert string to int. Bad request", "err", err.Error()) w.WriteHeader(http.StatusBadRequest) return } w.WriteHeader(http.StatusOK) - if err := p.deleteLastPosts(numPostToDelete, request.ChannelId, request.UserId); err != nil { + deletePinnedPost := false + if request.Submission["deletePinnedPosts"] == true { + deletePinnedPost = true + } + + if err := p.deleteLastPostsInChannel(numPostToDelete, request.ChannelId, request.UserId, deletePinnedPost); err != nil { p.API.LogError("Failed to delete posts", "err", err.Error()) return } diff --git a/server/manifest.go b/server/manifest.go index 119f901..60c157f 100644 --- a/server/manifest.go +++ b/server/manifest.go @@ -24,11 +24,6 @@ const manifestStr = ` "windows-amd64": "server/dist/plugin-windows-amd64.exe" }, "executable": "" - }, - "settings_schema": { - "header": "", - "footer": "* To report an issue, make a suggestion or a contribution, [check the repository](https://github.com/nathanaelhoun/mattermost-plugin-clear).", - "settings": [] } } ` diff --git a/server/utilities.go b/server/utilities.go index 43327b7..d5e39ff 100644 --- a/server/utilities.go +++ b/server/utilities.go @@ -27,7 +27,7 @@ func (p *Plugin) sendEphemeralPost(args *model.CommandArgs, message string) *mod ) } -func (p *Plugin) deleteLastPosts(numPostToDelete int, channelID string, userID string) *model.AppError { +func (p *Plugin) deleteLastPostsInChannel(numPostToDelete int, channelID string, userID string, deletePinnedPosts bool) *model.AppError { postList, err := p.API.GetPostsForChannel(channelID, 0, numPostToDelete) if err != nil { p.API.LogError( @@ -39,26 +39,32 @@ func (p *Plugin) deleteLastPosts(numPostToDelete int, channelID string, userID s isError := false isErrorNotAdmin := false + isErrorPinnedPost := false numDeletedPost := 0 hasAdminRights := hasAdminRights(p, userID) for _, postID := range postList.Order { - if !hasAdminRights { - post, err := p.API.GetPost(postID) - if err != nil { - isError = true - p.API.LogError( - "Unable to get post "+postID+" informations.", - "err", err.Error(), - ) - continue // process next post - } + post, err := p.API.GetPost(postID) + if err != nil { + isError = true + p.API.LogError( + "Unable to get post "+postID+" informations", + "err", err.Error(), + ) + continue // process next post + } + if !hasAdminRights { if post.UserId != userID { isErrorNotAdmin = true continue // process next post } } + if post.IsPinned && !deletePinnedPosts { + isErrorPinnedPost = true + continue // process next post + } + if err := p.API.DeletePost(postID); err != nil { isError = true p.API.LogError( @@ -74,11 +80,15 @@ func (p *Plugin) deleteLastPosts(numPostToDelete int, channelID string, userID s strResponse := "" if isError { - strResponse += "An error has occurred, some post could not be deleted.\n" + strResponse += "An error has occurred, some post could not be deleted\n" } if isErrorNotAdmin { - strResponse += "Some posts have not been deleted because they were not yours.\n" + strResponse += "Some posts have not been deleted because they were not yours\n" + } + + if isErrorPinnedPost { + strResponse += "Some posts have not been deleted because they were pinned in the channel\n" } if numDeletedPost > 0 { @@ -86,11 +96,11 @@ func (p *Plugin) deleteLastPosts(numPostToDelete int, channelID string, userID s if numDeletedPost > 1 { plural = "s" } - strResponse += fmt.Sprintf("Successfully deleted %d post%s.", numDeletedPost, plural) + strResponse += fmt.Sprintf("Successfully deleted %d post%s", numDeletedPost, plural) } if strResponse == "" { - strResponse = "There are no posts in this channel." + strResponse = "There are no posts in this channel" } p.sendEphemeralPost(&model.CommandArgs{