From ff2fe1e62c4aae7ad3b0525f2cdfc6e08184cf80 Mon Sep 17 00:00:00 2001 From: Manoj <77336594+manojmalik20@users.noreply.github.com> Date: Wed, 26 May 2021 23:33:08 +0530 Subject: [PATCH] Fixed MM-33249 issue (#17475) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added logic to detect and set unicode emoji in the custom status slash command * Replaced strings.split with strings.Fields * Added logic to handle empty string as message in custom status slash command * Changed custom status slash command empty message behavior to set def… (#14) * Changed custom status slash command empty message behavior to set default emoji * Code refactoring * Added unit tests and refactored some code * WIP: Unit tests and refactoring for detecting unicode emoji in custom status slash commands * Complete unit testing for Get custom status * Fixed lint * Added logic for removing skin tone from unicode emoji (#16) * Added logic for removing skin tone from unicode emoji Made a reverse system emojis map of string vs []string and stored the emojiNames in sorted order Added the logic for detecting and replacing/removing skin tone in unicode emoji with variation selector Added new unit tests with different skin tone emojis * Refactored removeSkinTone logic to a separate function * Added check for emoji before removing skin tone in custom status slash command * Fixed custom status slash command unit test and refactored some code Chanded the return type of GetEmojiNameFromUnicode from bool to int Changed the logic for checking presence of emoji without removing skin tone Fixed the unit tests * Review fixes: Indentation changes --- app/slashcommands/command_custom_status.go | 85 ++++++++++++++++--- .../command_custom_status_test.go | 34 ++++++++ model/custom_status.go | 5 ++ model/emoji.go | 23 +++++ 4 files changed, 133 insertions(+), 14 deletions(-) create mode 100644 app/slashcommands/command_custom_status_test.go diff --git a/app/slashcommands/command_custom_status.go b/app/slashcommands/command_custom_status.go index 8851bfbcf5..926812ad47 100644 --- a/app/slashcommands/command_custom_status.go +++ b/app/slashcommands/command_custom_status.go @@ -4,7 +4,9 @@ package slashcommands import ( + "regexp" "strings" + "unicode/utf8" "github.com/mattermost/mattermost-server/v5/app" "github.com/mattermost/mattermost-server/v5/app/request" @@ -49,7 +51,7 @@ func (*CustomStatusProvider) DoCommand(a *app.App, c *request.Context, args *mod message = strings.TrimSpace(message) if message == CmdCustomStatusClear { if err := a.RemoveCustomStatus(args.UserId); err != nil { - mlog.Error(err.Error()) + mlog.Debug(err.Error()) return &model.CommandResponse{Text: args.T("api.command_custom_status.clear.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } @@ -59,20 +61,9 @@ func (*CustomStatusProvider) DoCommand(a *app.App, c *request.Context, args *mod } } - customStatus := &model.CustomStatus{ - Emoji: DefaultCustomStatusEmoji, - Text: message, - } - firstEmojiLocations := model.ALL_EMOJI_PATTERN.FindIndex([]byte(message)) - if len(firstEmojiLocations) > 0 && firstEmojiLocations[0] == 0 { - // emoji found at starting index - customStatus.Emoji = message[firstEmojiLocations[0]+1 : firstEmojiLocations[1]-1] - customStatus.Text = strings.TrimSpace(message[firstEmojiLocations[1]:]) - } - - customStatus.TrimMessage() + customStatus := GetCustomStatus(message) if err := a.SetCustomStatus(args.UserId, customStatus); err != nil { - mlog.Error(err.Error()) + mlog.Debug(err.Error()) return &model.CommandResponse{Text: args.T("api.command_custom_status.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } @@ -84,3 +75,69 @@ func (*CustomStatusProvider) DoCommand(a *app.App, c *request.Context, args *mod }), } } + +func GetCustomStatus(message string) *model.CustomStatus { + customStatus := &model.CustomStatus{ + Emoji: DefaultCustomStatusEmoji, + Text: message, + } + + firstEmojiLocations := model.ALL_EMOJI_PATTERN.FindIndex([]byte(message)) + if len(firstEmojiLocations) > 0 && firstEmojiLocations[0] == 0 { + // emoji found at starting index + customStatus.Emoji = message[firstEmojiLocations[0]+1 : firstEmojiLocations[1]-1] + customStatus.Text = strings.TrimSpace(message[firstEmojiLocations[1]:]) + customStatus.TrimMessage() + return customStatus + } + + if message == "" { + return customStatus + } + + spaceSeparatedMessage := strings.Fields(message) + if len(spaceSeparatedMessage) == 0 { + return customStatus + } + + emojiString := spaceSeparatedMessage[0] + var unicode []string + for utf8.RuneCountInString(emojiString) >= 1 { + codepoint, size := utf8.DecodeRuneInString(emojiString) + code := model.RuneToHexadecimalString(codepoint) + unicode = append(unicode, code) + emojiString = emojiString[size:] + } + + unicodeString := removeUnicodeSkinTone(strings.Join(unicode, "-")) + emoji, count := model.GetEmojiNameFromUnicode(unicodeString) + if count > 0 { + customStatus.Emoji = emoji + textString := strings.Join(spaceSeparatedMessage[1:], " ") + customStatus.Text = strings.TrimSpace(textString) + } + + customStatus.TrimMessage() + return customStatus +} + +func removeUnicodeSkinTone(unicodeString string) string { + skinToneDetectorRegex := regexp.MustCompile("-(1f3fb|1f3fc|1f3fd|1f3fe|1f3ff)") + skinToneLocations := skinToneDetectorRegex.FindIndex([]byte(unicodeString)) + + if len(skinToneLocations) == 0 { + return unicodeString + } + if _, count := model.GetEmojiNameFromUnicode(unicodeString); count == 1 { + return unicodeString + } + unicodeWithRemovedSkinTone := unicodeString[:skinToneLocations[0]] + unicodeString[skinToneLocations[1]:] + unicodeWithVariationSelector := unicodeString[:skinToneLocations[0]] + "-fe0f" + unicodeString[skinToneLocations[1]:] + if _, count := model.GetEmojiNameFromUnicode(unicodeWithRemovedSkinTone); count > 0 { + unicodeString = unicodeWithRemovedSkinTone + } else if _, count := model.GetEmojiNameFromUnicode(unicodeWithVariationSelector); count > 0 { + unicodeString = unicodeWithVariationSelector + } + + return unicodeString +} diff --git a/app/slashcommands/command_custom_status_test.go b/app/slashcommands/command_custom_status_test.go new file mode 100644 index 0000000000..4e99dad52e --- /dev/null +++ b/app/slashcommands/command_custom_status_test.go @@ -0,0 +1,34 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package slashcommands + +import ( + "testing" + + "github.com/mattermost/mattermost-server/v5/model" +) + +func TestGetCustomStatus(t *testing.T) { + for msg, expected := range map[string]model.CustomStatus{ + "": {Emoji: DefaultCustomStatusEmoji, Text: ""}, + "Hey": {Emoji: DefaultCustomStatusEmoji, Text: "Hey"}, + ":cactus: Hurt": {Emoji: "cactus", Text: "Hurt"}, + "πŸ‘…": {Emoji: "tongue", Text: ""}, + "πŸ‘… Eating": {Emoji: "tongue", Text: "Eating"}, + "πŸ’ͺ🏻 Working out": {Emoji: "muscle_light_skin_tone", Text: "Working out"}, + "πŸ‘™ Swimming": {Emoji: "bikini", Text: "Swimming"}, + "πŸ‘™Swimming": {Emoji: DefaultCustomStatusEmoji, Text: "πŸ‘™Swimming"}, + "πŸ‘πŸΏ Okay": {Emoji: "+1_dark_skin_tone", Text: "Okay"}, + "🀴🏾 Dark king": {Emoji: "prince_medium_dark_skin_tone", Text: "Dark king"}, + "β›ΉπŸΎβ€β™€οΈ Playing basketball": {Emoji: "basketball_woman", Text: "Playing basketball"}, + "πŸ‹πŸΏβ€β™€οΈ Weightlifting": {Emoji: "weight_lifting_woman", Text: "Weightlifting"}, + "πŸ„ Surfing": {Emoji: "surfer", Text: "Surfing"}, + "πŸ‘¨β€πŸ‘¨β€πŸ‘¦β€πŸ‘¦ Family": {Emoji: "family_man_man_boy_boy", Text: "Family"}, + } { + actual := GetCustomStatus(msg) + if actual.Emoji != expected.Emoji || actual.Text != expected.Text { + t.Errorf("expected `%v`, got `%v`", expected, *actual) + } + } +} diff --git a/model/custom_status.go b/model/custom_status.go index 5479144d26..68e78e4d29 100644 --- a/model/custom_status.go +++ b/model/custom_status.go @@ -5,6 +5,7 @@ package model import ( "encoding/json" + "fmt" "io" ) @@ -39,6 +40,10 @@ func CustomStatusFromJson(data io.Reader) *CustomStatus { return cs } +func RuneToHexadecimalString(r rune) string { + return fmt.Sprintf("%04x", r) +} + type RecentCustomStatuses []CustomStatus func (rcs *RecentCustomStatuses) Contains(cs *CustomStatus) bool { diff --git a/model/emoji.go b/model/emoji.go index bcc35e9ff3..bc9b23b891 100644 --- a/model/emoji.go +++ b/model/emoji.go @@ -8,6 +8,7 @@ import ( "io" "net/http" "regexp" + "sort" ) const ( @@ -23,6 +24,8 @@ var EMOJI_PATTERN = regexp.MustCompile(`:[a-zA-Z0-9_-]+:`) // TODO: Merge ALL_EMOJI_PATTERN with EMOJI_PATTERN after updating custom emoji help texts var ALL_EMOJI_PATTERN = regexp.MustCompile(`:[a-zA-Z0-9_+-]+:`) +var ReverseSystemEmojisMap = makeReverseEmojiMap() + type Emoji struct { Id string `json:"id"` CreateAt int64 `json:"create_at"` @@ -42,6 +45,26 @@ func GetSystemEmojiId(emojiName string) (string, bool) { return id, found } +func makeReverseEmojiMap() map[string][]string { + reverseEmojiMap := make(map[string][]string) + for key, value := range SystemEmojis { + emojiNames := reverseEmojiMap[value] + emojiNames = append(emojiNames, key) + sort.Strings(emojiNames) + reverseEmojiMap[value] = emojiNames + } + + return reverseEmojiMap +} + +func GetEmojiNameFromUnicode(unicode string) (emojiName string, count int) { + if emojiNames, found := ReverseSystemEmojisMap[unicode]; found { + return emojiNames[0], len(emojiNames) + } + + return "", 0 +} + func (emoji *Emoji) IsValid() *AppError { if !IsValidId(emoji.Id) { return NewAppError("Emoji.IsValid", "model.emoji.id.app_error", nil, "", http.StatusBadRequest)