package main import ( "errors" "fmt" "github.com/larkox/mattermost-plugin-badges/badgesmodel" commandparser "github.com/larkox/mattermost-plugin-badges/server/command_parser" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/plugin" "github.com/spf13/pflag" ) func getHelp() string { return `Available Commands: ` } func (p *Plugin) getCommand() *model.Command { return &model.Command{ Trigger: "badges", DisplayName: "Badges Bot", Description: "Badges", AutoComplete: true, AutoCompleteDesc: "Available commands:", AutoCompleteHint: "[command]", AutocompleteData: p.getAutocompleteData(), } } func (p *Plugin) postCommandResponse(args *model.CommandArgs, text string) { post := &model.Post{ UserId: p.BotUserID, ChannelId: args.ChannelId, Message: text, } p.mm.Post.SendEphemeralPost(args.UserId, post) } func commandError(text string) (bool, *model.CommandResponse, error) { return true, &model.CommandResponse{}, errors.New(text) } // ExecuteCommand executes a given command and returns a command response. func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { stringArgs := commandparser.Parse(args.Command) lengthOfArgs := len(stringArgs) restOfArgs := []string{} var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error) if lengthOfArgs == 1 { p.postCommandResponse(args, getHelp()) return &model.CommandResponse{}, nil } command := stringArgs[1] if lengthOfArgs > 2 { restOfArgs = stringArgs[2:] } switch command { case "test-clean": handler = p.runClean case "grant": handler = p.runGrant case "edit": handler = p.runEdit case "create": handler = p.runCreate case "subscription": handler = p.runSubscription default: p.postCommandResponse(args, getHelp()) return &model.CommandResponse{}, nil } isUserError, resp, err := handler(restOfArgs, args) if err != nil { if isUserError { p.postCommandResponse(args, fmt.Sprintf("__Error: %s__", err.Error())) } else { p.mm.Log.Error(err.Error()) p.postCommandResponse(args, "An unknown error occurred. Please talk to your system administrator for help.") } } if resp != nil { return resp, nil } return &model.CommandResponse{}, nil } func (p *Plugin) runClean(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { user, err := p.mm.User.Get(extra.UserId) if err != nil { return false, &model.CommandResponse{Text: "Cannot get user."}, nil } if !user.IsSystemAdmin() { return false, &model.CommandResponse{Text: "Only a system admin can clean the badges database."}, nil } _ = p.mm.KV.DeleteAll() return false, &model.CommandResponse{Text: "Clean"}, nil } func (p *Plugin) runCreate(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { lengthOfArgs := len(args) restOfArgs := []string{} var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error) if lengthOfArgs == 0 { return false, &model.CommandResponse{Text: "Specify what you want to create."}, nil } command := args[0] if lengthOfArgs > 1 { restOfArgs = args[1:] } switch command { case "badge": handler = p.runCreateBadge case "type": handler = p.runCreateType default: return false, &model.CommandResponse{Text: "You can create either badge or type"}, nil } return handler(restOfArgs, extra) } func (p *Plugin) runCreateBadge(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { u, err := p.mm.User.Get(extra.UserId) if err != nil { return commandError(err.Error()) } typeSuggestions, err := p.filterCreateBadgeTypes(u) if err != nil { return commandError(err.Error()) } typeOptions := []*model.PostActionOptions{} for _, typeSuggestion := range typeSuggestions { id := string(typeSuggestion.ID) typeOptions = append(typeOptions, &model.PostActionOptions{Text: typeSuggestion.Name, Value: id}) } if len(typeOptions) == 0 { return commandError("You cannot create badges from any type.") } err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ TriggerId: extra.TriggerId, URL: p.getDialogURL() + DialogPathCreateBadge, Dialog: model.Dialog{ Title: "Create badge", SubmitLabel: "Create", Elements: []model.DialogElement{ { DisplayName: "Name", Type: "text", Name: DialogFieldBadgeName, MaxLength: badgesmodel.NameMaxLength, }, { DisplayName: "Description", Type: "text", Name: DialogFieldBadgeDescription, MaxLength: badgesmodel.DescriptionMaxLength, }, { DisplayName: "Image", Type: "text", Name: DialogFieldBadgeImage, HelpText: "Insert a emoticon name", }, { DisplayName: "Type", Type: "select", Name: DialogFieldBadgeType, Options: typeOptions, }, { DisplayName: "Multiple", Type: "bool", Name: DialogFieldBadgeMultiple, HelpText: "Whether the badge can be granted multiple times", Optional: true, }, }, }, }) if err != nil { return commandError(err.Error()) } return false, &model.CommandResponse{}, nil } func (p *Plugin) runEdit(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { lengthOfArgs := len(args) restOfArgs := []string{} var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error) if lengthOfArgs == 0 { return false, &model.CommandResponse{Text: "Specify what you want to create."}, nil } command := args[0] if lengthOfArgs > 1 { restOfArgs = args[1:] } switch command { case "badge": handler = p.runEditBadge case "type": handler = p.runEditType default: return false, &model.CommandResponse{Text: "You can create either badge or type"}, nil } return handler(restOfArgs, extra) } func (p *Plugin) runEditBadge(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { u, err := p.mm.User.Get(extra.UserId) if err != nil { return commandError(err.Error()) } var badgeIDStr string fs := pflag.NewFlagSet("", pflag.ContinueOnError) fs.StringVar(&badgeIDStr, "id", "", "ID of the badge") if err = fs.Parse(args); err != nil { return commandError(err.Error()) } if badgeIDStr == "" { return commandError("You must set the badge ID") } badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr)) if err != nil { return commandError(err.Error()) } if !canEditBadge(u, p.badgeAdminUserID, badge) { return commandError("you cannot edit this badge") } typeSuggestions, err := p.filterCreateBadgeTypes(u) if err != nil { return commandError(err.Error()) } typeOptions := []*model.PostActionOptions{} for _, typeSuggestion := range typeSuggestions { id := string(typeSuggestion.ID) typeOptions = append(typeOptions, &model.PostActionOptions{Text: typeSuggestion.Name, Value: id}) } if len(typeOptions) == 0 { return commandError("You cannot create badges from any type.") } err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ TriggerId: extra.TriggerId, URL: p.getDialogURL() + DialogPathEditBadge, Dialog: model.Dialog{ Title: "Create badge", SubmitLabel: "Edit", State: string(badge.ID), Elements: []model.DialogElement{ { DisplayName: "Name", Type: "text", Name: DialogFieldBadgeName, MaxLength: badgesmodel.NameMaxLength, Default: badge.Name, }, { DisplayName: "Description", Type: "text", Name: DialogFieldBadgeDescription, MaxLength: badgesmodel.DescriptionMaxLength, Default: badge.Description, }, { DisplayName: "Image", Type: "text", Name: DialogFieldBadgeImage, HelpText: "Insert a emoticon name", Default: badge.Image, }, { DisplayName: "Type", Type: "select", Name: DialogFieldBadgeType, Options: typeOptions, Default: string(badge.Type), }, { DisplayName: "Multiple", Type: "bool", Name: DialogFieldBadgeMultiple, HelpText: "Whether the badge can be granted multiple times", Optional: true, Default: getBooleanString(badge.Multiple), }, { DisplayName: "Delete badge", Type: "bool", Name: DialogFieldBadgeDelete, HelpText: "WARNING: Checking this will remove this badge permanently.", Optional: true, }, }, }, }) if err != nil { return commandError(err.Error()) } return false, &model.CommandResponse{}, nil } func (p *Plugin) runEditType(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { u, err := p.mm.User.Get(extra.UserId) if err != nil { return commandError(err.Error()) } if !canCreateType(u, p.badgeAdminUserID, false) { return commandError("You have no permissions to edit a badge type.") } var badgeTypeStr string fs := pflag.NewFlagSet("", pflag.ContinueOnError) fs.StringVar(&badgeTypeStr, "type", "", "ID of the type") if err = fs.Parse(args); err != nil { return commandError(err.Error()) } if badgeTypeStr == "" { return commandError("You must provide a type id") } typeDefinition, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr)) if err != nil { return commandError(err.Error()) } if !canEditType(u, p.badgeAdminUserID, typeDefinition) { return commandError("you cannot edit this type") } canGrantAllowList := "" for uID, allowed := range typeDefinition.CanGrant.AllowList { if !allowed { continue } var allowedUser *model.User allowedUser, err = p.mm.User.Get(uID) if err != nil { continue } if canGrantAllowList == "" { canGrantAllowList += allowedUser.Username continue } canGrantAllowList += ", " + allowedUser.Username } canCreateAllowList := "" for uID, allowed := range typeDefinition.CanCreate.AllowList { if !allowed { continue } var allowedUser *model.User allowedUser, err = p.mm.User.Get(uID) if err != nil { continue } if canCreateAllowList == "" { canCreateAllowList += allowedUser.Username continue } canCreateAllowList += ", " + allowedUser.Username } err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ TriggerId: extra.TriggerId, URL: p.getDialogURL() + DialogPathEditType, Dialog: model.Dialog{ Title: "Edit type", SubmitLabel: "Edit", State: badgeTypeStr, Elements: []model.DialogElement{ { DisplayName: "Name", Type: "text", Name: DialogFieldTypeName, MaxLength: badgesmodel.NameMaxLength, Default: typeDefinition.Name, }, { DisplayName: "Everyone can create badge", Type: "bool", Name: DialogFieldTypeEveryoneCanCreate, HelpText: "Whether any user can create a badge of this type", Optional: true, Default: getBooleanString(typeDefinition.CanCreate.Everyone), }, { DisplayName: "Can create allowlist", Type: "text", Name: DialogFieldTypeAllowlistCanCreate, HelpText: "Fill the usernames separated by comma (,) of the people that can create badges of this type.", Placeholder: "user-1, user-2, user-3", Optional: true, Default: canCreateAllowList, }, { DisplayName: "Everyone can grant badge", Type: "bool", Name: DialogFieldTypeEveryoneCanGrant, HelpText: "Whether any user can grant a badge of this type", Optional: true, Default: getBooleanString(typeDefinition.CanGrant.Everyone), }, { DisplayName: "Can grant allowlist", Type: "text", Name: DialogFieldTypeAllowlistCanGrant, HelpText: "Fill the usernames separated by comma (,) of the people that can grant badges of this type.", Placeholder: "user-1, user-2, user-3", Optional: true, Default: canGrantAllowList, }, { DisplayName: "Remove type", Type: "bool", Name: DialogFieldTypeDelete, HelpText: "WARNING: checking this will remove this type and all associated badges permanently.", Optional: true, }, }, }, }) if err != nil { return commandError(err.Error()) } return false, &model.CommandResponse{}, nil } func (p *Plugin) runCreateType(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { u, err := p.mm.User.Get(extra.UserId) if err != nil { return commandError(err.Error()) } if !canCreateType(u, p.badgeAdminUserID, false) { return commandError("You have no permissions to create a badge type.") } err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ TriggerId: extra.TriggerId, URL: p.getDialogURL() + DialogPathCreateType, Dialog: model.Dialog{ Title: "Create type", SubmitLabel: "Create", Elements: []model.DialogElement{ { DisplayName: "Name", Type: "text", Name: DialogFieldTypeName, MaxLength: badgesmodel.NameMaxLength, }, { DisplayName: "Everyone can create badge", Type: "bool", Name: DialogFieldTypeEveryoneCanCreate, HelpText: "Whether any user can create a badge of this type", Optional: true, }, { DisplayName: "Can create allowlist", Type: "text", Name: DialogFieldTypeAllowlistCanCreate, HelpText: "Fill the usernames separated by comma (,) of the people that can create badges of this type.", Placeholder: "user-1, user-2, user-3", Optional: true, }, { DisplayName: "Everyone can grant badge", Type: "bool", Name: DialogFieldTypeEveryoneCanGrant, HelpText: "Whether any user can grant a badge of this type", Optional: true, }, { DisplayName: "Can grant allowlist", Type: "text", Name: DialogFieldTypeAllowlistCanGrant, HelpText: "Fill the usernames separated by comma (,) of the people that can grant badges of this type.", Placeholder: "user-1, user-2, user-3", Optional: true, }, }, }, }) if err != nil { return commandError(err.Error()) } return false, &model.CommandResponse{}, nil } func (p *Plugin) runGrant(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { badgeStr := "" username := "" fs := pflag.NewFlagSet("", pflag.ContinueOnError) fs.StringVar(&badgeStr, "badge", "", "ID of the badge") fs.StringVar(&username, "user", "", "Username to grant to") if err := fs.Parse(args); err != nil { return commandError(err.Error()) } if username != "" && badgeStr != "" { if username[0] == '@' { username = username[1:] } granter, err := p.mm.User.Get(extra.UserId) if err != nil { return commandError(err.Error()) } badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeStr)) if err != nil { return commandError(err.Error()) } badgeType, err := p.store.GetType(badge.Type) if err != nil { return commandError(err.Error()) } if !canGrantBadge(granter, p.badgeAdminUserID, badge, badgeType) { return commandError("you have no permissions to grant this badge") } user, err := p.mm.User.GetByUsername(username) if err != nil { return commandError(err.Error()) } shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(badgeStr), user.Id, extra.UserId, "") if err != nil { return commandError(err.Error()) } if shouldNotify { p.notifyGrant(badgesmodel.BadgeID(badgeStr), extra.UserId, user, false, "", "") } p.postCommandResponse(extra, "Granted") return false, &model.CommandResponse{}, nil } elements := []model.DialogElement{} stateText := "" introductionText := "" if username != "" { if username[0] == '@' { username = username[1:] } user, err := p.mm.User.GetByUsername(username) if err != nil { return commandError(err.Error()) } introductionText = "Grant badge to @" + username stateText = user.Id } if stateText == "" { elements = append(elements, model.DialogElement{ DisplayName: "User", Type: "select", Name: DialogFieldUser, DataSource: "users", }) } actingUser, err := p.mm.User.Get(extra.UserId) if err != nil { return commandError(err.Error()) } options := []*model.PostActionOptions{} grantableBadges, err := p.filterGrantBadges(actingUser) if err != nil { return commandError(err.Error()) } for _, badge := range grantableBadges { options = append(options, &model.PostActionOptions{Text: badge.Name, Value: string(badge.ID)}) } badgeElement := model.DialogElement{ DisplayName: "Badge", Type: "select", Name: DialogFieldBadge, Options: options, } if badgeStr != "" { found := false for _, badge := range grantableBadges { if badgeStr == string(badge.ID) { found = true break } } if !found { return commandError("You cannot grant that badge") } badgeElement.Default = badgeStr } elements = append(elements, badgeElement) elements = append(elements, model.DialogElement{ DisplayName: "Reason", Name: DialogFieldGrantReason, Optional: true, HelpText: "Reason why you are granting this badge. This will be seen by the user, and wherever this grant notification is shown (e.g. subscriptions).", Type: "text", }) elements = append(elements, model.DialogElement{ DisplayName: "Notify on this channel", Name: DialogFieldNotifyHere, Type: "bool", HelpText: "If you mark this, the bot will send a message to this channel notifying that you granted this badge to this person.", Optional: true, }) err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ TriggerId: extra.TriggerId, URL: p.getDialogURL() + DialogPathGrant, Dialog: model.Dialog{ Title: "Grant badge", IntroductionText: introductionText, SubmitLabel: "Grant", Elements: elements, State: stateText, }, }) if err != nil { return commandError(err.Error()) } return false, &model.CommandResponse{}, nil } func (p *Plugin) runSubscription(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { lengthOfArgs := len(args) restOfArgs := []string{} var handler func([]string, *model.CommandArgs) (bool, *model.CommandResponse, error) if lengthOfArgs == 0 { return false, &model.CommandResponse{Text: "Specify what you want to do."}, nil } command := args[0] if lengthOfArgs > 1 { restOfArgs = args[1:] } switch command { case "create": handler = p.runCreateSubscription case "remove": handler = p.runDeleteSubscription default: return false, &model.CommandResponse{Text: "You can either create or delete subscriptions"}, nil } return handler(restOfArgs, extra) } func (p *Plugin) runCreateSubscription(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { typeStr := "" fs := pflag.NewFlagSet("", pflag.ContinueOnError) fs.StringVar(&typeStr, "type", "", "ID of the badge") if err := fs.Parse(args); err != nil { return commandError(err.Error()) } actingUser, err := p.mm.User.Get(extra.UserId) if err != nil { return commandError(err.Error()) } if !canCreateSubscription(actingUser, p.badgeAdminUserID, extra.ChannelId) { return commandError("You cannot create subscriptions") } if typeStr != "" { err = p.store.AddSubscription(badgesmodel.BadgeType(typeStr), extra.ChannelId) if err != nil { return commandError(err.Error()) } p.postCommandResponse(extra, "Granted") return false, &model.CommandResponse{}, nil } options := []*model.PostActionOptions{} typesDefinitions, err := p.filterEditTypes(actingUser) if err != nil { return commandError(err.Error()) } for _, typeDefinition := range typesDefinitions { options = append(options, &model.PostActionOptions{Text: typeDefinition.Name, Value: string(typeDefinition.ID)}) } err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ TriggerId: extra.TriggerId, URL: p.getDialogURL() + DialogPathCreateSubscription, Dialog: model.Dialog{ Title: "Create subscription", IntroductionText: "Introduce the badge type you want to subscribe to this channel.", SubmitLabel: "Add", Elements: []model.DialogElement{ { DisplayName: "Type", Type: "select", Name: DialogFieldBadgeType, Options: options, }, }, }, }) if err != nil { return commandError(err.Error()) } return false, &model.CommandResponse{}, nil } func (p *Plugin) runDeleteSubscription(args []string, extra *model.CommandArgs) (bool, *model.CommandResponse, error) { typeStr := "" fs := pflag.NewFlagSet("", pflag.ContinueOnError) fs.StringVar(&typeStr, "type", "", "ID of the badge") if err := fs.Parse(args); err != nil { return commandError(err.Error()) } actingUser, err := p.mm.User.Get(extra.UserId) if err != nil { return commandError(err.Error()) } if !canCreateSubscription(actingUser, p.badgeAdminUserID, extra.ChannelId) { return commandError("You cannot create subscriptions") } if typeStr != "" { err = p.store.RemoveSubscriptions(badgesmodel.BadgeType(typeStr), extra.ChannelId) if err != nil { return commandError(err.Error()) } p.postCommandResponse(extra, "Removed") return false, &model.CommandResponse{}, nil } options := []*model.PostActionOptions{} typesDefinitions, err := p.store.GetChannelSubscriptions(extra.ChannelId) if err != nil { return commandError(err.Error()) } for _, typeDefinition := range typesDefinitions { options = append(options, &model.PostActionOptions{Text: typeDefinition.Name, Value: string(typeDefinition.ID)}) } err = p.mm.Frontend.OpenInteractiveDialog(model.OpenDialogRequest{ TriggerId: extra.TriggerId, URL: p.getDialogURL() + DialogPathDeleteSubscription, Dialog: model.Dialog{ Title: "Delete subscription", IntroductionText: "Introduce the badge type you want to remove from this channel.", SubmitLabel: "Remove", Elements: []model.DialogElement{ { DisplayName: "Type", Type: "select", Name: DialogFieldBadgeType, Options: options, }, }, }, }) if err != nil { return commandError(err.Error()) } return false, &model.CommandResponse{}, nil } func (p *Plugin) getAutocompleteData() *model.AutocompleteData { badges := model.NewAutocompleteData("badges", "[command]", "Available commands: grant") grant := model.NewAutocompleteData("grant", "--user @username --badge id", "Grant a badge to a user") grant.AddNamedDynamicListArgument("badge", "--badge badgeID", getAutocompletePath(AutocompletePathBadgeSuggestions), true) grant.AddNamedTextArgument("user", "User to grant the badge to", "--user @username", "", true) badges.AddCommand(grant) create := model.NewAutocompleteData("create", "badge | type", "Create a badge or a type") badge := model.NewAutocompleteData( "badge", "", "Create a badge", ) create.AddCommand(badge) createType := model.NewAutocompleteData( "type", "", "Create a badge type", ) create.AddCommand(createType) badges.AddCommand(create) edit := model.NewAutocompleteData("edit", "badge | type", "Edit a badge or a type") editBadge := model.NewAutocompleteData( "badge", "", "Edit a badge", ) editBadge.AddNamedDynamicListArgument("id", "--id badgeID", getAutocompletePath(AutocompletePathEditBadgeSuggestions), true) edit.AddCommand(editBadge) editType := model.NewAutocompleteData( "type", "", "Edit a badge type", ) editType.AddNamedDynamicListArgument("type", "--type typeID", getAutocompletePath(AutocompletePathEditTypeSuggestions), true) edit.AddCommand(editType) badges.AddCommand(edit) subscription := model.NewAutocompleteData("subscription", "create | remove", "Manage this channel subscriptions") createSubscription := model.NewAutocompleteData( "create", "", "Create a subscription", ) subscription.AddCommand(createSubscription) deleteSubscription := model.NewAutocompleteData( "remove", "", "Remove a subscription", ) subscription.AddCommand(deleteSubscription) badges.AddCommand(subscription) return badges } func getAutocompletePath(path string) string { return "plugins/" + manifest.Id + AutocompletePath + path }