package main import ( "encoding/json" "net/http" "runtime/debug" "strings" "github.com/gorilla/mux" "github.com/larkox/mattermost-plugin-badges/badgesmodel" "github.com/mattermost/mattermost-server/v5/model" ) // HTTPHandlerFuncWithUser is http.HandleFunc but userID is already exported type HTTPHandlerFuncWithUser func(w http.ResponseWriter, r *http.Request, userID string) // ResponseType indicates type of response returned by api type ResponseType string const ( // ResponseTypeJSON indicates that response type is json ResponseTypeJSON ResponseType = "JSON_RESPONSE" // ResponseTypePlain indicates that response type is text plain ResponseTypePlain ResponseType = "TEXT_RESPONSE" // ResponseTypeDialog indicates that response type is a dialog response ResponseTypeDialog ResponseType = "DIALOG" ) type APIErrorResponse struct { ID string `json:"id"` Message string `json:"message"` StatusCode int `json:"status_code"` } type CreateBadgeRequest struct { Name string `json:"name"` Description string `json:"description"` Image string `json:"image"` Type string `json:"type"` Multiple bool `json:"multiple"` ChannelID string `json:"channel_id"` } type UpdateBadgeRequest struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` Image string `json:"image"` Type string `json:"type"` Multiple bool `json:"multiple"` } type CreateTypeRequest struct { Name string `json:"name"` EveryoneCanCreate bool `json:"everyone_can_create"` EveryoneCanGrant bool `json:"everyone_can_grant"` ChannelID string `json:"channel_id"` } type TypeWithBadgeCount struct { *badgesmodel.BadgeTypeDefinition BadgeCount int `json:"badge_count"` } type GetTypesResponse struct { Types []TypeWithBadgeCount `json:"types"` CanCreateType bool `json:"can_create_type"` } func (p *Plugin) initializeAPI() { p.router = mux.NewRouter() p.router.Use(p.withRecovery) apiRouter := p.router.PathPrefix("/api/v1").Subrouter() pluginAPIRouter := p.router.PathPrefix(badgesmodel.PluginAPIPath).Subrouter() autocompleteRouter := p.router.PathPrefix(AutocompletePath).Subrouter() dialogRouter := p.router.PathPrefix(DialogPath).Subrouter() apiRouter.HandleFunc("/getUserBadges/{userID}", p.extractUserMiddleWare(p.getUserBadges, ResponseTypeJSON)).Methods(http.MethodGet) apiRouter.HandleFunc("/getBadgeDetails/{badgeID}", p.extractUserMiddleWare(p.getBadgeDetails, ResponseTypeJSON)).Methods(http.MethodGet) apiRouter.HandleFunc("/getAllBadges", p.extractUserMiddleWare(p.getAllBadges, ResponseTypeJSON)).Methods(http.MethodGet) apiRouter.HandleFunc("/getTypes", p.extractUserMiddleWare(p.getTypes, ResponseTypeJSON)).Methods(http.MethodGet) apiRouter.HandleFunc("/createBadge", p.extractUserMiddleWare(p.apiCreateBadge, ResponseTypeJSON)).Methods(http.MethodPost) apiRouter.HandleFunc("/createType", p.extractUserMiddleWare(p.apiCreateType, ResponseTypeJSON)).Methods(http.MethodPost) apiRouter.HandleFunc("/updateBadge", p.extractUserMiddleWare(p.apiUpdateBadge, ResponseTypeJSON)).Methods(http.MethodPut) apiRouter.HandleFunc("/deleteBadge/{badgeID}", p.extractUserMiddleWare(p.apiDeleteBadge, ResponseTypeJSON)).Methods(http.MethodDelete) apiRouter.HandleFunc("/deleteType/{typeID}", p.extractUserMiddleWare(p.apiDeleteType, ResponseTypeJSON)).Methods(http.MethodDelete) pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathEnsure, checkPluginRequest(p.ensureBadges)).Methods(http.MethodPost) pluginAPIRouter.HandleFunc(badgesmodel.PluginAPIPathGrant, checkPluginRequest(p.grantBadge)).Methods(http.MethodPost) autocompleteRouter.HandleFunc(AutocompletePathBadgeSuggestions, p.extractUserMiddleWare(p.getBadgeSuggestions, ResponseTypeJSON)).Methods(http.MethodGet) autocompleteRouter.HandleFunc(AutocompletePathEditBadgeSuggestions, p.extractUserMiddleWare(p.getEditBadgeSuggestions, ResponseTypeJSON)).Methods(http.MethodGet) autocompleteRouter.HandleFunc(AutocompletePathTypeSuggestions, p.extractUserMiddleWare(p.getBadgeTypeSuggestions, ResponseTypeJSON)).Methods(http.MethodGet) autocompleteRouter.HandleFunc(AutocompletePathEditTypeSuggestions, p.extractUserMiddleWare(p.getEditBadgeTypeSuggestions, ResponseTypeJSON)).Methods(http.MethodGet) dialogRouter.HandleFunc(DialogPathCreateBadge, p.extractUserMiddleWare(p.dialogCreateBadge, ResponseTypeDialog)).Methods(http.MethodPost) dialogRouter.HandleFunc(DialogPathCreateType, p.extractUserMiddleWare(p.dialogCreateType, ResponseTypeDialog)).Methods(http.MethodPost) dialogRouter.HandleFunc(DialogPathGrant, p.extractUserMiddleWare(p.dialogGrant, ResponseTypeDialog)).Methods(http.MethodPost) dialogRouter.HandleFunc(DialogPathSelectBadge, p.extractUserMiddleWare(p.dialogSelectBadge, ResponseTypeDialog)).Methods(http.MethodPost) dialogRouter.HandleFunc(DialogPathSelectType, p.extractUserMiddleWare(p.dialogSelectType, ResponseTypeDialog)).Methods(http.MethodPost) dialogRouter.HandleFunc(DialogPathEditBadge, p.extractUserMiddleWare(p.dialogEditBadge, ResponseTypeDialog)).Methods(http.MethodPost) dialogRouter.HandleFunc(DialogPathEditType, p.extractUserMiddleWare(p.dialogEditType, ResponseTypeDialog)).Methods(http.MethodPost) dialogRouter.HandleFunc(DialogPathCreateSubscription, p.extractUserMiddleWare(p.dialogCreateSubscription, ResponseTypeDialog)).Methods(http.MethodPost) dialogRouter.HandleFunc(DialogPathDeleteSubscription, p.extractUserMiddleWare(p.dialogDeleteSubscription, ResponseTypeDialog)).Methods(http.MethodPost) p.router.PathPrefix("/").HandlerFunc(p.defaultHandler) } func (p *Plugin) defaultHandler(w http.ResponseWriter, r *http.Request) { p.mm.Log.Debug("Unexpected call", "url", r.URL) w.WriteHeader(http.StatusNotFound) } func dialogError(w http.ResponseWriter, text string, errors map[string]string) { resp := &model.SubmitDialogResponse{ Error: "Error: " + text, Errors: errors, } _, _ = w.Write(resp.ToJson()) } func dialogOK(w http.ResponseWriter) { resp := &model.SubmitDialogResponse{} _, _ = w.Write(resp.ToJson()) } func dialogKeepOpen(w http.ResponseWriter) { resp := &model.SubmitDialogResponse{ Error: "_", } _, _ = w.Write(resp.ToJson()) } func (p *Plugin) getTypes(w http.ResponseWriter, r *http.Request, userID string) { u, err := p.mm.User.Get(userID) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError, }) return } types, err := p.filterCreateBadgeTypes(u) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_get_types", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } badges, err := p.store.GetRawBadges() if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_get_badges", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } badgeCountByType := map[badgesmodel.BadgeType]int{} for _, badge := range badges { badgeCountByType[badge.Type]++ } result := make([]TypeWithBadgeCount, len(types)) for i, t := range types { result[i] = TypeWithBadgeCount{ BadgeTypeDefinition: t, BadgeCount: badgeCountByType[t.ID], } } resp := GetTypesResponse{ Types: result, CanCreateType: canCreateType(u, p.badgeAdminUserID, false), } b, _ := json.Marshal(resp) _, _ = w.Write(b) } func (p *Plugin) apiCreateBadge(w http.ResponseWriter, r *http.Request, userID string) { var req CreateBadgeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "invalid_request", Message: "Invalid request body", StatusCode: http.StatusBadRequest, }) return } user, err := p.mm.User.Get(userID) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError, }) return } req.Name = strings.TrimSpace(req.Name) req.Description = strings.TrimSpace(req.Description) req.Image = strings.TrimSpace(req.Image) if req.Name == "" { p.writeAPIError(w, &APIErrorResponse{ ID: "invalid_name", Message: "Name is required", StatusCode: http.StatusBadRequest, }) return } if length := len(req.Image); length > 1 && req.Image[0] == ':' && req.Image[length-1] == ':' { req.Image = req.Image[1 : length-1] } if req.Image == "" { p.writeAPIError(w, &APIErrorResponse{ ID: "invalid_image", Message: "Emoji is required", StatusCode: http.StatusBadRequest, }) return } t, err := p.store.GetType(badgesmodel.BadgeType(req.Type)) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "type_not_found", Message: "Badge type not found", StatusCode: http.StatusBadRequest, }) return } if !canCreateBadge(user, p.badgeAdminUserID, t) { p.writeAPIError(w, &APIErrorResponse{ ID: "no_permission", Message: "No permission to create badge of this type", StatusCode: http.StatusForbidden, }) return } toCreate := &badgesmodel.Badge{ Name: req.Name, Description: req.Description, Image: req.Image, ImageType: badgesmodel.ImageTypeEmoji, Multiple: req.Multiple, Type: badgesmodel.BadgeType(req.Type), CreatedBy: userID, } created, err := p.store.AddBadge(toCreate) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_create_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } if req.ChannelID != "" { T := p.getT(user.Locale) p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelID, Message: T("badges.api.badge_created", "Значок `%s` создан.", toCreate.Name), }) } b, _ := json.Marshal(created) w.WriteHeader(http.StatusCreated) _, _ = w.Write(b) } func (p *Plugin) apiCreateType(w http.ResponseWriter, r *http.Request, userID string) { var req CreateTypeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "invalid_request", Message: "Invalid request body", StatusCode: http.StatusBadRequest, }) return } user, err := p.mm.User.Get(userID) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError, }) return } if !canCreateType(user, p.badgeAdminUserID, false) { p.writeAPIError(w, &APIErrorResponse{ ID: "no_permission", Message: "No permission to create type", StatusCode: http.StatusForbidden, }) return } req.Name = strings.TrimSpace(req.Name) if req.Name == "" { p.writeAPIError(w, &APIErrorResponse{ ID: "invalid_name", Message: "Type name is required", StatusCode: http.StatusBadRequest, }) return } toCreate := &badgesmodel.BadgeTypeDefinition{ Name: req.Name, CreatedBy: userID, } toCreate.CanCreate.Everyone = req.EveryoneCanCreate toCreate.CanGrant.Everyone = req.EveryoneCanGrant created, err := p.store.AddType(toCreate) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_create_type", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } if req.ChannelID != "" { T := p.getT(user.Locale) p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelID, Message: T("badges.api.type_created", "Тип `%s` создан.", toCreate.Name), }) } b, _ := json.Marshal(created) w.WriteHeader(http.StatusCreated) _, _ = w.Write(b) } func (p *Plugin) apiUpdateBadge(w http.ResponseWriter, r *http.Request, userID string) { var req UpdateBadgeRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "invalid_request", Message: "Invalid request body", StatusCode: http.StatusBadRequest, }) return } user, err := p.mm.User.Get(userID) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError, }) return } badge, err := p.store.GetBadge(badgesmodel.BadgeID(req.ID)) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "badge_not_found", Message: "Badge not found", StatusCode: http.StatusNotFound, }) return } if !canEditBadge(user, p.badgeAdminUserID, badge) { p.writeAPIError(w, &APIErrorResponse{ ID: "no_permission", Message: "No permission to edit this badge", StatusCode: http.StatusForbidden, }) return } req.Name = strings.TrimSpace(req.Name) req.Description = strings.TrimSpace(req.Description) req.Image = strings.TrimSpace(req.Image) if req.Name == "" { p.writeAPIError(w, &APIErrorResponse{ ID: "invalid_name", Message: "Name is required", StatusCode: http.StatusBadRequest, }) return } if length := len(req.Image); length > 1 && req.Image[0] == ':' && req.Image[length-1] == ':' { req.Image = req.Image[1 : length-1] } if req.Image == "" { p.writeAPIError(w, &APIErrorResponse{ ID: "invalid_image", Message: "Emoji is required", StatusCode: http.StatusBadRequest, }) return } badge.Name = req.Name badge.Description = req.Description badge.Image = req.Image badge.Type = badgesmodel.BadgeType(req.Type) badge.Multiple = req.Multiple if err := p.store.UpdateBadge(badge); err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_update_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } b, _ := json.Marshal(badge) _, _ = w.Write(b) } func (p *Plugin) apiDeleteBadge(w http.ResponseWriter, r *http.Request, userID string) { badgeID, ok := mux.Vars(r)["badgeID"] if !ok { p.writeAPIError(w, &APIErrorResponse{ ID: "missing_badge_id", Message: "Missing badge ID", StatusCode: http.StatusBadRequest, }) return } user, err := p.mm.User.Get(userID) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError, }) return } badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeID)) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "badge_not_found", Message: "Badge not found", StatusCode: http.StatusNotFound, }) return } if !canEditBadge(user, p.badgeAdminUserID, badge) { p.writeAPIError(w, &APIErrorResponse{ ID: "no_permission", Message: "No permission to delete this badge", StatusCode: http.StatusForbidden, }) return } if err := p.store.DeleteBadge(badgesmodel.BadgeID(badgeID)); err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_delete_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } _, _ = w.Write([]byte(`{"success": true}`)) } func (p *Plugin) apiDeleteType(w http.ResponseWriter, r *http.Request, userID string) { typeID, ok := mux.Vars(r)["typeID"] if !ok { p.writeAPIError(w, &APIErrorResponse{ ID: "missing_type_id", Message: "Missing type ID", StatusCode: http.StatusBadRequest, }) return } user, err := p.mm.User.Get(userID) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_get_user", Message: "Cannot get user", StatusCode: http.StatusInternalServerError, }) return } t, err := p.store.GetType(badgesmodel.BadgeType(typeID)) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "type_not_found", Message: "Type not found", StatusCode: http.StatusNotFound, }) return } if t.IsDefault { T := p.getT("ru") p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_delete_default_type", Message: T("badges.api.cannot_delete_default_type", "Нельзя удалить тип по умолчанию"), StatusCode: http.StatusForbidden, }) return } if !canEditType(user, p.badgeAdminUserID, t) { p.writeAPIError(w, &APIErrorResponse{ ID: "no_permission", Message: "No permission to delete this type", StatusCode: http.StatusForbidden, }) return } if err := p.store.DeleteType(badgesmodel.BadgeType(typeID)); err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot_delete_type", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } _, _ = w.Write([]byte(`{"success": true}`)) } func (p *Plugin) dialogCreateBadge(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } user, err := p.mm.User.Get(userID) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.cannot_get_user", "Не удалось найти пользователя"), nil) return } T := p.getT(user.Locale) toCreate := &badgesmodel.Badge{} toCreate.CreatedBy = userID toCreate.ImageType = badgesmodel.ImageTypeEmoji name, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeName) if errors != nil { dialogError(w, errText, errors) return } toCreate.Name = name description, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeDescription) if errors != nil { dialogError(w, errText, errors) return } toCreate.Description = description image, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeImage) if errors != nil { dialogError(w, errText, errors) return } if length := len(image); length > 1 && image[0] == ':' && image[length-1] == ':' { image = image[1 : len(image)-1] } if image == "" { dialogError(w, T("badges.api.invalid_field", "Некорректное поле"), map[string]string{"image": T("badges.api.empty_emoji", "Пустой эмодзи")}) return } toCreate.Image = image badgeTypeStr, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeType) if errors != nil { dialogError(w, errText, errors) return } toCreate.Type = badgesmodel.BadgeType(badgeTypeStr) toCreate.Multiple = getDialogSubmissionBoolField(req, DialogFieldBadgeMultiple) t, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr)) if err != nil { dialogError(w, T("badges.api.type_not_exist", "Этот тип не существует"), nil) return } if !canCreateBadge(user, p.badgeAdminUserID, t) { dialogError(w, T("badges.api.no_permissions_create_badge", "У вас нет прав на создание этого значка"), nil) return } _, err = p.store.AddBadge(toCreate) if err != nil { dialogError(w, err.Error(), nil) return } p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelId, Message: T("badges.api.badge_created", "Значок `%s` создан.", toCreate.Name), }) dialogOK(w) } func (p *Plugin) dialogCreateType(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } u, err := p.mm.User.Get(userID) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.cannot_get_user", "Не удалось найти пользователя"), nil) return } T := p.getT(u.Locale) if !canCreateType(u, p.badgeAdminUserID, false) { dialogError(w, T("badges.api.no_permissions_create_type", "У вас нет прав на создание типа"), nil) return } toCreate := &badgesmodel.BadgeTypeDefinition{} toCreate.CreatedBy = userID toCreate.CanCreate.Everyone = getDialogSubmissionBoolField(req, DialogFieldTypeEveryoneCanCreate) toCreate.CanGrant.Everyone = getDialogSubmissionBoolField(req, DialogFieldTypeEveryoneCanGrant) name, errText, errors := getDialogSubmissionTextField(req, DialogFieldTypeName) if errors != nil { dialogError(w, errText, errors) return } toCreate.Name = name createAllowList, _ := req.Submission[DialogFieldTypeAllowlistCanCreate].(string) grantAllowList, _ := req.Submission[DialogFieldTypeAllowlistCanGrant].(string) if createAllowList != "" { toCreate.CanCreate.AllowList = map[string]bool{} usernames := strings.Split(createAllowList, ",") for _, username := range usernames { username = strings.TrimSpace(username) if username == "" { continue } foundUser, userErr := p.mm.User.GetByUsername(username) if userErr != nil { dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), map[string]string{DialogFieldTypeAllowlistCanCreate: T("badges.api.error_getting_user", "Ошибка получения пользователя %s: %v", username, userErr)}) return } toCreate.CanCreate.AllowList[foundUser.Id] = true } } if grantAllowList != "" { toCreate.CanGrant.AllowList = map[string]bool{} usernames := strings.Split(grantAllowList, ",") for _, username := range usernames { username = strings.TrimSpace(username) if username == "" { continue } foundUser, userErr := p.mm.User.GetByUsername(username) if userErr != nil { dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), map[string]string{DialogFieldTypeAllowlistCanGrant: T("badges.api.error_getting_user", "Ошибка получения пользователя %s: %v", username, userErr)}) return } toCreate.CanGrant.AllowList[foundUser.Id] = true } } _, err = p.store.AddType(toCreate) if err != nil { dialogError(w, err.Error(), nil) return } p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelId, Message: T("badges.api.type_created", "Тип `%s` создан.", toCreate.Name), }) dialogOK(w) } // This is not working on the current webapp architecture. A similar approach should be handled using Apps. func (p *Plugin) dialogSelectType(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } badgeTypeStr, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeType) if errors != nil { dialogError(w, errText, errors) return } t, err := p.store.GetType(badgesmodel.BadgeType(badgeTypeStr)) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.cannot_get_type", "Не удалось получить тип"), map[string]string{DialogFieldBadgeType: T("badges.api.cannot_get_type", "Не удалось получить тип")}) return } u, err := p.mm.User.Get(userID) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), nil) return } T := p.getT(u.Locale) if !canEditType(u, p.badgeAdminUserID, t) { dialogError(w, T("badges.api.cannot_edit_type", "Вы не можете редактировать этот тип"), nil) return } _, _ = p.mm.SlashCommand.Execute(&model.CommandArgs{ UserId: userID, ChannelId: req.ChannelId, TeamId: req.TeamId, Command: "/badges edit type --type " + badgeTypeStr, }) dialogKeepOpen(w) } func (p *Plugin) dialogEditType(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } u, err := p.mm.User.Get(userID) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), nil) return } T := p.getT(u.Locale) originalTypeID := req.State originalType, err := p.store.GetType(badgesmodel.BadgeType(originalTypeID)) if err != nil { dialogError(w, T("badges.api.could_not_get_type", "Не удалось получить тип"), nil) return } if !canEditType(u, p.badgeAdminUserID, originalType) { dialogError(w, T("badges.api.no_permissions_edit_type", "У вас нет прав на редактирование этого типа"), nil) return } if getDialogSubmissionBoolField(req, DialogFieldTypeDelete) { if originalType.IsDefault { dialogError(w, T("badges.api.cannot_delete_default_type", "Нельзя удалить тип по умолчанию"), nil) return } err = p.store.DeleteType(badgesmodel.BadgeType(originalTypeID)) if err != nil { dialogError(w, err.Error(), nil) } return } originalType.CanCreate.Everyone = getDialogSubmissionBoolField(req, DialogFieldTypeEveryoneCanCreate) originalType.CanGrant.Everyone = getDialogSubmissionBoolField(req, DialogFieldTypeEveryoneCanGrant) name, errText, errors := getDialogSubmissionTextField(req, DialogFieldTypeName) if errors != nil { dialogError(w, errText, errors) return } originalType.Name = name createAllowList, _ := req.Submission[DialogFieldTypeAllowlistCanCreate].(string) grantAllowList, _ := req.Submission[DialogFieldTypeAllowlistCanGrant].(string) originalType.CanCreate.AllowList = map[string]bool{} usernames := strings.Split(createAllowList, ",") for _, username := range usernames { username = strings.TrimSpace(username) if username == "" { continue } var allowedUser *model.User allowedUser, err = p.mm.User.GetByUsername(username) if err != nil { dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), map[string]string{DialogFieldTypeAllowlistCanCreate: T("badges.api.error_getting_user", "Ошибка получения пользователя %s: %v", username, err)}) return } originalType.CanCreate.AllowList[allowedUser.Id] = true } originalType.CanGrant.AllowList = map[string]bool{} usernames = strings.Split(grantAllowList, ",") for _, username := range usernames { username = strings.TrimSpace(username) if username == "" { continue } var allowedUser *model.User allowedUser, err = p.mm.User.GetByUsername(username) if err != nil { dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), map[string]string{DialogFieldTypeAllowlistCanGrant: T("badges.api.error_getting_user", "Ошибка получения пользователя %s: %v", username, err)}) return } originalType.CanGrant.AllowList[allowedUser.Id] = true } err = p.store.UpdateType(originalType) if err != nil { dialogError(w, err.Error(), nil) return } p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelId, Message: T("badges.api.type_updated", "Тип `%s` обновлён.", originalType.Name), }) dialogOK(w) } // This is not working on the current webapp architecture. A similar approach should be handled using Apps. func (p *Plugin) dialogSelectBadge(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } badgeIDStr, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadge) if errors != nil { dialogError(w, errText, errors) return } b, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr)) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.cannot_get_badge", "Не удалось получить значок"), map[string]string{DialogFieldBadge: T("badges.api.cannot_get_badge", "Не удалось получить значок")}) return } u, err := p.mm.User.Get(userID) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), nil) return } T := p.getT(u.Locale) if !canEditBadge(u, p.badgeAdminUserID, b) { dialogError(w, T("badges.api.cannot_edit_badge", "Вы не можете редактировать этот значок"), nil) return } _, _ = p.mm.SlashCommand.Execute(&model.CommandArgs{ UserId: userID, ChannelId: req.ChannelId, TeamId: req.TeamId, Command: "/badges edit badge --id " + badgeIDStr, }) dialogKeepOpen(w) } func (p *Plugin) dialogEditBadge(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } u, err := p.mm.User.Get(userID) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.cannot_find_user", "Не удалось найти пользователя"), nil) return } T := p.getT(u.Locale) originalBadgeID := req.State originalBadge, err := p.store.GetBadge(badgesmodel.BadgeID(originalBadgeID)) if err != nil { dialogError(w, T("badges.api.could_not_get_badge", "Не удалось получить значок"), nil) return } if !canEditBadge(u, p.badgeAdminUserID, originalBadge) { dialogError(w, T("badges.api.no_permissions_edit_badge", "У вас нет прав на редактирование этого значка"), nil) return } if getDialogSubmissionBoolField(req, DialogFieldBadgeDelete) { err = p.store.DeleteBadge(badgesmodel.BadgeID(originalBadgeID)) if err != nil { dialogError(w, err.Error(), nil) return } return } name, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeName) if errors != nil { dialogError(w, errText, errors) return } originalBadge.Name = name description, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeDescription) if errors != nil { dialogError(w, errText, errors) return } originalBadge.Description = description image, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeImage) if errors != nil { dialogError(w, errText, errors) return } if length := len(image); length > 1 && image[0] == ':' && image[length-1] == ':' { image = image[1 : len(image)-1] } if image == "" { dialogError(w, T("badges.api.invalid_field", "Некорректное поле"), map[string]string{"image": T("badges.api.empty_emoji", "Пустой эмодзи")}) return } originalBadge.Image = image badgeTypeStr, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeType) if errors != nil { dialogError(w, errText, errors) return } originalBadge.Type = badgesmodel.BadgeType(badgeTypeStr) originalBadge.Multiple = getDialogSubmissionBoolField(req, DialogFieldBadgeMultiple) err = p.store.UpdateBadge(originalBadge) if err != nil { dialogError(w, err.Error(), nil) return } p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelId, Message: T("badges.api.badge_updated", "Значок `%s` обновлён.", originalBadge.Name), }) dialogOK(w) } func (p *Plugin) dialogGrant(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } badgeIDStr, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadge) if errors != nil { dialogError(w, errText, errors) return } notifyHere := getDialogSubmissionBoolField(req, DialogFieldNotifyHere) badge, err := p.store.GetBadge(badgesmodel.BadgeID(badgeIDStr)) if err != nil { T := p.getT("ru") dialogError(w, T("badges.api.badge_not_found", "Значок не найден"), nil) return } granter, err := p.mm.User.Get(userID) if err != nil { dialogError(w, err.Error(), nil) return } T := p.getT(granter.Locale) badgeType, err := p.store.GetType(badge.Type) if err != nil { dialogError(w, err.Error(), nil) return } if !canGrantBadge(granter, p.badgeAdminUserID, badge, badgeType) { dialogError(w, T("badges.api.no_permissions_grant", "У вас нет прав на выдачу этого значка"), nil) return } grantToID := req.State if grantToID == "" { grantToID, errText, errors = getDialogSubmissionTextField(req, DialogFieldUser) if errors != nil { dialogError(w, errText, errors) return } } grantToUser, err := p.mm.User.Get(grantToID) if err != nil { dialogError(w, T("badges.api.user_not_found", "Пользователь не найден"), nil) return } reason, _ := req.Submission[DialogFieldGrantReason].(string) shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(badgeIDStr), grantToID, userID, reason) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot grant badge", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } if shouldNotify { p.notifyGrant(badgesmodel.BadgeID(badgeIDStr), userID, grantToUser, notifyHere, req.ChannelId, reason) } p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelId, Message: T("badges.api.badge_granted", "Значок `%s` выдан @%s.", badge.Name, grantToUser.Username), }) dialogOK(w) } func (p *Plugin) dialogCreateSubscription(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } u, err := p.mm.User.Get(userID) if err != nil { dialogError(w, err.Error(), nil) return } T := p.getT(u.Locale) if !canCreateSubscription(u, p.badgeAdminUserID, req.ChannelId) { dialogError(w, T("badges.api.cannot_create_subscription", "Вы не можете создать подписку"), nil) return } typeIDStr, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeType) if errors != nil { dialogError(w, errText, errors) return } err = p.store.AddSubscription(badgesmodel.BadgeType(typeIDStr), req.ChannelId) if err != nil { dialogError(w, err.Error(), nil) } p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelId, Message: T("badges.api.subscription_added", "Подписка добавлена"), }) dialogOK(w) } func (p *Plugin) dialogDeleteSubscription(w http.ResponseWriter, r *http.Request, userID string) { req := model.SubmitDialogRequestFromJson(r.Body) if req == nil { T := p.getT("ru") dialogError(w, T("badges.api.dialog_parse_error", "Не удалось получить данные диалога"), nil) return } u, err := p.mm.User.Get(userID) if err != nil { dialogError(w, err.Error(), nil) return } T := p.getT(u.Locale) if !canCreateSubscription(u, p.badgeAdminUserID, req.ChannelId) { dialogError(w, T("badges.api.cannot_delete_subscription", "Вы не можете удалить подписку"), nil) return } typeIDStr, errText, errors := getDialogSubmissionTextField(req, DialogFieldBadgeType) if errors != nil { dialogError(w, errText, errors) return } err = p.store.RemoveSubscriptions(badgesmodel.BadgeType(typeIDStr), req.ChannelId) if err != nil { dialogError(w, err.Error(), nil) } p.mm.Post.SendEphemeralPost(userID, &model.Post{ UserId: p.BotUserID, ChannelId: req.ChannelId, Message: T("badges.api.subscription_removed", "Подписка удалена"), }) dialogOK(w) } func getDialogSubmissionTextField(req *model.SubmitDialogRequest, fieldName string) (value string, errText string, errors map[string]string) { value, ok := req.Submission[fieldName].(string) value = strings.TrimSpace(value) if !ok || value == "" { return "", "Invalid argument", map[string]string{fieldName: "Field empty or not recognized."} } return value, "", nil } func getDialogSubmissionBoolField(req *model.SubmitDialogRequest, fieldName string) bool { value, _ := req.Submission[fieldName].(bool) return value } func (p *Plugin) grantBadge(w http.ResponseWriter, r *http.Request, pluginID string) { var req *badgesmodel.GrantBadgeRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot unmarshal request", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } p.mm.Log.Debug("Granting badge", "req", req) if req == nil { p.writeAPIError(w, &APIErrorResponse{ ID: "missing request", Message: "Missing grant request on request body", StatusCode: http.StatusInternalServerError, }) return } granter, err := p.mm.User.Get(req.BotID) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot get user", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } badge, err := p.store.GetBadge(badgesmodel.BadgeID(req.BadgeID)) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot get badge", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } badgeType, err := p.store.GetType(badge.Type) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot get type", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } if !canGrantBadge(granter, p.badgeAdminUserID, badge, badgeType) { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot grant badge", Message: "you have no permissions to grant this badge", StatusCode: http.StatusUnauthorized, }) return } shouldNotify, err := p.store.GrantBadge(req.BadgeID, req.UserID, req.BotID, req.Reason) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot grant badge", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } if shouldNotify { u, err := p.mm.User.Get(req.UserID) if err == nil { p.notifyGrant(req.BadgeID, req.BotID, u, false, "", req.Reason) } } _, _ = w.Write([]byte(`{"sucess": true}`)) } func (p *Plugin) ensureBadges(w http.ResponseWriter, r *http.Request, pluginID string) { var req *badgesmodel.EnsureBadgesRequest err := json.NewDecoder(r.Body).Decode(&req) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot unmarshal request", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } if req == nil { p.writeAPIError(w, &APIErrorResponse{ ID: "missing request", Message: "Missing ensure request on request body", StatusCode: http.StatusInternalServerError, }) return } badges, err := p.store.EnsureBadges(req.Badges, pluginID, req.BotID) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot ensure", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } b, err := json.Marshal(badges) if err != nil { p.writeAPIError(w, &APIErrorResponse{ ID: "cannot marshal", Message: err.Error(), StatusCode: http.StatusInternalServerError, }) return } _, _ = w.Write(b) } func (p *Plugin) getBadgeSuggestions(w http.ResponseWriter, r *http.Request, actingUserID string) { out := []model.AutocompleteListItem{} u, err := p.mm.User.Get(actingUserID) if err != nil { p.mm.Log.Debug("Error getting user", "error", err) _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) return } bb, err := p.filterGrantBadges(u) if err != nil { p.mm.Log.Debug("Error getting suggestions", "error", err) _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) return } for _, b := range bb { s := model.AutocompleteListItem{ Item: string(b.ID), Hint: b.Name, HelpText: b.Description, } out = append(out, s) } _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) } func (p *Plugin) getEditBadgeSuggestions(w http.ResponseWriter, r *http.Request, actingUserID string) { out := []model.AutocompleteListItem{} u, err := p.mm.User.Get(actingUserID) if err != nil { p.mm.Log.Debug("Error getting user", "error", err) _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) return } bb, err := p.filterEditBadges(u) if err != nil { p.mm.Log.Debug("Error getting suggestions", "error", err) _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) return } for _, b := range bb { s := model.AutocompleteListItem{ Item: string(b.ID), Hint: b.Name, HelpText: b.Description, } out = append(out, s) } _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) } func (p *Plugin) getBadgeTypeSuggestions(w http.ResponseWriter, r *http.Request, actingUserID string) { out := []model.AutocompleteListItem{} u, err := p.mm.User.Get(actingUserID) if err != nil { p.mm.Log.Debug("Error getting user", "error", err) _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) return } types, err := p.filterCreateBadgeTypes(u) if err != nil { p.mm.Log.Debug("Error getting suggestions", "error", err) _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) return } for _, t := range types { s := model.AutocompleteListItem{ Item: string(t.ID), Hint: t.Name, } out = append(out, s) } _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) } func (p *Plugin) getEditBadgeTypeSuggestions(w http.ResponseWriter, r *http.Request, actingUserID string) { out := []model.AutocompleteListItem{} u, err := p.mm.User.Get(actingUserID) if err != nil { p.mm.Log.Debug("Error getting user", "error", err) _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) return } types, err := p.filterEditTypes(u) if err != nil { p.mm.Log.Debug("Error getting suggestions", "error", err) _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) return } for _, t := range types { s := model.AutocompleteListItem{ Item: string(t.ID), Hint: t.Name, } out = append(out, s) } _, _ = w.Write(model.AutocompleteStaticListItemsToJSON(out)) } func (p *Plugin) getUserBadges(w http.ResponseWriter, r *http.Request, actingUserID string) { userID, ok := mux.Vars(r)["userID"] if !ok { userID = actingUserID } badges, err := p.store.GetUserBadges(userID) if err != nil { p.mm.Log.Debug("Error getting the badges for user", "error", err, "user", userID) } b, _ := json.Marshal(badges) _, _ = w.Write(b) } func (p *Plugin) getBadgeDetails(w http.ResponseWriter, r *http.Request, actingUserID string) { badgeIDString, ok := mux.Vars(r)["badgeID"] if !ok { errMessage := "Missing badge id" p.mm.Log.Debug(errMessage) w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(errMessage)) return } badgeID := badgesmodel.BadgeID(badgeIDString) badge, err := p.store.GetBadgeDetails(badgeID) if err != nil { p.mm.Log.Debug("Cannot get badge details", "badgeID", badgeID, "error", err) } b, _ := json.Marshal(badge) _, _ = w.Write(b) } func (p *Plugin) getAllBadges(w http.ResponseWriter, r *http.Request, actingUserID string) { badge, err := p.store.GetAllBadges() if err != nil { p.mm.Log.Debug("Cannot get all badges", "error", err) } b, _ := json.Marshal(badge) _, _ = w.Write(b) } func (p *Plugin) extractUserMiddleWare(handler HTTPHandlerFuncWithUser, responseType ResponseType) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { userID := r.Header.Get("Mattermost-User-ID") if userID == "" { T := p.getT("ru") msg := T("badges.api.not_authorized", "Не авторизован") switch responseType { case ResponseTypeJSON: p.writeAPIError(w, &APIErrorResponse{ID: "", Message: msg, StatusCode: http.StatusUnauthorized}) case ResponseTypePlain: http.Error(w, msg, http.StatusUnauthorized) case ResponseTypeDialog: dialogError(w, msg, nil) default: p.mm.Log.Error("Unknown ResponseType detected") } return } handler(w, r, userID) } } func (p *Plugin) withRecovery(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if x := recover(); x != nil { p.mm.Log.Error("Recovered from a panic", "url", r.URL.String(), "error", x, "stack", string(debug.Stack())) } }() next.ServeHTTP(w, r) }) } func checkPluginRequest(next HTTPHandlerFuncWithUser) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // All other plugins are allowed pluginID := r.Header.Get("Mattermost-Plugin-ID") if pluginID == "" { http.Error(w, "Not authorized", http.StatusUnauthorized) return } next(w, r, pluginID) } } func (p *Plugin) writeAPIError(w http.ResponseWriter, apiErr *APIErrorResponse) { b, err := json.Marshal(apiErr) if err != nil { p.mm.Log.Warn("Failed to marshal API error", "error", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(apiErr.StatusCode) _, err = w.Write(b) if err != nil { p.mm.Log.Warn("Failed to write JSON response", "error", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } } func (p *Plugin) getPluginURL() string { urlP := p.mm.Configuration.GetConfig().ServiceSettings.SiteURL url := "/" if urlP != nil { url = *urlP } if url[len(url)-1] == '/' { url = url[0 : len(url)-1] } return url + "/plugins/" + manifest.Id } func (p *Plugin) getDialogURL() string { return p.getPluginURL() + DialogPath }