[MM-16389] Add icon_emoji parameter to webhooks (#11586)
[MM-16389] Add icon_emoji field to posts from incoming webhooks To be used to substitute the profile picture on posts.
Этот коммит содержится в:
24
app/emoji.go
24
app/emoji.go
@@ -14,12 +14,14 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"image/color/palette"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -53,7 +55,7 @@ func (a *App) CreateEmoji(sessionUserId string, emoji *model.Emoji, multiPartIma
|
||||
return nil, model.NewAppError("createEmoji", "api.emoji.create.other_user.app_error", nil, "", http.StatusForbidden)
|
||||
}
|
||||
|
||||
if existingEmoji, err := a.Srv.Store.Emoji().GetByName(emoji.Name); err == nil && existingEmoji != nil {
|
||||
if existingEmoji, err := a.Srv.Store.Emoji().GetByName(emoji.Name, true); err == nil && existingEmoji != nil {
|
||||
return nil, model.NewAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -156,7 +158,7 @@ func (a *App) UploadEmojiImage(id string, imageData *multipart.FileHeader) *mode
|
||||
}
|
||||
|
||||
func (a *App) DeleteEmoji(emoji *model.Emoji) *model.AppError {
|
||||
if err := a.Srv.Store.Emoji().Delete(emoji.Id, model.GetMillis()); err != nil {
|
||||
if err := a.Srv.Store.Emoji().Delete(emoji, model.GetMillis()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -186,7 +188,7 @@ func (a *App) GetEmojiByName(emojiName string) (*model.Emoji, *model.AppError) {
|
||||
return nil, model.NewAppError("GetEmoji", "api.emoji.storage.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
return a.Srv.Store.Emoji().GetByName(emojiName)
|
||||
return a.Srv.Store.Emoji().GetByName(emojiName, true)
|
||||
}
|
||||
|
||||
func (a *App) GetMultipleEmojiByName(names []string) ([]*model.Emoji, *model.AppError) {
|
||||
@@ -224,6 +226,22 @@ func (a *App) SearchEmoji(name string, prefixOnly bool, limit int) ([]*model.Emo
|
||||
return a.Srv.Store.Emoji().Search(name, prefixOnly, limit)
|
||||
}
|
||||
|
||||
// GetEmojiStaticUrl returns a relative static URL for system default emojis,
|
||||
// and the API route for custom ones. Errors if not found or if custom and deleted.
|
||||
func (a *App) GetEmojiStaticUrl(emojiName string) (string, *model.AppError) {
|
||||
subPath, _ := utils.GetSubpathFromConfig(a.Config())
|
||||
|
||||
if id, found := model.GetSystemEmojiId(emojiName); found {
|
||||
return path.Join(subPath, "/static/emoji", id+".png"), nil
|
||||
}
|
||||
|
||||
if emoji, err := a.Srv.Store.Emoji().GetByName(emojiName, true); err == nil {
|
||||
return path.Join(subPath, "/api/v4/emoji", emoji.Id, "image"), nil
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
func resizeEmojiGif(gifImg *gif.GIF) *gif.GIF {
|
||||
// Create a new RGBA image to hold the incremental frames.
|
||||
firstFrame := gifImg.Image[0].Bounds()
|
||||
|
||||
@@ -1287,7 +1287,7 @@ func (a *App) ImportEmoji(data *EmojiImportData, dryRun bool) *model.AppError {
|
||||
|
||||
var emoji *model.Emoji
|
||||
|
||||
emoji, appError := a.Srv.Store.Emoji().GetByName(*data.Name)
|
||||
emoji, appError := a.Srv.Store.Emoji().GetByName(*data.Name, true)
|
||||
if appError != nil && appError.StatusCode != http.StatusNotFound {
|
||||
return appError
|
||||
}
|
||||
|
||||
@@ -2647,7 +2647,7 @@ func TestImportImportEmoji(t *testing.T) {
|
||||
err := th.App.ImportEmoji(&data, true)
|
||||
assert.NotNil(t, err, "Invalid emoji should have failed dry run")
|
||||
|
||||
emoji, err := th.App.Srv.Store.Emoji().GetByName(*data.Name)
|
||||
emoji, err := th.App.Srv.Store.Emoji().GetByName(*data.Name, true)
|
||||
assert.Nil(t, emoji, "Emoji should not have been imported")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
@@ -2667,7 +2667,7 @@ func TestImportImportEmoji(t *testing.T) {
|
||||
err = th.App.ImportEmoji(&data, false)
|
||||
assert.Nil(t, err, "Valid emoji should have succeeded apply mode")
|
||||
|
||||
emoji, err = th.App.Srv.Store.Emoji().GetByName(*data.Name)
|
||||
emoji, err = th.App.Srv.Store.Emoji().GetByName(*data.Name, true)
|
||||
assert.NotNil(t, emoji, "Emoji should have been imported")
|
||||
assert.Nil(t, err, "Emoji should have been imported without any error")
|
||||
|
||||
|
||||
@@ -55,12 +55,37 @@ func (a *App) PreparePostListForClient(originalList *model.PostList) *model.Post
|
||||
return list
|
||||
}
|
||||
|
||||
// OverrideIconURLIfEmoji changes the post icon override URL prop, if it has an emoji icon,
|
||||
// so that it points to the URL (relative) of the emoji - static if emoji is default, /api if custom.
|
||||
func (a *App) OverrideIconURLIfEmoji(post *model.Post) {
|
||||
prop, ok := post.Props[model.POST_PROPS_OVERRIDE_ICON_EMOJI]
|
||||
if !ok || prop == nil {
|
||||
return
|
||||
}
|
||||
emojiName := prop.(string)
|
||||
delete(post.Props, model.POST_PROPS_OVERRIDE_ICON_EMOJI)
|
||||
|
||||
if !*a.Config().ServiceSettings.EnablePostIconOverride || emojiName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if emojiUrl, err := a.GetEmojiStaticUrl(emojiName); err == nil {
|
||||
post.AddProp(model.POST_PROPS_OVERRIDE_ICON_URL, emojiUrl)
|
||||
} else {
|
||||
mlog.Warn("Failed to retrieve URL for overriden profile icon (emoji)", mlog.String("emojiName", emojiName), mlog.Err(err))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *App) PreparePostForClient(originalPost *model.Post, isNewPost bool, isEditPost bool) *model.Post {
|
||||
post := originalPost.Clone()
|
||||
|
||||
// Proxy image links before constructing metadata so that requests go through the proxy
|
||||
post = a.PostWithProxyAddedToImageURLs(post)
|
||||
|
||||
a.OverrideIconURLIfEmoji(post)
|
||||
|
||||
post.Metadata = &model.PostMetadata{}
|
||||
|
||||
// Emojis and reaction counts
|
||||
|
||||
@@ -235,6 +235,49 @@ func TestPreparePostForClient(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("emojis overriding profile icon", func(t *testing.T) {
|
||||
th := setup()
|
||||
defer th.TearDown()
|
||||
|
||||
prepare := func(override bool, url, emoji string) *model.Post {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnablePostIconOverride = override
|
||||
})
|
||||
|
||||
post, err := th.App.CreatePost(&model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
Message: "Test",
|
||||
}, th.BasicChannel, false)
|
||||
|
||||
require.Nil(t, err)
|
||||
|
||||
post.AddProp(model.POST_PROPS_OVERRIDE_ICON_URL, url)
|
||||
post.AddProp(model.POST_PROPS_OVERRIDE_ICON_EMOJI, emoji)
|
||||
|
||||
return th.App.PreparePostForClient(post, false, false)
|
||||
}
|
||||
|
||||
emoji := "basketball"
|
||||
url := "http://host.com/image.png"
|
||||
overridenUrl := "/static/emoji/1f3c0.png"
|
||||
|
||||
t.Run("does not override icon URL", func(t *testing.T) {
|
||||
clientPost := prepare(false, url, emoji)
|
||||
|
||||
s, _ := clientPost.Props[model.POST_PROPS_OVERRIDE_ICON_URL]
|
||||
assert.EqualValues(t, url, s)
|
||||
})
|
||||
|
||||
t.Run("overrides icon URL", func(t *testing.T) {
|
||||
clientPost := prepare(true, url, emoji)
|
||||
|
||||
s, _ := clientPost.Props[model.POST_PROPS_OVERRIDE_ICON_URL]
|
||||
assert.EqualValues(t, overridenUrl, s)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
t.Run("markdown image dimensions", func(t *testing.T) {
|
||||
th := setup()
|
||||
defer th.TearDown()
|
||||
|
||||
@@ -138,7 +138,7 @@ func (a *App) TriggerWebhook(payload *model.OutgoingWebhookPayload, hook *model.
|
||||
if *a.Config().ServiceSettings.EnablePostIconOverride && hook.IconURL != "" && webhookResp.IconURL == "" {
|
||||
webhookResp.IconURL = hook.IconURL
|
||||
}
|
||||
if _, err := a.CreateWebhookPost(hook.CreatorId, channel, text, webhookResp.Username, webhookResp.IconURL, webhookResp.Props, webhookResp.Type, postRootId); err != nil {
|
||||
if _, err := a.CreateWebhookPost(hook.CreatorId, channel, text, webhookResp.Username, webhookResp.IconURL, "", webhookResp.Props, webhookResp.Type, postRootId); err != nil {
|
||||
mlog.Error(fmt.Sprintf("Failed to create response post, err=%v", err))
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func SplitWebhookPost(post *model.Post, maxPostSize int) ([]*model.Post, *model.
|
||||
return splits, nil
|
||||
}
|
||||
|
||||
func (a *App) CreateWebhookPost(userId string, channel *model.Channel, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) {
|
||||
func (a *App) CreateWebhookPost(userId string, channel *model.Channel, text, overrideUsername, overrideIconUrl, overrideIconEmoji string, props model.StringInterface, postType string, postRootId string) (*model.Post, *model.AppError) {
|
||||
// parse links into Markdown format
|
||||
linkWithTextRegex := regexp.MustCompile(`<([^\n<\|>]+)\|([^\n>]+)>`)
|
||||
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
||||
@@ -274,6 +274,9 @@ func (a *App) CreateWebhookPost(userId string, channel *model.Channel, text, ove
|
||||
if len(overrideIconUrl) != 0 {
|
||||
post.AddProp("override_icon_url", overrideIconUrl)
|
||||
}
|
||||
if len(overrideIconEmoji) != 0 {
|
||||
post.AddProp("override_icon_emoji", overrideIconEmoji)
|
||||
}
|
||||
}
|
||||
|
||||
if len(props) > 0 {
|
||||
@@ -660,7 +663,7 @@ func (a *App) HandleIncomingWebhook(hookId string, req *model.IncomingWebhookReq
|
||||
overrideIconUrl = req.IconURL
|
||||
}
|
||||
|
||||
_, err := a.CreateWebhookPost(hook.UserId, channel, text, overrideUsername, overrideIconUrl, req.Props, webhookType, "")
|
||||
_, err := a.CreateWebhookPost(hook.UserId, channel, text, overrideUsername, overrideIconUrl, req.IconEmoji, req.Props, webhookType, "")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ func TestCreateWebhookPost(t *testing.T) {
|
||||
}
|
||||
defer th.App.DeleteIncomingWebhook(hook.Id)
|
||||
|
||||
post, err := th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", model.StringInterface{
|
||||
post, err := th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", model.StringInterface{
|
||||
"attachments": []*model.SlackAttachment{
|
||||
{
|
||||
Text: "text",
|
||||
@@ -319,13 +319,13 @@ func TestCreateWebhookPost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
_, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", nil, model.POST_SYSTEM_GENERIC, "")
|
||||
_, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, "foo", "user", "http://iconurl", "", nil, model.POST_SYSTEM_GENERIC, "")
|
||||
if err == nil {
|
||||
t.Fatal("should have failed - bad post type")
|
||||
}
|
||||
|
||||
expectedText := "`<>|<>|`"
|
||||
post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{
|
||||
post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
|
||||
"attachments": []*model.SlackAttachment{
|
||||
{
|
||||
Text: "text",
|
||||
@@ -339,7 +339,7 @@ func TestCreateWebhookPost(t *testing.T) {
|
||||
assert.Equal(t, expectedText, post.Message)
|
||||
|
||||
expectedText = "< | \n|\n>"
|
||||
post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{
|
||||
post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
|
||||
"attachments": []*model.SlackAttachment{
|
||||
{
|
||||
Text: "text",
|
||||
@@ -369,7 +369,7 @@ Date: Thu Mar 1 19:46:48 2018 +0300
|
||||
|
||||
test | 3 +++
|
||||
1 file changed, 3 insertions(+)`
|
||||
post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", model.StringInterface{
|
||||
post, err = th.App.CreateWebhookPost(hook.UserId, th.BasicChannel, expectedText, "user", "http://iconurl", "", model.StringInterface{
|
||||
"attachments": []*model.SlackAttachment{
|
||||
{
|
||||
Text: "text",
|
||||
|
||||
Ссылка в новой задаче
Block a user