summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api4/api.go2
-rw-r--r--api4/emoji.go44
-rw-r--r--api4/emoji_test.go134
-rw-r--r--app/emoji.go16
-rw-r--r--model/client4.go21
-rw-r--r--model/emoji_search.go34
-rw-r--r--model/emoji_search_test.go19
-rw-r--r--store/sqlstore/emoji_store.go29
-rw-r--r--store/store.go1
-rw-r--r--store/storetest/emoji_store.go68
-rw-r--r--store/storetest/mocks/EmojiStore.go16
11 files changed, 381 insertions, 3 deletions
diff --git a/api4/api.go b/api4/api.go
index dd5e56d35..90c9d0df7 100644
--- a/api4/api.go
+++ b/api4/api.go
@@ -184,7 +184,7 @@ func Init(a *app.App, root *mux.Router, full bool) *API {
api.BaseRoutes.DataRetention = api.BaseRoutes.ApiRoot.PathPrefix("/data_retention").Subrouter()
api.BaseRoutes.Emojis = api.BaseRoutes.ApiRoot.PathPrefix("/emoji").Subrouter()
- api.BaseRoutes.Emoji = api.BaseRoutes.Emojis.PathPrefix("/{emoji_id:[A-Za-z0-9]+}").Subrouter()
+ api.BaseRoutes.Emoji = api.BaseRoutes.ApiRoot.PathPrefix("/emoji/{emoji_id:[A-Za-z0-9]+}").Subrouter()
api.BaseRoutes.ReactionByNameForPostForUser = api.BaseRoutes.PostForUser.PathPrefix("/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()
diff --git a/api4/emoji.go b/api4/emoji.go
index 049e77d3c..30d59125b 100644
--- a/api4/emoji.go
+++ b/api4/emoji.go
@@ -11,9 +11,15 @@ import (
"github.com/mattermost/mattermost-server/model"
)
+const (
+ EMOJI_MAX_AUTOCOMPLETE_ITEMS = 100
+)
+
func (api *API) InitEmoji() {
api.BaseRoutes.Emojis.Handle("", api.ApiSessionRequired(createEmoji)).Methods("POST")
api.BaseRoutes.Emojis.Handle("", api.ApiSessionRequired(getEmojiList)).Methods("GET")
+ api.BaseRoutes.Emojis.Handle("/search", api.ApiSessionRequired(searchEmojis)).Methods("POST")
+ api.BaseRoutes.Emojis.Handle("/autocomplete", api.ApiSessionRequired(autocompleteEmojis)).Methods("GET")
api.BaseRoutes.Emoji.Handle("", api.ApiSessionRequired(deleteEmoji)).Methods("DELETE")
api.BaseRoutes.Emoji.Handle("", api.ApiSessionRequired(getEmoji)).Methods("GET")
api.BaseRoutes.Emoji.Handle("/image", api.ApiSessionRequiredTrustRequester(getEmojiImage)).Methods("GET")
@@ -162,3 +168,41 @@ func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "max-age=2592000, public")
w.Write(image)
}
+
+func searchEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
+ emojiSearch := model.EmojiSearchFromJson(r.Body)
+ if emojiSearch == nil {
+ c.SetInvalidParam("term")
+ return
+ }
+
+ if emojiSearch.Term == "" {
+ c.SetInvalidParam("term")
+ return
+ }
+
+ emojis, err := c.App.SearchEmoji(emojiSearch.Term, emojiSearch.PrefixOnly, PER_PAGE_MAXIMUM)
+ if err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Write([]byte(model.EmojiListToJson(emojis)))
+ }
+}
+
+func autocompleteEmojis(c *Context, w http.ResponseWriter, r *http.Request) {
+ name := r.URL.Query().Get("name")
+
+ if name == "" {
+ c.SetInvalidUrlParam("name")
+ return
+ }
+
+ emojis, err := c.App.SearchEmoji(name, true, EMOJI_MAX_AUTOCOMPLETE_ITEMS)
+ if err != nil {
+ c.Err = err
+ return
+ } else {
+ w.Write([]byte(model.EmojiListToJson(emojis)))
+ }
+}
diff --git a/api4/emoji_test.go b/api4/emoji_test.go
index 3b0fecb2b..b8b093656 100644
--- a/api4/emoji_test.go
+++ b/api4/emoji_test.go
@@ -11,6 +11,8 @@ import (
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
+
+ "github.com/stretchr/testify/assert"
)
func TestCreateEmoji(t *testing.T) {
@@ -432,3 +434,135 @@ func TestGetEmojiImage(t *testing.T) {
_, resp = Client.GetEmojiImage("")
CheckBadRequestStatus(t, resp)
}
+
+func TestSearchEmoji(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+ Client := th.Client
+
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
+
+ searchTerm1 := model.NewId()
+ searchTerm2 := model.NewId()
+
+ emojis := []*model.Emoji{
+ {
+ CreatorId: th.BasicUser.Id,
+ Name: searchTerm1,
+ },
+ {
+ CreatorId: th.BasicUser.Id,
+ Name: "blargh_" + searchTerm2,
+ },
+ }
+
+ for idx, emoji := range emojis {
+ emoji, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+ emojis[idx] = emoji
+ }
+
+ search := &model.EmojiSearch{Term: searchTerm1}
+ remojis, resp := Client.SearchEmoji(search)
+ CheckNoError(t, resp)
+ CheckOKStatus(t, resp)
+
+ found := false
+ for _, e := range remojis {
+ if e.Name == emojis[0].Name {
+ found = true
+ }
+ }
+
+ assert.True(t, found)
+
+ search.Term = searchTerm2
+ search.PrefixOnly = true
+ remojis, resp = Client.SearchEmoji(search)
+ CheckNoError(t, resp)
+ CheckOKStatus(t, resp)
+
+ found = false
+ for _, e := range remojis {
+ if e.Name == emojis[1].Name {
+ found = true
+ }
+ }
+
+ assert.False(t, found)
+
+ search.PrefixOnly = false
+ remojis, resp = Client.SearchEmoji(search)
+ CheckNoError(t, resp)
+ CheckOKStatus(t, resp)
+
+ found = false
+ for _, e := range remojis {
+ if e.Name == emojis[1].Name {
+ found = true
+ }
+ }
+
+ assert.True(t, found)
+
+ search.Term = ""
+ _, resp = Client.SearchEmoji(search)
+ CheckBadRequestStatus(t, resp)
+
+ Client.Logout()
+ _, resp = Client.SearchEmoji(search)
+ CheckUnauthorizedStatus(t, resp)
+}
+
+func TestAutocompleteEmoji(t *testing.T) {
+ th := Setup().InitBasic()
+ defer th.TearDown()
+ Client := th.Client
+
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCustomEmoji = true })
+
+ searchTerm1 := model.NewId()
+
+ emojis := []*model.Emoji{
+ {
+ CreatorId: th.BasicUser.Id,
+ Name: searchTerm1,
+ },
+ {
+ CreatorId: th.BasicUser.Id,
+ Name: "blargh_" + searchTerm1,
+ },
+ }
+
+ for idx, emoji := range emojis {
+ emoji, resp := Client.CreateEmoji(emoji, utils.CreateTestGif(t, 10, 10), "image.gif")
+ CheckNoError(t, resp)
+ emojis[idx] = emoji
+ }
+
+ remojis, resp := Client.AutocompleteEmoji(searchTerm1, "")
+ CheckNoError(t, resp)
+ CheckOKStatus(t, resp)
+
+ found1 := false
+ found2 := false
+ for _, e := range remojis {
+ if e.Name == emojis[0].Name {
+ found1 = true
+ }
+
+ if e.Name == emojis[1].Name {
+ found2 = true
+ }
+ }
+
+ assert.True(t, found1)
+ assert.False(t, found2)
+
+ _, resp = Client.AutocompleteEmoji("", "")
+ CheckBadRequestStatus(t, resp)
+
+ Client.Logout()
+ _, resp = Client.AutocompleteEmoji(searchTerm1, "")
+ CheckUnauthorizedStatus(t, resp)
+}
diff --git a/app/emoji.go b/app/emoji.go
index 2786af9c9..2271d650d 100644
--- a/app/emoji.go
+++ b/app/emoji.go
@@ -134,11 +134,11 @@ func (a *App) DeleteEmoji(emoji *model.Emoji) *model.AppError {
func (a *App) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji {
- return nil, model.NewAppError("deleteEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
+ return nil, model.NewAppError("GetEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
}
if len(*a.Config().FileSettings.DriverName) == 0 {
- return nil, model.NewAppError("deleteImage", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
+ return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
}
if result := <-a.Srv.Store.Emoji().Get(emojiId, false); result.Err != nil {
@@ -169,6 +169,18 @@ func (a *App) GetEmojiImage(emojiId string) (imageByte []byte, imageType string,
}
}
+func (a *App) SearchEmoji(name string, prefixOnly bool, limit int) ([]*model.Emoji, *model.AppError) {
+ if !*a.Config().ServiceSettings.EnableCustomEmoji {
+ return nil, model.NewAppError("SearchEmoji", "api.emoji.disabled.app_error", nil, "", http.StatusNotImplemented)
+ }
+
+ if result := <-a.Srv.Store.Emoji().Search(name, prefixOnly, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Emoji), nil
+ }
+}
+
func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
// Create a new RGBA image to hold the incremental frames.
firstFrame := gifImg.Image[0].Bounds()
diff --git a/model/client4.go b/model/client4.go
index c44855993..151b5a491 100644
--- a/model/client4.go
+++ b/model/client4.go
@@ -3070,6 +3070,27 @@ func (c *Client4) GetEmojiImage(emojiId string) ([]byte, *Response) {
}
}
+// SearchEmoji returns a list of emoji matching some search criteria.
+func (c *Client4) SearchEmoji(search *EmojiSearch) ([]*Emoji, *Response) {
+ if r, err := c.DoApiPost(c.GetEmojisRoute()+"/search", search.ToJson()); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return EmojiListFromJson(r.Body), BuildResponse(r)
+ }
+}
+
+// AutocompleteEmoji returns a list of emoji starting with or matching name.
+func (c *Client4) AutocompleteEmoji(name string, etag string) ([]*Emoji, *Response) {
+ query := fmt.Sprintf("?name=%v", name)
+ if r, err := c.DoApiGet(c.GetEmojisRoute()+"/autocomplete"+query, ""); err != nil {
+ return nil, BuildErrorResponse(r, err)
+ } else {
+ defer closeBody(r)
+ return EmojiListFromJson(r.Body), BuildResponse(r)
+ }
+}
+
// Reaction Section
// SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned.
diff --git a/model/emoji_search.go b/model/emoji_search.go
new file mode 100644
index 000000000..31931170e
--- /dev/null
+++ b/model/emoji_search.go
@@ -0,0 +1,34 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "encoding/json"
+ "io"
+)
+
+type EmojiSearch struct {
+ Term string `json:"term"`
+ PrefixOnly bool `json:"prefix_only"`
+}
+
+func (es *EmojiSearch) ToJson() string {
+ b, err := json.Marshal(es)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func EmojiSearchFromJson(data io.Reader) *EmojiSearch {
+ decoder := json.NewDecoder(data)
+ var es EmojiSearch
+ err := decoder.Decode(&es)
+ if err == nil {
+ return &es
+ } else {
+ return nil
+ }
+}
diff --git a/model/emoji_search_test.go b/model/emoji_search_test.go
new file mode 100644
index 000000000..6e3b01213
--- /dev/null
+++ b/model/emoji_search_test.go
@@ -0,0 +1,19 @@
+// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package model
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestEmojiSearchJson(t *testing.T) {
+ emojiSearch := EmojiSearch{Term: NewId()}
+ json := emojiSearch.ToJson()
+ remojiSearch := EmojiSearchFromJson(strings.NewReader(json))
+
+ if emojiSearch.Term != remojiSearch.Term {
+ t.Fatal("Terms do not match")
+ }
+}
diff --git a/store/sqlstore/emoji_store.go b/store/sqlstore/emoji_store.go
index 734190dbb..afd87b83d 100644
--- a/store/sqlstore/emoji_store.go
+++ b/store/sqlstore/emoji_store.go
@@ -46,6 +46,7 @@ func (es SqlEmojiStore) CreateIndexesIfNotExists() {
es.CreateIndexIfNotExists("idx_emoji_update_at", "Emoji", "UpdateAt")
es.CreateIndexIfNotExists("idx_emoji_create_at", "Emoji", "CreateAt")
es.CreateIndexIfNotExists("idx_emoji_delete_at", "Emoji", "DeleteAt")
+ es.CreateIndexIfNotExists("idx_emoji_name", "Emoji", "Name")
}
func (es SqlEmojiStore) Save(emoji *model.Emoji) store.StoreChannel {
@@ -162,3 +163,31 @@ func (es SqlEmojiStore) Delete(id string, time int64) store.StoreChannel {
emojiCache.Remove(id)
})
}
+
+func (es SqlEmojiStore) Search(name string, prefixOnly bool, limit int) store.StoreChannel {
+ return store.Do(func(result *store.StoreResult) {
+ var emojis []*model.Emoji
+
+ term := ""
+ if !prefixOnly {
+ term = "%"
+ }
+
+ term += name + "%"
+
+ if _, err := es.GetReplica().Select(&emojis,
+ `SELECT
+ *
+ FROM
+ Emoji
+ WHERE
+ Name LIKE :Name
+ AND DeleteAt = 0
+ ORDER BY Name
+ LIMIT :Limit`, map[string]interface{}{"Name": term, "Limit": limit}); err != nil {
+ result.Err = model.NewAppError("SqlEmojiStore.Search", "store.sql_emoji.get_by_name.app_error", nil, "name="+name+", "+err.Error(), http.StatusInternalServerError)
+ } else {
+ result.Data = emojis
+ }
+ })
+}
diff --git a/store/store.go b/store/store.go
index 8cb5093ea..2742c0889 100644
--- a/store/store.go
+++ b/store/store.go
@@ -393,6 +393,7 @@ type EmojiStore interface {
GetByName(name string) StoreChannel
GetList(offset, limit int, sort string) StoreChannel
Delete(id string, time int64) StoreChannel
+ Search(name string, prefixOnly bool, limit int) StoreChannel
}
type StatusStore interface {
diff --git a/store/storetest/emoji_store.go b/store/storetest/emoji_store.go
index a862440e5..9e4dbaa6e 100644
--- a/store/storetest/emoji_store.go
+++ b/store/storetest/emoji_store.go
@@ -18,6 +18,7 @@ func TestEmojiStore(t *testing.T, ss store.Store) {
t.Run("EmojiGet", func(t *testing.T) { testEmojiGet(t, ss) })
t.Run("EmojiGetByName", func(t *testing.T) { testEmojiGetByName(t, ss) })
t.Run("EmojiGetList", func(t *testing.T) { testEmojiGetList(t, ss) })
+ t.Run("EmojiSearch", func(t *testing.T) { testEmojiSearch(t, ss) })
}
func testEmojiSaveDelete(t *testing.T, ss store.Store) {
@@ -191,3 +192,70 @@ func testEmojiGetList(t *testing.T, ss store.Store) {
assert.Equal(t, emojis[2].Name, remojis[1].Name)
}
+
+func testEmojiSearch(t *testing.T, ss store.Store) {
+ emojis := []model.Emoji{
+ {
+ CreatorId: model.NewId(),
+ Name: "blargh_" + model.NewId(),
+ },
+ {
+ CreatorId: model.NewId(),
+ Name: model.NewId() + "_blargh",
+ },
+ {
+ CreatorId: model.NewId(),
+ Name: model.NewId() + "_blargh_" + model.NewId(),
+ },
+ {
+ CreatorId: model.NewId(),
+ Name: model.NewId(),
+ },
+ }
+
+ for i, emoji := range emojis {
+ emojis[i] = *store.Must(ss.Emoji().Save(&emoji)).(*model.Emoji)
+ }
+ defer func() {
+ for _, emoji := range emojis {
+ store.Must(ss.Emoji().Delete(emoji.Id, time.Now().Unix()))
+ }
+ }()
+
+ shouldFind := []bool{true, false, false, false}
+
+ if result := <-ss.Emoji().Search("blargh", true, 100); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ for i, emoji := range emojis {
+ found := false
+
+ for _, savedEmoji := range result.Data.([]*model.Emoji) {
+ if emoji.Id == savedEmoji.Id {
+ found = true
+ break
+ }
+ }
+
+ assert.Equal(t, shouldFind[i], found, emoji.Name)
+ }
+ }
+
+ shouldFind = []bool{true, true, true, false}
+ if result := <-ss.Emoji().Search("blargh", false, 100); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ for i, emoji := range emojis {
+ found := false
+
+ for _, savedEmoji := range result.Data.([]*model.Emoji) {
+ if emoji.Id == savedEmoji.Id {
+ found = true
+ break
+ }
+ }
+
+ assert.Equal(t, shouldFind[i], found, emoji.Name)
+ }
+ }
+}
diff --git a/store/storetest/mocks/EmojiStore.go b/store/storetest/mocks/EmojiStore.go
index d1bfe7f00..9871c98aa 100644
--- a/store/storetest/mocks/EmojiStore.go
+++ b/store/storetest/mocks/EmojiStore.go
@@ -92,3 +92,19 @@ func (_m *EmojiStore) Save(emoji *model.Emoji) store.StoreChannel {
return r0
}
+
+// Search provides a mock function with given fields: name, prefixOnly, limit
+func (_m *EmojiStore) Search(name string, prefixOnly bool, limit int) store.StoreChannel {
+ ret := _m.Called(name, prefixOnly, limit)
+
+ var r0 store.StoreChannel
+ if rf, ok := ret.Get(0).(func(string, bool, int) store.StoreChannel); ok {
+ r0 = rf(name, prefixOnly, limit)
+ } else {
+ if ret.Get(0) != nil {
+ r0 = ret.Get(0).(store.StoreChannel)
+ }
+ }
+
+ return r0
+}