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
Этот коммит содержится в:
Harrison Healey
2016-06-14 09:38:19 -04:00
коммит произвёл Christopher Speller
родитель 661f221727
Коммит a0cc913b85
18 изменённых файлов: 1538 добавлений и 5 удалений

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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",

Просмотреть файл

@@ -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 couldnt 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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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={

Просмотреть файл

@@ -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}