ABC-90 Add POST /emoji/search and GET /emoji/autocomplete API endpoints (#8125)

* Add POST /emoji/search and GET /emoji/autocomplete API endpoints

* Add constant to be clearer
Этот коммит содержится в:
Joram Wilander
2018-01-23 11:04:44 -05:00
коммит произвёл Christopher Speller
родитель 599991ea73
Коммит 4f4a765e7d
11 изменённых файлов: 381 добавлений и 3 удалений

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

@@ -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.DataRetention = api.BaseRoutes.ApiRoot.PathPrefix("/data_retention").Subrouter()
api.BaseRoutes.Emojis = api.BaseRoutes.ApiRoot.PathPrefix("/emoji").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() api.BaseRoutes.ReactionByNameForPostForUser = api.BaseRoutes.PostForUser.PathPrefix("/reactions/{emoji_name:[A-Za-z0-9\\_\\-\\+]+}").Subrouter()

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

@@ -11,9 +11,15 @@ import (
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
) )
const (
EMOJI_MAX_AUTOCOMPLETE_ITEMS = 100
)
func (api *API) InitEmoji() { func (api *API) InitEmoji() {
api.BaseRoutes.Emojis.Handle("", api.ApiSessionRequired(createEmoji)).Methods("POST") api.BaseRoutes.Emojis.Handle("", api.ApiSessionRequired(createEmoji)).Methods("POST")
api.BaseRoutes.Emojis.Handle("", api.ApiSessionRequired(getEmojiList)).Methods("GET") 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(deleteEmoji)).Methods("DELETE")
api.BaseRoutes.Emoji.Handle("", api.ApiSessionRequired(getEmoji)).Methods("GET") api.BaseRoutes.Emoji.Handle("", api.ApiSessionRequired(getEmoji)).Methods("GET")
api.BaseRoutes.Emoji.Handle("/image", api.ApiSessionRequiredTrustRequester(getEmojiImage)).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.Header().Set("Cache-Control", "max-age=2592000, public")
w.Write(image) 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)))
}
}

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

@@ -11,6 +11,8 @@ import (
"github.com/mattermost/mattermost-server/model" "github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/utils"
"github.com/stretchr/testify/assert"
) )
func TestCreateEmoji(t *testing.T) { func TestCreateEmoji(t *testing.T) {
@@ -432,3 +434,135 @@ func TestGetEmojiImage(t *testing.T) {
_, resp = Client.GetEmojiImage("") _, resp = Client.GetEmojiImage("")
CheckBadRequestStatus(t, resp) 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)
}

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

@@ -134,11 +134,11 @@ func (a *App) DeleteEmoji(emoji *model.Emoji) *model.AppError {
func (a *App) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) { func (a *App) GetEmoji(emojiId string) (*model.Emoji, *model.AppError) {
if !*a.Config().ServiceSettings.EnableCustomEmoji { 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 { 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 { 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 { func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
// Create a new RGBA image to hold the incremental frames. // Create a new RGBA image to hold the incremental frames.
firstFrame := gifImg.Image[0].Bounds() firstFrame := gifImg.Image[0].Bounds()

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

@@ -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 // Reaction Section
// SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned. // SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned.

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

19
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")
}
}

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

@@ -46,6 +46,7 @@ func (es SqlEmojiStore) CreateIndexesIfNotExists() {
es.CreateIndexIfNotExists("idx_emoji_update_at", "Emoji", "UpdateAt") es.CreateIndexIfNotExists("idx_emoji_update_at", "Emoji", "UpdateAt")
es.CreateIndexIfNotExists("idx_emoji_create_at", "Emoji", "CreateAt") es.CreateIndexIfNotExists("idx_emoji_create_at", "Emoji", "CreateAt")
es.CreateIndexIfNotExists("idx_emoji_delete_at", "Emoji", "DeleteAt") es.CreateIndexIfNotExists("idx_emoji_delete_at", "Emoji", "DeleteAt")
es.CreateIndexIfNotExists("idx_emoji_name", "Emoji", "Name")
} }
func (es SqlEmojiStore) Save(emoji *model.Emoji) store.StoreChannel { 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) 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
}
})
}

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

@@ -393,6 +393,7 @@ type EmojiStore interface {
GetByName(name string) StoreChannel GetByName(name string) StoreChannel
GetList(offset, limit int, sort string) StoreChannel GetList(offset, limit int, sort string) StoreChannel
Delete(id string, time int64) StoreChannel Delete(id string, time int64) StoreChannel
Search(name string, prefixOnly bool, limit int) StoreChannel
} }
type StatusStore interface { type StatusStore interface {

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

@@ -18,6 +18,7 @@ func TestEmojiStore(t *testing.T, ss store.Store) {
t.Run("EmojiGet", func(t *testing.T) { testEmojiGet(t, ss) }) t.Run("EmojiGet", func(t *testing.T) { testEmojiGet(t, ss) })
t.Run("EmojiGetByName", func(t *testing.T) { testEmojiGetByName(t, ss) }) t.Run("EmojiGetByName", func(t *testing.T) { testEmojiGetByName(t, ss) })
t.Run("EmojiGetList", func(t *testing.T) { testEmojiGetList(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) { 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) 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)
}
}
}

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

@@ -92,3 +92,19 @@ func (_m *EmojiStore) Save(emoji *model.Emoji) store.StoreChannel {
return r0 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
}