PLT-3143 Added serverside code for custom Emoji (#3311)
* Added model objects for emoji * Added database tables for emoji * Added settings for custom emoji * Added serverside APIs and unit tests for custom emoji * Added additional validation to catch duplicate emoji names earlier on * Added additional validation to prevent users from adding emoji as another user
Этот коммит содержится в:
коммит произвёл
Christopher Speller
родитель
661f221727
Коммит
a0cc913b85
@@ -46,6 +46,8 @@ type Routes struct {
|
||||
License *mux.Router // 'api/v3/license'
|
||||
|
||||
Public *mux.Router // 'api/v3/public'
|
||||
|
||||
Emoji *mux.Router // 'api/v3/emoji'
|
||||
}
|
||||
|
||||
var BaseRoutes *Routes
|
||||
@@ -72,6 +74,7 @@ func InitApi() {
|
||||
BaseRoutes.Preferences = BaseRoutes.ApiRoot.PathPrefix("/preferences").Subrouter()
|
||||
BaseRoutes.License = BaseRoutes.ApiRoot.PathPrefix("/license").Subrouter()
|
||||
BaseRoutes.Public = BaseRoutes.ApiRoot.PathPrefix("/public").Subrouter()
|
||||
BaseRoutes.Emoji = BaseRoutes.ApiRoot.PathPrefix("/emoji").Subrouter()
|
||||
|
||||
InitUser()
|
||||
InitTeam()
|
||||
@@ -86,6 +89,7 @@ func InitApi() {
|
||||
InitWebhook()
|
||||
InitPreference()
|
||||
InitLicense()
|
||||
InitEmoji()
|
||||
|
||||
// 404 on any api route before web.go has a chance to serve it
|
||||
Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))
|
||||
|
||||
252
api/emoji.go
Обычный файл
252
api/emoji.go
Обычный файл
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxEmojiFileSize = 64 * 1024 // 64 KB
|
||||
MaxEmojiWidth = 128
|
||||
MaxEmojiHeight = 128
|
||||
)
|
||||
|
||||
func InitEmoji() {
|
||||
l4g.Debug(utils.T("api.emoji.init.debug"))
|
||||
|
||||
BaseRoutes.Emoji.Handle("/list", ApiUserRequired(getEmoji)).Methods("GET")
|
||||
BaseRoutes.Emoji.Handle("/create", ApiUserRequired(createEmoji)).Methods("POST")
|
||||
BaseRoutes.Emoji.Handle("/delete", ApiUserRequired(deleteEmoji)).Methods("POST")
|
||||
BaseRoutes.Emoji.Handle("/{id:[A-Za-z0-9_]+}", ApiUserRequired(getEmojiImage)).Methods("GET")
|
||||
}
|
||||
|
||||
func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewLocAppError("getEmoji", "api.emoji.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Emoji().GetAll(); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
emoji := result.Data.([]*model.Emoji)
|
||||
w.Write([]byte(model.EmojiListToJson(emoji)))
|
||||
}
|
||||
}
|
||||
|
||||
func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewLocAppError("createEmoji", "api.emoji.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
if !(*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation == model.RESTRICT_EMOJI_CREATION_ALL || c.IsSystemAdmin()) {
|
||||
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.permissions.app_error", nil, "user_id="+c.Session.UserId)
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return
|
||||
}
|
||||
|
||||
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
||||
c.Err = model.NewLocAppError("createEmoji", "api.emoji.storage.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > MaxEmojiFileSize {
|
||||
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.too_large.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusRequestEntityTooLarge
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseMultipartForm(MaxEmojiFileSize); err != nil {
|
||||
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.parse.app_error", nil, err.Error())
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
|
||||
m := r.MultipartForm
|
||||
props := m.Value
|
||||
|
||||
emoji := model.EmojiFromJson(strings.NewReader(props["emoji"][0]))
|
||||
if emoji == nil {
|
||||
c.SetInvalidParam("createEmoji", "emoji")
|
||||
return
|
||||
}
|
||||
|
||||
// wipe the emoji id so that existing emojis can't get overwritten
|
||||
emoji.Id = ""
|
||||
|
||||
// do our best to validate the emoji before committing anything to the DB so that we don't have to clean up
|
||||
// orphaned files left over when validation fails later on
|
||||
emoji.PreSave()
|
||||
if err := emoji.IsValid(); err != nil {
|
||||
c.Err = err
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
|
||||
if emoji.CreatorId != c.Session.UserId {
|
||||
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Emoji().GetByName(emoji.Name); result.Err == nil && result.Data != nil {
|
||||
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusBadRequest
|
||||
return
|
||||
}
|
||||
|
||||
if imageData := m.File["image"]; len(imageData) == 0 {
|
||||
c.SetInvalidParam("createEmoji", "image")
|
||||
return
|
||||
} else if err := uploadEmojiImage(emoji.Id, imageData[0]); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Emoji().Save(emoji); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
w.Write([]byte(result.Data.(*model.Emoji).ToJson()))
|
||||
}
|
||||
}
|
||||
|
||||
func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError {
|
||||
file, err := imageData.Open()
|
||||
if err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.open.app_error", nil, "")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
io.Copy(buf, file)
|
||||
|
||||
// make sure the file is an image and is within the required dimensions
|
||||
if config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes())); err != nil {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.image.app_error", nil, err.Error())
|
||||
} else if config.Width > MaxEmojiWidth || config.Height > MaxEmojiHeight {
|
||||
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.app_error", nil, "")
|
||||
}
|
||||
|
||||
if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewLocAppError("deleteEmoji", "api.emoji.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
||||
c.Err = model.NewLocAppError("deleteImage", "api.emoji.storage.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
props := model.MapFromJson(r.Body)
|
||||
|
||||
id := props["id"]
|
||||
if len(id) == 0 {
|
||||
c.SetInvalidParam("deleteEmoji", "id")
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Emoji().Get(id); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
if c.Session.UserId != result.Data.(*model.Emoji).CreatorId && !c.IsSystemAdmin() {
|
||||
c.Err = model.NewLocAppError("deleteEmoji", "api.emoji.delete.permissions.app_error", nil, "user_id="+c.Session.UserId)
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := (<-Srv.Store.Emoji().Delete(id, model.GetMillis())).Err; err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
go deleteEmojiImage(id)
|
||||
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func deleteEmojiImage(id string) {
|
||||
if err := MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
|
||||
l4g.Error("Failed to rename image when deleting emoji %v", id)
|
||||
}
|
||||
}
|
||||
|
||||
func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !*utils.Cfg.ServiceSettings.EnableCustomEmoji {
|
||||
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
if len(utils.Cfg.FileSettings.DriverName) == 0 {
|
||||
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.storage.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
id := params["id"]
|
||||
if len(id) == 0 {
|
||||
c.SetInvalidParam("getEmojiImage", "id")
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Emoji().Get(id); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
var img []byte
|
||||
|
||||
if data, err := ReadFile(getEmojiImagePath(id)); err != nil {
|
||||
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, err.Error())
|
||||
return
|
||||
} else {
|
||||
img = data
|
||||
}
|
||||
|
||||
if _, imageType, err := image.DecodeConfig(bytes.NewReader(img)); err != nil {
|
||||
model.NewLocAppError("getEmojiImage", "api.emoji.get_image.decode.app_error", nil, err.Error())
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "image/"+imageType)
|
||||
}
|
||||
|
||||
w.Write(img)
|
||||
}
|
||||
}
|
||||
|
||||
func getEmojiImagePath(id string) string {
|
||||
return "emoji/" + id + "/image"
|
||||
}
|
||||
445
api/emoji_test.go
Обычный файл
445
api/emoji_test.go
Обычный файл
@@ -0,0 +1,445 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/store"
|
||||
"github.com/mattermost/platform/utils"
|
||||
)
|
||||
|
||||
func TestGetEmoji(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
|
||||
emojis := []*model.Emoji{
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
for i, emoji := range emojis {
|
||||
emojis[i] = store.Must(Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
|
||||
}
|
||||
defer func() {
|
||||
for _, emoji := range emojis {
|
||||
store.Must(Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
|
||||
}
|
||||
}()
|
||||
|
||||
if returnedEmojis, err := Client.ListEmoji(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
for _, emoji := range emojis {
|
||||
found := false
|
||||
|
||||
for _, savedEmoji := range returnedEmojis {
|
||||
if emoji.Id == savedEmoji.Id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatalf("failed to get emoji with id %v", emoji.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleted := &model.Emoji{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
DeleteAt: 1,
|
||||
}
|
||||
deleted = store.Must(Srv.Store.Emoji().Save(deleted)).(*model.Emoji)
|
||||
|
||||
if returnedEmojis, err := Client.ListEmoji(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
found := false
|
||||
|
||||
for _, savedEmoji := range returnedEmojis {
|
||||
if deleted.Id == savedEmoji.Id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
t.Fatalf("souldn't have gotten deleted emoji %v", deleted.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateEmoji(t *testing.T) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
Client := th.BasicClient
|
||||
|
||||
EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji
|
||||
RestrictCustomEmojiCreation := *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation
|
||||
defer func() {
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji
|
||||
*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = RestrictCustomEmojiCreation
|
||||
}()
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = false
|
||||
*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ALL
|
||||
|
||||
emoji := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
// try to create an emoji when they're disabled
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji when they're disabled")
|
||||
}
|
||||
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = true
|
||||
|
||||
// try to create a valid gif emoji when they're enabled
|
||||
if emojiResult, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
emoji = emojiResult
|
||||
}
|
||||
|
||||
// try to create an emoji with a duplicate name
|
||||
emoji2 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: emoji.Name,
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji2, createTestGif(t, 10, 10), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji with a duplicate name")
|
||||
}
|
||||
|
||||
Client.MustGeneric(Client.DeleteEmoji(emoji.Id))
|
||||
|
||||
// try to create a valid animated gif emoji
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if emojiResult, err := Client.CreateEmoji(emoji, createTestAnimatedGif(t, 10, 10, 10), "image.gif"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
emoji = emojiResult
|
||||
}
|
||||
Client.MustGeneric(Client.DeleteEmoji(emoji.Id))
|
||||
|
||||
// try to create a valid jpeg emoji
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if emojiResult, err := Client.CreateEmoji(emoji, createTestJpeg(t, 10, 10), "image.jpeg"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
emoji = emojiResult
|
||||
}
|
||||
Client.MustGeneric(Client.DeleteEmoji(emoji.Id))
|
||||
|
||||
// try to create a valid png emoji
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if emojiResult, err := Client.CreateEmoji(emoji, createTestPng(t, 10, 10), "image.png"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
emoji = emojiResult
|
||||
}
|
||||
Client.MustGeneric(Client.DeleteEmoji(emoji.Id))
|
||||
|
||||
// try to create an emoji that's too wide
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 1000, 10), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji that's too wide")
|
||||
}
|
||||
|
||||
// try to create an emoji that's too tall
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 1000), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji that's too tall")
|
||||
}
|
||||
|
||||
// try to create an emoji that's too large
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, createTestAnimatedGif(t, 100, 100, 4000), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji that's too large")
|
||||
}
|
||||
|
||||
// try to create an emoji with data that isn't an image
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, make([]byte, 100, 100), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji with non-image data")
|
||||
}
|
||||
|
||||
// try to create an emoji as another user
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser2.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji as another user")
|
||||
}
|
||||
|
||||
*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ADMIN
|
||||
|
||||
// try to create an emoji when only system admins are allowed to create them
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if _, err := Client.CreateEmoji(emoji, createTestGif(t, 10, 10), "image.gif"); err == nil {
|
||||
t.Fatal("shouldn't be able to create an emoji when not a system admin")
|
||||
}
|
||||
|
||||
emoji = &model.Emoji{
|
||||
CreatorId: th.SystemAdminUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
if emojiResult, err := th.SystemAdminClient.CreateEmoji(emoji, createTestPng(t, 10, 10), "image.png"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
emoji = emojiResult
|
||||
}
|
||||
th.SystemAdminClient.MustGeneric(th.SystemAdminClient.DeleteEmoji(emoji.Id))
|
||||
}
|
||||
|
||||
func TestDeleteEmoji(t *testing.T) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
Client := th.BasicClient
|
||||
|
||||
EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji
|
||||
defer func() {
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji
|
||||
}()
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = false
|
||||
|
||||
emoji1 := createTestEmoji(t, &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}, createTestGif(t, 10, 10))
|
||||
|
||||
if _, err := Client.DeleteEmoji(emoji1.Id); err == nil {
|
||||
t.Fatal("shouldn't have been able to delete an emoji when they're disabled")
|
||||
}
|
||||
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = true
|
||||
|
||||
if deleted, err := Client.DeleteEmoji(emoji1.Id); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !deleted {
|
||||
t.Fatalf("should be able to delete your own emoji %v", emoji1.Id)
|
||||
}
|
||||
|
||||
if _, err := Client.DeleteEmoji(emoji1.Id); err == nil {
|
||||
t.Fatal("shouldn't be able to delete an already-deleted emoji")
|
||||
}
|
||||
|
||||
emoji2 := createTestEmoji(t, &model.Emoji{
|
||||
CreatorId: th.BasicUser2.Id,
|
||||
Name: model.NewId(),
|
||||
}, createTestGif(t, 10, 10))
|
||||
|
||||
if _, err := Client.DeleteEmoji(emoji2.Id); err == nil {
|
||||
t.Fatal("shouldn't be able to delete another user's emoji")
|
||||
}
|
||||
|
||||
if deleted, err := th.SystemAdminClient.DeleteEmoji(emoji2.Id); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if !deleted {
|
||||
t.Fatalf("system admin should be able to delete anyone's emoji %v", emoji2.Id)
|
||||
}
|
||||
}
|
||||
|
||||
func createTestGif(t *testing.T, width int, height int) []byte {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if err := gif.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil); err != nil {
|
||||
t.Fatalf("failed to create gif: %v", err.Error())
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func createTestAnimatedGif(t *testing.T, width int, height int, frames int) []byte {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
img := gif.GIF{
|
||||
Image: make([]*image.Paletted, frames, frames),
|
||||
Delay: make([]int, frames, frames),
|
||||
}
|
||||
for i := 0; i < frames; i++ {
|
||||
img.Image[i] = image.NewPaletted(image.Rect(0, 0, width, height), color.Palette{color.Black})
|
||||
img.Delay[i] = 0
|
||||
}
|
||||
if err := gif.EncodeAll(&buffer, &img); err != nil {
|
||||
t.Fatalf("failed to create animated gif: %v", err.Error())
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func createTestJpeg(t *testing.T, width int, height int) []byte {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if err := jpeg.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height)), nil); err != nil {
|
||||
t.Fatalf("failed to create jpeg: %v", err.Error())
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func createTestPng(t *testing.T, width int, height int) []byte {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
if err := png.Encode(&buffer, image.NewRGBA(image.Rect(0, 0, width, height))); err != nil {
|
||||
t.Fatalf("failed to create png: %v", err.Error())
|
||||
}
|
||||
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func createTestEmoji(t *testing.T, emoji *model.Emoji, imageData []byte) *model.Emoji {
|
||||
emoji = store.Must(Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
|
||||
|
||||
if err := WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil {
|
||||
store.Must(Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
|
||||
t.Fatalf("failed to write image: %v", err.Error())
|
||||
}
|
||||
|
||||
return emoji
|
||||
}
|
||||
|
||||
func TestGetEmojiImage(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
|
||||
EnableCustomEmoji := *utils.Cfg.ServiceSettings.EnableCustomEmoji
|
||||
RestrictCustomEmojiCreation := *utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation
|
||||
defer func() {
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = EnableCustomEmoji
|
||||
*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = RestrictCustomEmojiCreation
|
||||
}()
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = true
|
||||
*utils.Cfg.ServiceSettings.RestrictCustomEmojiCreation = model.RESTRICT_EMOJI_CREATION_ALL
|
||||
|
||||
emoji1 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji1 = Client.MustGeneric(Client.CreateEmoji(emoji1, createTestGif(t, 10, 10), "image.gif")).(*model.Emoji)
|
||||
defer func() { Client.MustGeneric(Client.DeleteEmoji(emoji1.Id)) }()
|
||||
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = false
|
||||
|
||||
if _, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji1.Id), "", ""); err == nil {
|
||||
t.Fatal("should've failed to get emoji image when disabled")
|
||||
}
|
||||
|
||||
*utils.Cfg.ServiceSettings.EnableCustomEmoji = true
|
||||
|
||||
if resp, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji1.Id), "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.Header.Get("Content-Type") != "image/gif" {
|
||||
t.Fatal("should've received a gif")
|
||||
} else if _, imageType, err := image.DecodeConfig(resp.Body); err != nil {
|
||||
t.Fatalf("unable to identify received image: %v", err.Error())
|
||||
} else if imageType != "gif" {
|
||||
t.Fatal("should've received gif data")
|
||||
}
|
||||
|
||||
emoji2 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji2 = Client.MustGeneric(Client.CreateEmoji(emoji2, createTestAnimatedGif(t, 10, 10, 10), "image.gif")).(*model.Emoji)
|
||||
defer func() { Client.MustGeneric(Client.DeleteEmoji(emoji2.Id)) }()
|
||||
|
||||
if resp, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji2.Id), "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.Header.Get("Content-Type") != "image/gif" {
|
||||
t.Fatal("should've received a gif")
|
||||
} else if _, imageType, err := image.DecodeConfig(resp.Body); err != nil {
|
||||
t.Fatalf("unable to identify received image: %v", err.Error())
|
||||
} else if imageType != "gif" {
|
||||
t.Fatal("should've received gif data")
|
||||
}
|
||||
|
||||
emoji3 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji3 = Client.MustGeneric(Client.CreateEmoji(emoji3, createTestJpeg(t, 10, 10), "image.jpeg")).(*model.Emoji)
|
||||
defer func() { Client.MustGeneric(Client.DeleteEmoji(emoji3.Id)) }()
|
||||
|
||||
if resp, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji3.Id), "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.Header.Get("Content-Type") != "image/jpeg" {
|
||||
t.Fatal("should've received a jpeg")
|
||||
} else if _, imageType, err := image.DecodeConfig(resp.Body); err != nil {
|
||||
t.Fatalf("unable to identify received image: %v", err.Error())
|
||||
} else if imageType != "jpeg" {
|
||||
t.Fatal("should've received jpeg data")
|
||||
}
|
||||
|
||||
emoji4 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji4 = Client.MustGeneric(Client.CreateEmoji(emoji4, createTestPng(t, 10, 10), "image.png")).(*model.Emoji)
|
||||
defer func() { Client.MustGeneric(Client.DeleteEmoji(emoji4.Id)) }()
|
||||
|
||||
if resp, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji4.Id), "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if resp.Header.Get("Content-Type") != "image/png" {
|
||||
t.Fatal("should've received a png")
|
||||
} else if _, imageType, err := image.DecodeConfig(resp.Body); err != nil {
|
||||
t.Fatalf("unable to identify received image: %v", err.Error())
|
||||
} else if imageType != "png" {
|
||||
t.Fatal("should've received png data")
|
||||
}
|
||||
|
||||
emoji5 := &model.Emoji{
|
||||
CreatorId: th.BasicUser.Id,
|
||||
Name: model.NewId(),
|
||||
}
|
||||
emoji5 = Client.MustGeneric(Client.CreateEmoji(emoji5, createTestPng(t, 10, 10), "image.png")).(*model.Emoji)
|
||||
Client.MustGeneric(Client.DeleteEmoji(emoji5.Id))
|
||||
|
||||
if _, err := Client.DoApiGet(Client.GetCustomEmojiImageUrl(emoji5.Id), "", ""); err == nil {
|
||||
t.Fatal("should've failed to get image for deleted emoji")
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@
|
||||
"SessionCacheInMinutes": 10,
|
||||
"WebsocketSecurePort": 443,
|
||||
"WebsocketPort": 80,
|
||||
"WebserverMode": "regular"
|
||||
"WebserverMode": "regular",
|
||||
"EnableCustomEmoji": true,
|
||||
"RestrictCustomEmojiCreation": "all"
|
||||
},
|
||||
"TeamSettings": {
|
||||
"SiteName": "Mattermost",
|
||||
|
||||
112
i18n/en.json
112
i18n/en.json
@@ -51,10 +51,6 @@
|
||||
"id": "system.message.name",
|
||||
"translation": "System"
|
||||
},
|
||||
{
|
||||
"id": "api.general.init.debug",
|
||||
"translation": "Initializing general api routes"
|
||||
},
|
||||
{
|
||||
"id": "api.admin.file_read_error",
|
||||
"translation": "Error reading log file"
|
||||
@@ -579,6 +575,54 @@
|
||||
"id": "api.context.unknown.app_error",
|
||||
"translation": "An unknown error has occurred. Please contact support."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.create.duplicate.app_error",
|
||||
"translation": "Unable to create emoji. Another emoji with the same name already exists."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.create.parse.app_error",
|
||||
"translation": "Unable to create emoji. Image exceeds maximum file size."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.create.permissions.app_error",
|
||||
"translation": "Inappropriate permissions to create emoji."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.create.too_large.app_error",
|
||||
"translation": "Unable to create emoji. Could not understand request."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.delete.permissions.app_error",
|
||||
"translation": "Inappropriate permissions to delete emoji."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.disabled.app_error",
|
||||
"translation": "Custom emoji have been disabled by the system admin."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.get_image.decode.app_error",
|
||||
"translation": "Unable to decode image file for emoji."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.get_image.read.app_error",
|
||||
"translation": "Unable to read image file for emoji."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.storage.app_error",
|
||||
"translation": "File storage not configured properly. Please configure for either S3 or local server file storage."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.upload.image.app_error",
|
||||
"translation": "Unable to create emoji. File must be a PNG, JPEG, or GIF."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.upload.large_image.app_error",
|
||||
"translation": "Unable to create emoji. Image exceeds maximum dimensions."
|
||||
},
|
||||
{
|
||||
"id": "api.emoji.init.debug",
|
||||
"translation": "Initializing emoji api routes"
|
||||
},
|
||||
{
|
||||
"id": "api.export.json.app_error",
|
||||
"translation": "Unable to convert to json"
|
||||
@@ -743,6 +787,10 @@
|
||||
"id": "api.file.write_file_locally.writing.app_error",
|
||||
"translation": "Encountered an error writing to local server storage"
|
||||
},
|
||||
{
|
||||
"id": "api.general.init.debug",
|
||||
"translation": "Initializing general api routes"
|
||||
},
|
||||
{
|
||||
"id": "api.import.import_post.saving.debug",
|
||||
"translation": "Error saving post. user=%v, message=%v"
|
||||
@@ -2323,6 +2371,18 @@
|
||||
"id": "model.client.connecting.app_error",
|
||||
"translation": "We encountered an error while connecting to the server"
|
||||
},
|
||||
{
|
||||
"id": "model.client.create_emoji.emoji.app_error",
|
||||
"translation": "Unable to attach emoji data to request"
|
||||
},
|
||||
{
|
||||
"id": "model.client.create_emoji.image.app_error",
|
||||
"translation": "Unable to attach image to request"
|
||||
},
|
||||
{
|
||||
"id": "model.client.create_emoji.writer.app_error",
|
||||
"translation": "Unable to write request"
|
||||
},
|
||||
{
|
||||
"id": "model.client.login.app_error",
|
||||
"translation": "Authentication tokens didn't match"
|
||||
@@ -2499,6 +2559,26 @@
|
||||
"id": "model.config.is_valid.sql_max_conn.app_error",
|
||||
"translation": "Invalid maximum open connection for SQL settings. Must be a positive number."
|
||||
},
|
||||
{
|
||||
"id": "model.emoji.create_at.app_error",
|
||||
"translation": "Create at must be a valid time"
|
||||
},
|
||||
{
|
||||
"id": "model.emoji.creator_id.app_error",
|
||||
"translation": "Invalid creator id"
|
||||
},
|
||||
{
|
||||
"id": "model.emoji.id.app_error",
|
||||
"translation": "Invalid emoji id"
|
||||
},
|
||||
{
|
||||
"id": "model.emoji.name.app_error",
|
||||
"translation": "Name must be 1 to 64 lowercase alphanumeric characters"
|
||||
},
|
||||
{
|
||||
"id": "model.emoji.update_at.app_error",
|
||||
"translation": "Update at must be a valid time"
|
||||
},
|
||||
{
|
||||
"id": "model.file_info.get.gif.app_error",
|
||||
"translation": "Could not decode gif."
|
||||
@@ -3195,6 +3275,30 @@
|
||||
"id": "store.sql_compliance.save.saving.app_error",
|
||||
"translation": "We encountered an error saving the compliance report"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_emoji.delete.app_error",
|
||||
"translation": "We couldn't delete the emoji"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_emoji.delete.no_results",
|
||||
"translation": "We couldn’t find the emoji to delete"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_emoji.get.app_error",
|
||||
"translation": "We couldn't get the emoji"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_emoji.get_by_name.app_error",
|
||||
"translation": "We couldn't get the emoji"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_emoji.get_all.app_error",
|
||||
"translation": "We couldn't get the emoji"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_emoji.save.app_error",
|
||||
"translation": "We couldn't save the emoji"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_license.get.app_error",
|
||||
"translation": "We encountered an error getting the license"
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
l4g "github.com/alecthomas/log4go"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
@@ -106,6 +108,10 @@ func (c *Client) GetChannelNameRoute(channelName string) string {
|
||||
return fmt.Sprintf("/teams/%v/channels/name/%v", c.GetTeamId(), channelName)
|
||||
}
|
||||
|
||||
func (c *Client) GetEmojiRoute() string {
|
||||
return "/emoji"
|
||||
}
|
||||
|
||||
func (c *Client) GetGeneralRoute() string {
|
||||
return "/general"
|
||||
}
|
||||
@@ -185,6 +191,17 @@ func (c *Client) Must(result *Result, err *AppError) *Result {
|
||||
return result
|
||||
}
|
||||
|
||||
// MustGeneric is a convenience function used for testing.
|
||||
func (c *Client) MustGeneric(result interface{}, err *AppError) interface{} {
|
||||
if err != nil {
|
||||
l4g.Close()
|
||||
time.Sleep(time.Second)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// CheckStatusOK is a convenience function for checking the return of Web Service
|
||||
// call that return the a map of status=OK.
|
||||
func (c *Client) CheckStatusOK(r *http.Response) bool {
|
||||
@@ -1509,3 +1526,74 @@ func (c *Client) GetInitialLoad() (*Result, *AppError) {
|
||||
r.Header.Get(HEADER_ETAG_SERVER), InitialLoadFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// ListEmoji returns a list of all user-created emoji for the server.
|
||||
func (c *Client) ListEmoji() ([]*Emoji, *AppError) {
|
||||
if r, err := c.DoApiGet(c.GetEmojiRoute()+"/list", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
c.fillInExtraProperties(r)
|
||||
return EmojiListFromJson(r.Body), nil
|
||||
}
|
||||
}
|
||||
|
||||
// CreateEmoji will save an emoji to the server if the current user has permission
|
||||
// to do so. If successful, the provided emoji will be returned with its Id field
|
||||
// filled in. Otherwise, an error will be returned.
|
||||
func (c *Client) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoji, *AppError) {
|
||||
c.clearExtraProperties()
|
||||
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
if part, err := writer.CreateFormFile("image", filename); err != nil {
|
||||
return nil, NewLocAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error())
|
||||
} else if _, err = io.Copy(part, bytes.NewBuffer(image)); err != nil {
|
||||
return nil, NewLocAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if err := writer.WriteField("emoji", emoji.ToJson()); err != nil {
|
||||
return nil, NewLocAppError("CreateEmoji", "model.client.create_emoji.emoji.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
if err := writer.Close(); err != nil {
|
||||
return nil, NewLocAppError("CreateEmoji", "model.client.create_emoji.writer.app_error", nil, err.Error())
|
||||
}
|
||||
|
||||
rq, _ := http.NewRequest("POST", c.ApiUrl+c.GetEmojiRoute()+"/create", body)
|
||||
rq.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
if len(c.AuthToken) > 0 {
|
||||
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
|
||||
}
|
||||
|
||||
if r, err := c.HttpClient.Do(rq); err != nil {
|
||||
return nil, NewLocAppError("CreateEmoji", "model.client.connecting.app_error", nil, err.Error())
|
||||
} else if r.StatusCode >= 300 {
|
||||
return nil, AppErrorFromJson(r.Body)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
c.fillInExtraProperties(r)
|
||||
return EmojiFromJson(r.Body), nil
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteEmoji will delete an emoji from the server if the current user has permission
|
||||
// to do so. If successful, it will return status=ok. Otherwise, an error will be returned.
|
||||
func (c *Client) DeleteEmoji(id string) (bool, *AppError) {
|
||||
data := map[string]string{"id": id}
|
||||
|
||||
if r, err := c.DoApiPost(c.GetEmojiRoute()+"/delete", MapToJson(data)); err != nil {
|
||||
return false, err
|
||||
} else {
|
||||
c.fillInExtraProperties(r)
|
||||
return c.CheckStatusOK(r), nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetCustomEmojiImageUrl returns the API route that can be used to get the image used by
|
||||
// the given emoji.
|
||||
func (c *Client) GetCustomEmojiImageUrl(id string) string {
|
||||
return c.GetEmojiRoute() + "/" + id
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ const (
|
||||
DIRECT_MESSAGE_TEAM = "team"
|
||||
|
||||
FAKE_SETTING = "********************************"
|
||||
|
||||
RESTRICT_EMOJI_CREATION_ALL = "all"
|
||||
RESTRICT_EMOJI_CREATION_ADMIN = "system_admin"
|
||||
)
|
||||
|
||||
// should match the values in webapp/i18n/i18n.jsx
|
||||
@@ -70,6 +73,8 @@ type ServiceSettings struct {
|
||||
WebsocketSecurePort *int
|
||||
WebsocketPort *int
|
||||
WebserverMode *string
|
||||
EnableCustomEmoji *bool
|
||||
RestrictCustomEmojiCreation *string
|
||||
}
|
||||
|
||||
type SSOSettings struct {
|
||||
@@ -565,6 +570,16 @@ func (o *Config) SetDefaults() {
|
||||
*o.ServiceSettings.WebserverMode = "regular"
|
||||
}
|
||||
|
||||
if o.ServiceSettings.EnableCustomEmoji == nil {
|
||||
o.ServiceSettings.EnableCustomEmoji = new(bool)
|
||||
*o.ServiceSettings.EnableCustomEmoji = true
|
||||
}
|
||||
|
||||
if o.ServiceSettings.RestrictCustomEmojiCreation == nil {
|
||||
o.ServiceSettings.RestrictCustomEmojiCreation = new(string)
|
||||
*o.ServiceSettings.RestrictCustomEmojiCreation = RESTRICT_EMOJI_CREATION_ALL
|
||||
}
|
||||
|
||||
if o.ComplianceSettings.Enable == nil {
|
||||
o.ComplianceSettings.Enable = new(bool)
|
||||
*o.ComplianceSettings.Enable = false
|
||||
|
||||
95
model/emoji.go
Обычный файл
95
model/emoji.go
Обычный файл
@@ -0,0 +1,95 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Emoji struct {
|
||||
Id string `json:"id"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
UpdateAt int64 `json:"update_at"`
|
||||
DeleteAt int64 `json:"delete_at"`
|
||||
CreatorId string `json:"creator_id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (emoji *Emoji) IsValid() *AppError {
|
||||
if len(emoji.Id) != 26 {
|
||||
return NewLocAppError("Emoji.IsValid", "model.emoji.id.app_error", nil, "")
|
||||
}
|
||||
|
||||
if emoji.CreateAt == 0 {
|
||||
return NewLocAppError("Emoji.IsValid", "model.emoji.create_at.app_error", nil, "id="+emoji.Id)
|
||||
}
|
||||
|
||||
if emoji.UpdateAt == 0 {
|
||||
return NewLocAppError("Emoji.IsValid", "model.emoji.update_at.app_error", nil, "id="+emoji.Id)
|
||||
}
|
||||
|
||||
if len(emoji.CreatorId) != 26 {
|
||||
return NewLocAppError("Emoji.IsValid", "model.emoji.user_id.app_error", nil, "")
|
||||
}
|
||||
|
||||
if len(emoji.Name) == 0 || len(emoji.Name) > 64 {
|
||||
return NewLocAppError("Emoji.IsValid", "model.emoji.name.app_error", nil, "")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (emoji *Emoji) PreSave() {
|
||||
if emoji.Id == "" {
|
||||
emoji.Id = NewId()
|
||||
}
|
||||
|
||||
emoji.CreateAt = GetMillis()
|
||||
emoji.UpdateAt = emoji.CreateAt
|
||||
}
|
||||
|
||||
func (emoji *Emoji) PreUpdate() {
|
||||
emoji.UpdateAt = GetMillis()
|
||||
}
|
||||
|
||||
func (emoji *Emoji) ToJson() string {
|
||||
b, err := json.Marshal(emoji)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func EmojiFromJson(data io.Reader) *Emoji {
|
||||
decoder := json.NewDecoder(data)
|
||||
var emoji Emoji
|
||||
err := decoder.Decode(&emoji)
|
||||
if err == nil {
|
||||
return &emoji
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func EmojiListToJson(emojiList []*Emoji) string {
|
||||
b, err := json.Marshal(emojiList)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func EmojiListFromJson(data io.Reader) []*Emoji {
|
||||
decoder := json.NewDecoder(data)
|
||||
var emojiList []*Emoji
|
||||
err := decoder.Decode(&emojiList)
|
||||
if err == nil {
|
||||
return emojiList
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
63
model/emoji_test.go
Обычный файл
63
model/emoji_test.go
Обычный файл
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEmojiIsValid(t *testing.T) {
|
||||
emoji := Emoji{
|
||||
Id: NewId(),
|
||||
CreateAt: 1234,
|
||||
UpdateAt: 1234,
|
||||
DeleteAt: 0,
|
||||
CreatorId: NewId(),
|
||||
Name: "name",
|
||||
}
|
||||
|
||||
if err := emoji.IsValid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
emoji.Id = "1234"
|
||||
if err := emoji.IsValid(); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
emoji.Id = NewId()
|
||||
emoji.CreateAt = 0
|
||||
if err := emoji.IsValid(); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
emoji.CreateAt = 1234
|
||||
emoji.UpdateAt = 0
|
||||
if err := emoji.IsValid(); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
emoji.UpdateAt = 1234
|
||||
emoji.CreatorId = strings.Repeat("1", 25)
|
||||
if err := emoji.IsValid(); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
emoji.CreatorId = strings.Repeat("1", 27)
|
||||
if err := emoji.IsValid(); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
emoji.CreatorId = NewId()
|
||||
emoji.Name = strings.Repeat("1", 65)
|
||||
if err := emoji.IsValid(); err == nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
emoji.Name = strings.Repeat("1", 64)
|
||||
if err := emoji.IsValid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
169
store/sql_emoji_store.go
Обычный файл
169
store/sql_emoji_store.go
Обычный файл
@@ -0,0 +1,169 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
type SqlEmojiStore struct {
|
||||
*SqlStore
|
||||
}
|
||||
|
||||
func NewSqlEmojiStore(sqlStore *SqlStore) EmojiStore {
|
||||
s := &SqlEmojiStore{sqlStore}
|
||||
|
||||
for _, db := range sqlStore.GetAllConns() {
|
||||
table := db.AddTableWithName(model.Emoji{}, "Emoji").SetKeys(false, "Id")
|
||||
table.ColMap("Id").SetMaxSize(26)
|
||||
table.ColMap("CreatorId").SetMaxSize(26)
|
||||
table.ColMap("Name").SetMaxSize(64)
|
||||
|
||||
table.SetUniqueTogether("Name", "DeleteAt")
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) UpgradeSchemaIfNeeded() {
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) CreateIndexesIfNotExists() {
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) Save(emoji *model.Emoji) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
emoji.PreSave()
|
||||
if result.Err = emoji.IsValid(); result.Err != nil {
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
return
|
||||
}
|
||||
|
||||
if err := es.GetMaster().Insert(emoji); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlEmojiStore.Save", "store.sql_emoji.save.app_error", nil, "id="+emoji.Id+", "+err.Error())
|
||||
} else {
|
||||
result.Data = emoji
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) Get(id string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
var emoji *model.Emoji
|
||||
|
||||
if err := es.GetReplica().SelectOne(&emoji,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
Emoji
|
||||
WHERE
|
||||
Id = :Id
|
||||
AND DeleteAt = 0`, map[string]interface{}{"Id": id}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlEmojiStore.Get", "store.sql_emoji.get.app_error", nil, "id="+id+", "+err.Error())
|
||||
} else {
|
||||
result.Data = emoji
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) GetByName(name string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
var emoji *model.Emoji
|
||||
|
||||
if err := es.GetReplica().SelectOne(&emoji,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
Emoji
|
||||
WHERE
|
||||
Name = :Name
|
||||
AND DeleteAt = 0`, map[string]interface{}{"Name": name}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlEmojiStore.GetByName", "store.sql_emoji.get_by_name.app_error", nil, "name="+name+", "+err.Error())
|
||||
} else {
|
||||
result.Data = emoji
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) GetAll() StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
var emoji []*model.Emoji
|
||||
|
||||
if _, err := es.GetReplica().Select(&emoji,
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
Emoji
|
||||
WHERE
|
||||
DeleteAt = 0`); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlEmojiStore.Get", "store.sql_emoji.get_all.app_error", nil, err.Error())
|
||||
} else {
|
||||
result.Data = emoji
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (es SqlEmojiStore) Delete(id string, time int64) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
if sqlResult, err := es.GetMaster().Exec(
|
||||
`Update
|
||||
Emoji
|
||||
SET
|
||||
DeleteAt = :DeleteAt,
|
||||
UpdateAt = :UpdateAt
|
||||
WHERE
|
||||
Id = :Id
|
||||
AND DeleteAt = 0`, map[string]interface{}{"DeleteAt": time, "UpdateAt": time, "Id": id}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlEmojiStore.Delete", "store.sql_emoji.delete.app_error", nil, "id="+id+", err="+err.Error())
|
||||
} else if rows, _ := sqlResult.RowsAffected(); rows == 0 {
|
||||
result.Err = model.NewLocAppError("SqlEmojiStore.Delete", "store.sql_emoji.delete.no_results", nil, "id="+id+", err="+err.Error())
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
162
store/sql_emoji_store_test.go
Обычный файл
162
store/sql_emoji_store_test.go
Обычный файл
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/mattermost/platform/model"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEmojiSaveDelete(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
emoji1 := &model.Emoji{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
}
|
||||
|
||||
if result := <-store.Emoji().Save(emoji1); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
if len(emoji1.Id) != 26 {
|
||||
t.Fatal("should've set id for emoji")
|
||||
}
|
||||
|
||||
emoji2 := model.Emoji{
|
||||
CreatorId: model.NewId(),
|
||||
Name: emoji1.Name,
|
||||
}
|
||||
if result := <-store.Emoji().Save(&emoji2); result.Err == nil {
|
||||
t.Fatal("shouldn't be able to save emoji with duplicate name")
|
||||
}
|
||||
|
||||
if result := <-store.Emoji().Delete(emoji1.Id, time.Now().Unix()); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
if result := <-store.Emoji().Save(&emoji2); result.Err != nil {
|
||||
t.Fatal("should be able to save emoji with duplicate name now that original has been deleted", result.Err)
|
||||
}
|
||||
|
||||
if result := <-store.Emoji().Delete(emoji2.Id, time.Now().Unix()+1); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmojiGet(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
emojis := []model.Emoji{
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
for i, emoji := range emojis {
|
||||
emojis[i] = *Must(store.Emoji().Save(&emoji)).(*model.Emoji)
|
||||
}
|
||||
defer func() {
|
||||
for _, emoji := range emojis {
|
||||
Must(store.Emoji().Delete(emoji.Id, time.Now().Unix()))
|
||||
}
|
||||
}()
|
||||
|
||||
for _, emoji := range emojis {
|
||||
if result := <-store.Emoji().Get(emoji.Id); result.Err != nil {
|
||||
t.Fatalf("failed to get emoji with id %v: %v", emoji.Id, result.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmojiGetByName(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
emojis := []model.Emoji{
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
for i, emoji := range emojis {
|
||||
emojis[i] = *Must(store.Emoji().Save(&emoji)).(*model.Emoji)
|
||||
}
|
||||
defer func() {
|
||||
for _, emoji := range emojis {
|
||||
Must(store.Emoji().Delete(emoji.Id, time.Now().Unix()))
|
||||
}
|
||||
}()
|
||||
|
||||
for _, emoji := range emojis {
|
||||
if result := <-store.Emoji().GetByName(emoji.Name); result.Err != nil {
|
||||
t.Fatalf("failed to get emoji with name %v: %v", emoji.Name, result.Err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmojiGetAll(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
emojis := []model.Emoji{
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
{
|
||||
CreatorId: model.NewId(),
|
||||
Name: model.NewId(),
|
||||
},
|
||||
}
|
||||
|
||||
for i, emoji := range emojis {
|
||||
emojis[i] = *Must(store.Emoji().Save(&emoji)).(*model.Emoji)
|
||||
}
|
||||
defer func() {
|
||||
for _, emoji := range emojis {
|
||||
Must(store.Emoji().Delete(emoji.Id, time.Now().Unix()))
|
||||
}
|
||||
}()
|
||||
|
||||
if result := <-store.Emoji().GetAll(); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else {
|
||||
for _, emoji := range emojis {
|
||||
found := false
|
||||
|
||||
for _, savedEmoji := range result.Data.([]*model.Emoji) {
|
||||
if emoji.Id == savedEmoji.Id {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatalf("failed to get emoji with id %v", emoji.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ type SqlStore struct {
|
||||
preference PreferenceStore
|
||||
license LicenseStore
|
||||
recovery PasswordRecoveryStore
|
||||
emoji EmojiStore
|
||||
SchemaVersion string
|
||||
}
|
||||
|
||||
@@ -127,6 +128,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.preference = NewSqlPreferenceStore(sqlStore)
|
||||
sqlStore.license = NewSqlLicenseStore(sqlStore)
|
||||
sqlStore.recovery = NewSqlPasswordRecoveryStore(sqlStore)
|
||||
sqlStore.emoji = NewSqlEmojiStore(sqlStore)
|
||||
|
||||
err := sqlStore.master.CreateTablesIfNotExists()
|
||||
if err != nil {
|
||||
@@ -149,6 +151,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.preference.(*SqlPreferenceStore).UpgradeSchemaIfNeeded()
|
||||
sqlStore.license.(*SqlLicenseStore).UpgradeSchemaIfNeeded()
|
||||
sqlStore.recovery.(*SqlPasswordRecoveryStore).UpgradeSchemaIfNeeded()
|
||||
sqlStore.emoji.(*SqlEmojiStore).UpgradeSchemaIfNeeded()
|
||||
|
||||
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
|
||||
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
|
||||
@@ -164,6 +167,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
|
||||
sqlStore.license.(*SqlLicenseStore).CreateIndexesIfNotExists()
|
||||
sqlStore.recovery.(*SqlPasswordRecoveryStore).CreateIndexesIfNotExists()
|
||||
sqlStore.emoji.(*SqlEmojiStore).CreateIndexesIfNotExists()
|
||||
|
||||
sqlStore.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
||||
|
||||
@@ -688,6 +692,10 @@ func (ss SqlStore) PasswordRecovery() PasswordRecoveryStore {
|
||||
return ss.recovery
|
||||
}
|
||||
|
||||
func (ss SqlStore) Emoji() EmojiStore {
|
||||
return ss.emoji
|
||||
}
|
||||
|
||||
func (ss SqlStore) DropAllTables() {
|
||||
ss.master.TruncateTables()
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ type Store interface {
|
||||
Preference() PreferenceStore
|
||||
License() LicenseStore
|
||||
PasswordRecovery() PasswordRecoveryStore
|
||||
Emoji() EmojiStore
|
||||
MarkSystemRanUnitTests()
|
||||
Close()
|
||||
DropAllTables()
|
||||
@@ -254,3 +255,11 @@ type PasswordRecoveryStore interface {
|
||||
Get(userId string) StoreChannel
|
||||
GetByCode(code string) StoreChannel
|
||||
}
|
||||
|
||||
type EmojiStore interface {
|
||||
Save(emoji *model.Emoji) StoreChannel
|
||||
Get(id string) StoreChannel
|
||||
GetByName(name string) StoreChannel
|
||||
GetAll() StoreChannel
|
||||
Delete(id string, time int64) StoreChannel
|
||||
}
|
||||
|
||||
@@ -251,6 +251,9 @@ func getClientConfig(c *model.Config) map[string]string {
|
||||
props["AvailableLocales"] = *c.LocalizationSettings.AvailableLocales
|
||||
props["SQLDriverName"] = c.SqlSettings.DriverName
|
||||
|
||||
props["EnableCustomEmoji"] = strconv.FormatBool(*c.ServiceSettings.EnableCustomEmoji)
|
||||
props["RestrictCustomEmojiCreation"] = *c.ServiceSettings.RestrictCustomEmojiCreation
|
||||
|
||||
if IsLicensed {
|
||||
if *License.Features.CustomBrand {
|
||||
props["EnableCustomBrand"] = strconv.FormatBool(*c.TeamSettings.EnableCustomBrand)
|
||||
|
||||
@@ -532,6 +532,16 @@ export default class AdminSidebar extends React.Component {
|
||||
}
|
||||
>
|
||||
{customBranding}
|
||||
<AdminSidebarSection
|
||||
name='custom_emoji'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.customEmoji'
|
||||
defaultMessage='Custom Emoji'
|
||||
/>
|
||||
|
||||
}
|
||||
/>
|
||||
<AdminSidebarSection
|
||||
name='legal_and_support'
|
||||
title={
|
||||
|
||||
91
webapp/components/admin_console/custom_emoji_settings.jsx
Обычный файл
91
webapp/components/admin_console/custom_emoji_settings.jsx
Обычный файл
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import AdminSettings from './admin_settings.jsx';
|
||||
import BooleanSetting from './boolean_setting.jsx';
|
||||
import DropdownSetting from './dropdown_setting.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import SettingsGroup from './settings_group.jsx';
|
||||
|
||||
export default class CustomEmojiSettings extends AdminSettings {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getConfigFromState = this.getConfigFromState.bind(this);
|
||||
|
||||
this.renderSettings = this.renderSettings.bind(this);
|
||||
|
||||
this.state = Object.assign(this.state, {
|
||||
enableCustomEmoji: props.config.ServiceSettings.EnableCustomEmoji,
|
||||
restrictCustomEmojiCreation: props.config.ServiceSettings.RestrictCustomEmojiCreation
|
||||
});
|
||||
}
|
||||
|
||||
getConfigFromState(config) {
|
||||
config.ServiceSettings.EnableCustomEmoji = this.state.enableCustomEmoji;
|
||||
config.ServiceSettings.RestrictCustomEmojiCreation = this.state.restrictCustomEmojiCreation;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
renderTitle() {
|
||||
return (
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.customization.customEmoji'
|
||||
defaultMessage='Custom Emoji'
|
||||
/>
|
||||
</h3>
|
||||
);
|
||||
}
|
||||
|
||||
renderSettings() {
|
||||
return (
|
||||
<SettingsGroup>
|
||||
<BooleanSetting
|
||||
id='enableCustomEmoji'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.customization.enableCustomEmojiTitle'
|
||||
defaultMessage='Enable Custom Emoji:'
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='admin.customization.enableCustomEmojiDesc'
|
||||
defaultMessage='Enable users to create custom emoji for use in chat messages.'
|
||||
/>
|
||||
}
|
||||
value={this.state.enableCustomEmoji}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<DropdownSetting
|
||||
id='restrictCustomEmojiCreation'
|
||||
values={[
|
||||
{value: 'all', text: Utils.localizeMessage('admin.customization.restrictCustomEmojiCreationAll', 'Allow everyone to create custom emoji')},
|
||||
{value: 'system_admin', text: Utils.localizeMessage('admin.customization.restrictCustomEmojiCreationSystemAdmin', 'Only allow system admins to create custom emoji')}
|
||||
]}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.customization.restrictCustomEmojiCreationTitle'
|
||||
defaultMessage='Restrict Custom Emoji Creation:'
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='admin.customization.restrictCustomEmojiCreationDesc'
|
||||
defaultMessage='Restrict the creation of custom emoji to certain users.'
|
||||
/>
|
||||
}
|
||||
value={this.state.restrictCustomEmojiCreation}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enableCustomEmoji}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,13 @@
|
||||
"admin.connectionSecurityTitle": "Connection Security:",
|
||||
"admin.connectionSecurityTls": "TLS",
|
||||
"admin.connectionSecurityTlsDescription": "Encrypts the communication between Mattermost and your server.",
|
||||
"admin.customization.customEmoji": "Custom Emoji",
|
||||
"admin.customization.enableCustomEmojiDesc": "Enable users to create custom emoji for use in chat messages.",
|
||||
"admin.customization.enableCustomEmojiTitle": "Enable Custom Emoji:",
|
||||
"admin.customization.restrictCustomEmojiCreationAll": "Allow everyone to create custom emoji",
|
||||
"admin.customization.restrictCustomEmojiCreationDesc": "Restrict the creation of custom emoji to certain users.",
|
||||
"admin.customization.restrictCustomEmojiCreationSystemAdmin": "Only allow system admins to create custom emoji",
|
||||
"admin.customization.restrictCustomEmojiCreationTitle": "Restrict Custom Emoji Creation:",
|
||||
"admin.email.agreeHPNS": " I understand and accept the Mattermost Hosted Push Notification Service <a href=\"https://about.mattermost.com/hpns-terms/\" target=\"_blank\">Terms of Service</a> and <a href=\"https://about.mattermost.com/hpns-privacy/\" target=\"_blank\">Privacy Policy</a>.",
|
||||
"admin.email.allowEmailSignInDescription": "When true, Mattermost allows users to sign in using their email and password.",
|
||||
"admin.email.allowEmailSignInTitle": "Allow Sign In With Email: ",
|
||||
@@ -422,6 +429,7 @@
|
||||
"admin.sidebar.configuration": "Configuration",
|
||||
"admin.sidebar.connections": "Connections",
|
||||
"admin.sidebar.customBrand": "Custom Branding",
|
||||
"admin.sidebar.customEmoji": "Custom Emoji",
|
||||
"admin.sidebar.customization": "Customization",
|
||||
"admin.sidebar.database": "Database",
|
||||
"admin.sidebar.developer": "Developer",
|
||||
|
||||
@@ -73,6 +73,7 @@ import DatabaseSettings from 'components/admin_console/database_settings.jsx';
|
||||
import StorageSettings from 'components/admin_console/storage_settings.jsx';
|
||||
import ImageSettings from 'components/admin_console/image_settings.jsx';
|
||||
import CustomBrandSettings from 'components/admin_console/custom_brand_settings.jsx';
|
||||
import CustomEmojiSettings from 'components/admin_console/custom_emoji_settings.jsx';
|
||||
import LegalAndSupportSettings from 'components/admin_console/legal_and_support_settings.jsx';
|
||||
import ComplianceSettings from 'components/admin_console/compliance_settings.jsx';
|
||||
import RateSettings from 'components/admin_console/rate_settings.jsx';
|
||||
@@ -461,6 +462,10 @@ function renderRootComponent() {
|
||||
path='custom_brand'
|
||||
component={CustomBrandSettings}
|
||||
/>
|
||||
<Route
|
||||
path='custom_emoji'
|
||||
component={CustomEmojiSettings}
|
||||
/>
|
||||
<Route
|
||||
path='legal_and_support'
|
||||
component={LegalAndSupportSettings}
|
||||
|
||||
Ссылка в новой задаче
Block a user