1944 lines
60 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"encoding/json"
"errors"
"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"`
AllowlistCanCreate string `json:"allowlist_can_create"`
AllowlistCanGrant string `json:"allowlist_can_grant"`
ChannelID string `json:"channel_id"`
}
type UpdateTypeRequest struct {
ID string `json:"id"`
Name string `json:"name"`
EveryoneCanCreate bool `json:"everyone_can_create"`
EveryoneCanGrant bool `json:"everyone_can_grant"`
AllowlistCanCreate string `json:"allowlist_can_create"`
AllowlistCanGrant string `json:"allowlist_can_grant"`
}
type GrantBadgeAPIRequest struct {
BadgeID string `json:"badge_id"`
UserID string `json:"user_id"`
Reason string `json:"reason"`
NotifyHere bool `json:"notify_here"`
ChannelID string `json:"channel_id"`
}
type SubscriptionAPIRequest struct {
TypeID string `json:"type_id"`
ChannelID string `json:"channel_id"`
}
type RevokeOwnershipRequest struct {
BadgeID string `json:"badge_id"`
UserID string `json:"user_id"`
Time string `json:"time"`
}
type TypeWithBadgeCount struct {
*badgesmodel.BadgeTypeDefinition
BadgeCount int `json:"badge_count"`
CreatedByUsername string `json:"created_by_username"`
AllowlistCanCreate string `json:"allowlist_can_create"`
AllowlistCanGrant string `json:"allowlist_can_grant"`
}
type GetTypesResponse struct {
Types []TypeWithBadgeCount `json:"types"`
CanCreateType bool `json:"can_create_type"`
CanEditType bool `json:"can_edit_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("/updateType", p.extractUserMiddleWare(p.apiUpdateType, 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)
apiRouter.HandleFunc("/grantBadge", p.extractUserMiddleWare(p.apiGrantBadge, ResponseTypeJSON)).Methods(http.MethodPost)
apiRouter.HandleFunc("/revokeOwnership", p.extractUserMiddleWare(p.apiRevokeOwnership, ResponseTypeJSON)).Methods(http.MethodPost)
apiRouter.HandleFunc("/getChannelSubscriptions/{channelID}", p.extractUserMiddleWare(p.apiGetChannelSubscriptions, ResponseTypeJSON)).Methods(http.MethodGet)
apiRouter.HandleFunc("/createSubscription", p.extractUserMiddleWare(p.apiCreateSubscription, ResponseTypeJSON)).Methods(http.MethodPost)
apiRouter.HandleFunc("/deleteSubscription", p.extractUserMiddleWare(p.apiDeleteSubscription, ResponseTypeJSON)).Methods(http.MethodPost)
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 {
createdByUsername := t.CreatedBy
if creator, appErr := p.mm.User.Get(t.CreatedBy); appErr == nil {
createdByUsername = creator.Username
}
result[i] = TypeWithBadgeCount{
BadgeTypeDefinition: t,
BadgeCount: badgeCountByType[t.ID],
CreatedByUsername: createdByUsername,
AllowlistCanCreate: p.resolveUserIDList(t.CanCreate.AllowList),
AllowlistCanGrant: p.resolveUserIDList(t.CanGrant.AllowList),
}
}
resp := GetTypesResponse{
Types: result,
CanCreateType: canCreateType(u, p.badgeAdminUserIDs, false),
CanEditType: p.badgeAdminUserIDs[u.Id] || u.IsSystemAdmin(),
}
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.badgeAdminUserIDs, 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.badgeAdminUserIDs, 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
if req.AllowlistCanCreate != "" {
allowList, aErr := p.resolveUsernameList(req.AllowlistCanCreate)
if aErr != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_request", Message: aErr.Error(), StatusCode: http.StatusBadRequest,
})
return
}
toCreate.CanCreate.AllowList = allowList
}
if req.AllowlistCanGrant != "" {
allowList, aErr := p.resolveUsernameList(req.AllowlistCanGrant)
if aErr != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_request", Message: aErr.Error(), StatusCode: http.StatusBadRequest,
})
return
}
toCreate.CanGrant.AllowList = allowList
}
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
}
badgeType, err := p.store.GetType(badge.Type)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "type_not_found", Message: "Badge type not found", StatusCode: http.StatusInternalServerError,
})
return
}
if !canEditBadge(user, p.badgeAdminUserIDs, badge, badgeType) {
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) apiUpdateType(w http.ResponseWriter, r *http.Request, userID string) {
var req UpdateTypeRequest
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
}
originalType, err := p.store.GetType(badgesmodel.BadgeType(req.ID))
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "type_not_found", Message: "Badge type not found", StatusCode: http.StatusNotFound,
})
return
}
if !canEditType(user, p.badgeAdminUserIDs, originalType) {
p.writeAPIError(w, &APIErrorResponse{
ID: "no_permission", Message: "No permission to edit this type", StatusCode: http.StatusForbidden,
})
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_name", Message: "Name is required", StatusCode: http.StatusBadRequest,
})
return
}
originalType.Name = req.Name
originalType.CanCreate.Everyone = req.EveryoneCanCreate
originalType.CanGrant.Everyone = req.EveryoneCanGrant
createAllowList, aErr := p.resolveUsernameList(req.AllowlistCanCreate)
if aErr != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_request", Message: aErr.Error(), StatusCode: http.StatusBadRequest,
})
return
}
originalType.CanCreate.AllowList = createAllowList
grantAllowList, aErr := p.resolveUsernameList(req.AllowlistCanGrant)
if aErr != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_request", Message: aErr.Error(), StatusCode: http.StatusBadRequest,
})
return
}
originalType.CanGrant.AllowList = grantAllowList
if err := p.store.UpdateType(originalType); err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "cannot_update_type", Message: err.Error(), StatusCode: http.StatusInternalServerError,
})
return
}
b, _ := json.Marshal(originalType)
_, _ = 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
}
badgeType, err := p.store.GetType(badge.Type)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "type_not_found", Message: "Badge type not found", StatusCode: http.StatusInternalServerError,
})
return
}
if !canEditBadge(user, p.badgeAdminUserIDs, badge, badgeType) {
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.badgeAdminUserIDs, 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.badgeAdminUserIDs, 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.badgeAdminUserIDs, 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.badgeAdminUserIDs, 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.badgeAdminUserIDs, 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)
bt, err := p.store.GetType(b.Type)
if err != nil {
dialogError(w, T("badges.api.cannot_get_type", "Не удалось получить тип значка"), nil)
return
}
if !canEditBadge(u, p.badgeAdminUserIDs, b, bt) {
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
}
originalBadgeType, err := p.store.GetType(originalBadge.Type)
if err != nil {
dialogError(w, T("badges.api.cannot_get_type", "Не удалось получить тип значка"), nil)
return
}
if !canEditBadge(u, p.badgeAdminUserIDs, originalBadge, originalBadgeType) {
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.badgeAdminUserIDs, 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 == errAlreadyOwned {
dialogError(w, T("badges.error.already_owned", "Это достижение уже выдано этому пользователю"), nil)
return
}
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.badgeAdminUserIDs, 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.badgeAdminUserIDs, 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.badgeAdminUserIDs, 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 == errAlreadyOwned {
p.writeAPIError(w, &APIErrorResponse{
ID: "already_owned", Message: "This badge is already owned by this user", StatusCode: http.StatusConflict,
})
return
}
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)
}
for _, ub := range badges {
p.sanitizeBadgeEmoji(&ub.Badge)
}
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)
}
type BadgeDetailsResponse struct {
*badgesmodel.BadgeDetails
CanEdit bool `json:"can_edit"`
}
resp := BadgeDetailsResponse{BadgeDetails: badge}
if badge != nil {
p.sanitizeBadgeEmoji(&badge.Badge)
actingUser, userErr := p.mm.User.Get(actingUserID)
if userErr == nil {
bt, typeErr := p.store.GetType(badge.Type)
if typeErr == nil {
resp.CanEdit = canEditBadge(actingUser, p.badgeAdminUserIDs, &badge.Badge, bt)
}
}
}
b, _ := json.Marshal(resp)
_, _ = w.Write(b)
}
func (p *Plugin) getAllBadges(w http.ResponseWriter, r *http.Request, actingUserID string) {
badges, err := p.store.GetAllBadges()
if err != nil {
p.mm.Log.Debug("Cannot get all badges", "error", err)
}
for _, ab := range badges {
p.sanitizeBadgeEmoji(&ab.Badge)
}
b, _ := json.Marshal(badges)
_, _ = 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
}
func (p *Plugin) apiGrantBadge(w http.ResponseWriter, r *http.Request, userID string) {
var req GrantBadgeAPIRequest
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
}
req.BadgeID = strings.TrimSpace(req.BadgeID)
req.UserID = strings.TrimSpace(req.UserID)
if req.BadgeID == "" {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_badge_id", Message: "Badge ID is required", StatusCode: http.StatusBadRequest,
})
return
}
if req.UserID == "" {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_user_id", Message: "User ID is required", StatusCode: http.StatusBadRequest,
})
return
}
badge, err := p.store.GetBadge(badgesmodel.BadgeID(req.BadgeID))
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "badge_not_found", Message: "Badge not found", StatusCode: http.StatusNotFound,
})
return
}
granter, 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
}
badgeType, err := p.store.GetType(badge.Type)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "type_not_found", Message: "Badge type not found", StatusCode: http.StatusInternalServerError,
})
return
}
if !canGrantBadge(granter, p.badgeAdminUserIDs, badge, badgeType) {
p.writeAPIError(w, &APIErrorResponse{
ID: "no_permission_grant", Message: "No permission to grant this badge", StatusCode: http.StatusForbidden,
})
return
}
grantToUser, err := p.mm.User.Get(req.UserID)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "user_not_found", Message: "User not found", StatusCode: http.StatusNotFound,
})
return
}
shouldNotify, err := p.store.GrantBadge(badgesmodel.BadgeID(req.BadgeID), req.UserID, userID, req.Reason)
if errors.Is(err, errAlreadyOwned) {
p.writeAPIError(w, &APIErrorResponse{
ID: "already_owned", Message: "This badge is already owned by this user", StatusCode: http.StatusConflict,
})
return
}
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "cannot_grant_badge", Message: err.Error(), StatusCode: http.StatusInternalServerError,
})
return
}
if shouldNotify {
channelID := req.ChannelID
p.notifyGrant(badgesmodel.BadgeID(req.BadgeID), userID, grantToUser, req.NotifyHere, channelID, req.Reason)
}
resp := map[string]string{"status": "ok"}
b, _ := json.Marshal(resp)
_, _ = w.Write(b)
}
func (p *Plugin) apiCreateSubscription(w http.ResponseWriter, r *http.Request, userID string) {
var req SubscriptionAPIRequest
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
}
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
}
if !canCreateSubscription(u, p.badgeAdminUserIDs, req.ChannelID) {
p.writeAPIError(w, &APIErrorResponse{
ID: "no_permission_subscription", Message: "No permission to manage subscriptions", StatusCode: http.StatusForbidden,
})
return
}
req.TypeID = strings.TrimSpace(req.TypeID)
if req.TypeID == "" {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_type_id", Message: "Type ID is required", StatusCode: http.StatusBadRequest,
})
return
}
err = p.store.AddSubscription(badgesmodel.BadgeType(req.TypeID), req.ChannelID)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "cannot_create_subscription", Message: err.Error(), StatusCode: http.StatusInternalServerError,
})
return
}
T := p.getT(u.Locale)
p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID,
ChannelId: req.ChannelID,
Message: T("badges.api.subscription_added", "Подписка добавлена"),
})
resp := map[string]string{"status": "ok"}
b, _ := json.Marshal(resp)
_, _ = w.Write(b)
}
func (p *Plugin) apiDeleteSubscription(w http.ResponseWriter, r *http.Request, userID string) {
var req SubscriptionAPIRequest
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
}
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
}
if !canCreateSubscription(u, p.badgeAdminUserIDs, req.ChannelID) {
p.writeAPIError(w, &APIErrorResponse{
ID: "no_permission_subscription", Message: "No permission to manage subscriptions", StatusCode: http.StatusForbidden,
})
return
}
req.TypeID = strings.TrimSpace(req.TypeID)
if req.TypeID == "" {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_type_id", Message: "Type ID is required", StatusCode: http.StatusBadRequest,
})
return
}
err = p.store.RemoveSubscriptions(badgesmodel.BadgeType(req.TypeID), req.ChannelID)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "cannot_delete_subscription", Message: err.Error(), StatusCode: http.StatusInternalServerError,
})
return
}
T := p.getT(u.Locale)
p.mm.Post.SendEphemeralPost(userID, &model.Post{
UserId: p.BotUserID,
ChannelId: req.ChannelID,
Message: T("badges.api.subscription_removed", "Подписка удалена"),
})
resp := map[string]string{"status": "ok"}
b, _ := json.Marshal(resp)
_, _ = w.Write(b)
}
func (p *Plugin) apiGetChannelSubscriptions(w http.ResponseWriter, r *http.Request, userID string) {
channelID := mux.Vars(r)["channelID"]
if channelID == "" {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_request", Message: "Channel ID is required", StatusCode: http.StatusBadRequest,
})
return
}
types, err := p.store.GetChannelSubscriptions(channelID)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "cannot_get_types", Message: err.Error(), StatusCode: http.StatusInternalServerError,
})
return
}
b, _ := json.Marshal(types)
_, _ = w.Write(b)
}
func (p *Plugin) apiRevokeOwnership(w http.ResponseWriter, r *http.Request, userID string) {
var req RevokeOwnershipRequest
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
}
req.BadgeID = strings.TrimSpace(req.BadgeID)
req.UserID = strings.TrimSpace(req.UserID)
req.Time = strings.TrimSpace(req.Time)
if req.BadgeID == "" || req.UserID == "" || req.Time == "" {
p.writeAPIError(w, &APIErrorResponse{
ID: "invalid_request", Message: "badge_id, user_id and time are required", StatusCode: http.StatusBadRequest,
})
return
}
ownership, err := p.store.FindOwnership(badgesmodel.BadgeID(req.BadgeID), req.UserID, req.Time)
if err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "ownership_not_found", Message: "Ownership not found", StatusCode: http.StatusNotFound,
})
return
}
isAdmin := p.badgeAdminUserIDs[userID]
if ownership.GrantedBy != userID && !isAdmin {
p.writeAPIError(w, &APIErrorResponse{
ID: "no_permission_revoke", Message: "No permission to revoke this ownership", StatusCode: http.StatusForbidden,
})
return
}
if err := p.store.RevokeOwnership(badgesmodel.BadgeID(req.BadgeID), req.UserID, req.Time); err != nil {
p.writeAPIError(w, &APIErrorResponse{
ID: "cannot_revoke", Message: err.Error(), StatusCode: http.StatusInternalServerError,
})
return
}
resp := map[string]string{"status": "ok"}
b, _ := json.Marshal(resp)
_, _ = w.Write(b)
}