mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-54201 Refactor mention parsing in preparation for multi-word mentions (#25030)
* MM-54201 Move ExplicitMentions to its own file and rename it (#24932) * MM-54201 Move ExplicitMentions to its own file and rename it * Fix vet * MM-54201 Refactor current mention parsing into MentionParserStandard (#24936) * MM-54201 Refactor current mention parsing into MentionParserStandard * Fix vet * MM-54201 Unify user and group mention parsing logic (#24937) * MM-54201 Add MentionKeywords type * MM-54201 Move group mentions into MentionKeywords * Fix flaky test caused by random iteration order * Update server/channels/app/mention_results.go Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> * Address feedback --------- Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com> --------- Co-authored-by: Jesse Hallam <jesse.hallam@gmail.com>
This commit is contained in:
parent
74f35aa92c
commit
a78710c2a6
110
server/channels/app/mention_keywords.go
Normal file
110
server/channels/app/mention_keywords.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
const (
|
||||
mentionableUserPrefix = "user:"
|
||||
mentionableGroupPrefix = "group:"
|
||||
)
|
||||
|
||||
// A MentionableID stores the ID of a single User/Group with information about which type of object it refers to.
|
||||
type MentionableID string
|
||||
|
||||
func MentionableUserID(userID string) MentionableID {
|
||||
return MentionableID(fmt.Sprint(mentionableUserPrefix, userID))
|
||||
}
|
||||
|
||||
func MentionableGroupID(groupID string) MentionableID {
|
||||
return MentionableID(fmt.Sprint(mentionableGroupPrefix, groupID))
|
||||
}
|
||||
|
||||
func (id MentionableID) AsUserID() (userID string, ok bool) {
|
||||
idString := string(id)
|
||||
if !strings.HasPrefix(idString, mentionableUserPrefix) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return idString[len(mentionableUserPrefix):], true
|
||||
}
|
||||
|
||||
func (id MentionableID) AsGroupID() (groupID string, ok bool) {
|
||||
idString := string(id)
|
||||
if !strings.HasPrefix(idString, mentionableGroupPrefix) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return idString[len(mentionableGroupPrefix):], true
|
||||
}
|
||||
|
||||
// MentionKeywords is a collection of mention keywords and the IDs of the objects that have a given keyword.
|
||||
type MentionKeywords map[string][]MentionableID
|
||||
|
||||
func (k MentionKeywords) AddUser(profile *model.User, channelNotifyProps map[string]string, status *model.Status, allowChannelMentions bool) MentionKeywords {
|
||||
mentionableID := MentionableUserID(profile.Id)
|
||||
|
||||
userMention := "@" + strings.ToLower(profile.Username)
|
||||
k[userMention] = append(k[userMention], mentionableID)
|
||||
|
||||
// Add all the user's mention keys
|
||||
for _, mentionKey := range profile.GetMentionKeys() {
|
||||
if mentionKey != "" {
|
||||
// Note that these are made lower case so that we can do a case insensitive check for them
|
||||
mentionKey = strings.ToLower(mentionKey)
|
||||
|
||||
k[mentionKey] = append(k[mentionKey], mentionableID)
|
||||
}
|
||||
}
|
||||
|
||||
// If turned on, add the user's case sensitive first name
|
||||
if profile.NotifyProps[model.FirstNameNotifyProp] == "true" && profile.FirstName != "" {
|
||||
k[profile.FirstName] = append(k[profile.FirstName], mentionableID)
|
||||
}
|
||||
|
||||
// Add @channel and @all to k if user has them turned on and the server allows them
|
||||
if allowChannelMentions {
|
||||
// Ignore channel mentions if channel is muted and channel mention setting is default
|
||||
ignoreChannelMentions := channelNotifyProps[model.IgnoreChannelMentionsNotifyProp] == model.IgnoreChannelMentionsOn || (channelNotifyProps[model.MarkUnreadNotifyProp] == model.UserNotifyMention && channelNotifyProps[model.IgnoreChannelMentionsNotifyProp] == model.IgnoreChannelMentionsDefault)
|
||||
|
||||
if profile.NotifyProps[model.ChannelMentionsNotifyProp] == "true" && !ignoreChannelMentions {
|
||||
k["@channel"] = append(k["@channel"], mentionableID)
|
||||
k["@all"] = append(k["@all"], mentionableID)
|
||||
|
||||
if status != nil && status.Status == model.StatusOnline {
|
||||
k["@here"] = append(k["@here"], mentionableID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
func (k MentionKeywords) AddUserKeyword(userID string, keyword string) MentionKeywords {
|
||||
k[keyword] = append(k[keyword], MentionableUserID(userID))
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
func (k MentionKeywords) AddGroup(group *model.Group) MentionKeywords {
|
||||
if group.Name != nil {
|
||||
keyword := "@" + *group.Name
|
||||
k[keyword] = append(k[keyword], MentionableGroupID(group.Id))
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
func (k MentionKeywords) AddGroupsMap(groups map[string]*model.Group) MentionKeywords {
|
||||
for _, group := range groups {
|
||||
k.AddGroup(group)
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
292
server/channels/app/mention_keywords_test.go
Normal file
292
server/channels/app/mention_keywords_test.go
Normal file
@ -0,0 +1,292 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func mapsToMentionKeywords(userKeywords map[string][]string, groups map[string]*model.Group) MentionKeywords {
|
||||
keywords := make(MentionKeywords, len(userKeywords)+len(groups))
|
||||
|
||||
for keyword, ids := range userKeywords {
|
||||
for _, id := range ids {
|
||||
keywords[keyword] = append(keywords[keyword], MentionableUserID(id))
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range groups {
|
||||
keyword := "@" + *group.Name
|
||||
keywords[keyword] = append(keywords[keyword], MentionableGroupID(group.Id))
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
func TestMentionKeywords_AddUserProfile(t *testing.T) {
|
||||
t.Run("should add @user", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, nil, false)
|
||||
|
||||
assert.Contains(t, keywords["@user"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should add custom mention keywords", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
NotifyProps: map[string]string{
|
||||
model.MentionKeysNotifyProp: "apple,BANANA,OrAnGe",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, nil, false)
|
||||
|
||||
assert.Contains(t, keywords["apple"], MentionableUserID(user.Id))
|
||||
assert.Contains(t, keywords["banana"], MentionableUserID(user.Id))
|
||||
assert.Contains(t, keywords["orange"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should not add empty custom keywords", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
NotifyProps: map[string]string{
|
||||
model.MentionKeysNotifyProp: ",,",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, nil, false)
|
||||
|
||||
assert.Nil(t, keywords[""])
|
||||
})
|
||||
|
||||
t.Run("should add case sensitive first name if enabled", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
FirstName: "William",
|
||||
LastName: "Robert",
|
||||
NotifyProps: map[string]string{
|
||||
model.FirstNameNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, nil, false)
|
||||
|
||||
assert.Contains(t, keywords["William"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["william"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["Robert"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should not add case sensitive first name if enabled but empty First Name", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
FirstName: "",
|
||||
LastName: "Robert",
|
||||
NotifyProps: map[string]string{
|
||||
model.FirstNameNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, nil, false)
|
||||
|
||||
assert.NotContains(t, keywords[""], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should not add case sensitive first name if disabled", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
FirstName: "William",
|
||||
LastName: "Robert",
|
||||
NotifyProps: map[string]string{
|
||||
model.FirstNameNotifyProp: "false",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, nil, false)
|
||||
|
||||
assert.NotContains(t, keywords["William"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["william"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["Robert"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should add @channel/@all/@here when allowed", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
NotifyProps: map[string]string{
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
status := &model.Status{
|
||||
Status: model.StatusOnline,
|
||||
}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, status, true)
|
||||
|
||||
assert.Contains(t, keywords["@channel"], MentionableUserID(user.Id))
|
||||
assert.Contains(t, keywords["@all"], MentionableUserID(user.Id))
|
||||
assert.Contains(t, keywords["@here"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should not add @channel/@all/@here when not allowed", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
NotifyProps: map[string]string{
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
status := &model.Status{
|
||||
Status: model.StatusOnline,
|
||||
}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, status, false)
|
||||
|
||||
assert.NotContains(t, keywords["@channel"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@all"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@here"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should not add @channel/@all/@here when disabled for user", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
NotifyProps: map[string]string{
|
||||
model.ChannelMentionsNotifyProp: "false",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
status := &model.Status{
|
||||
Status: model.StatusOnline,
|
||||
}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, status, true)
|
||||
|
||||
assert.NotContains(t, keywords["@channel"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@all"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@here"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should not add @channel/@all/@here when disabled for channel", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
NotifyProps: map[string]string{
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{
|
||||
model.IgnoreChannelMentionsNotifyProp: model.IgnoreChannelMentionsOn,
|
||||
}
|
||||
status := &model.Status{
|
||||
Status: model.StatusOnline,
|
||||
}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, status, true)
|
||||
|
||||
assert.NotContains(t, keywords["@channel"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@all"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@here"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should not add @channel/@all/@here when channel is muted and channel mention setting is not updated by user", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
NotifyProps: map[string]string{
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{
|
||||
model.MarkUnreadNotifyProp: model.UserNotifyMention,
|
||||
model.IgnoreChannelMentionsNotifyProp: model.IgnoreChannelMentionsDefault,
|
||||
}
|
||||
status := &model.Status{
|
||||
Status: model.StatusOnline,
|
||||
}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, status, true)
|
||||
|
||||
assert.NotContains(t, keywords["@channel"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@all"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@here"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should not add @here when when user is not online", func(t *testing.T) {
|
||||
user := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user",
|
||||
NotifyProps: map[string]string{
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
channelNotifyProps := map[string]string{}
|
||||
status := &model.Status{
|
||||
Status: model.StatusAway,
|
||||
}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user, channelNotifyProps, status, true)
|
||||
|
||||
assert.Contains(t, keywords["@channel"], MentionableUserID(user.Id))
|
||||
assert.Contains(t, keywords["@all"], MentionableUserID(user.Id))
|
||||
assert.NotContains(t, keywords["@here"], MentionableUserID(user.Id))
|
||||
})
|
||||
|
||||
t.Run("should add for multiple users", func(t *testing.T) {
|
||||
user1 := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user1",
|
||||
NotifyProps: map[string]string{
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
user2 := &model.User{
|
||||
Id: model.NewId(),
|
||||
Username: "user2",
|
||||
NotifyProps: map[string]string{
|
||||
model.ChannelMentionsNotifyProp: "true",
|
||||
},
|
||||
}
|
||||
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(user1, map[string]string{}, nil, true)
|
||||
keywords.AddUser(user2, map[string]string{}, nil, true)
|
||||
|
||||
assert.Contains(t, keywords["@user1"], MentionableUserID(user1.Id))
|
||||
assert.Contains(t, keywords["@user2"], MentionableUserID(user2.Id))
|
||||
assert.Contains(t, keywords["@all"], MentionableUserID(user1.Id))
|
||||
assert.Contains(t, keywords["@all"], MentionableUserID(user2.Id))
|
||||
})
|
||||
}
|
9
server/channels/app/mention_parser.go
Normal file
9
server/channels/app/mention_parser.go
Normal file
@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
type MentionParser interface {
|
||||
ProcessText(text string)
|
||||
Results() *MentionResults
|
||||
}
|
160
server/channels/app/mention_parser_standard.go
Normal file
160
server/channels/app/mention_parser_standard.go
Normal file
@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Have the compiler confirm *StandardMentionParser implements MentionParser
|
||||
var _ MentionParser = &StandardMentionParser{}
|
||||
|
||||
type StandardMentionParser struct {
|
||||
keywords MentionKeywords
|
||||
|
||||
results *MentionResults
|
||||
}
|
||||
|
||||
func makeStandardMentionParser(keywords MentionKeywords) *StandardMentionParser {
|
||||
return &StandardMentionParser{
|
||||
keywords: keywords,
|
||||
|
||||
results: &MentionResults{},
|
||||
}
|
||||
}
|
||||
|
||||
// Processes text to filter mentioned users and other potential mentions
|
||||
func (p *StandardMentionParser) ProcessText(text string) {
|
||||
systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
|
||||
|
||||
for _, word := range strings.FieldsFunc(text, func(c rune) bool {
|
||||
// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
|
||||
return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
|
||||
}) {
|
||||
// skip word with format ':word:' with an assumption that it is an emoji format only
|
||||
if word[0] == ':' && word[len(word)-1] == ':' {
|
||||
continue
|
||||
}
|
||||
|
||||
word = strings.TrimLeft(word, ":.-_")
|
||||
|
||||
if p.checkForMention(word) {
|
||||
continue
|
||||
}
|
||||
|
||||
foundWithoutSuffix := false
|
||||
wordWithoutSuffix := word
|
||||
|
||||
for wordWithoutSuffix != "" && strings.LastIndexAny(wordWithoutSuffix, ".-:_") == (len(wordWithoutSuffix)-1) {
|
||||
wordWithoutSuffix = wordWithoutSuffix[0 : len(wordWithoutSuffix)-1]
|
||||
|
||||
if p.checkForMention(wordWithoutSuffix) {
|
||||
foundWithoutSuffix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundWithoutSuffix {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
|
||||
// No need to bother about unicode as we are looking for ASCII characters.
|
||||
last := word[len(word)-1]
|
||||
switch last {
|
||||
// If the word is possibly at the end of a sentence, remove that character.
|
||||
case '.', '-', ':':
|
||||
word = word[:len(word)-1]
|
||||
}
|
||||
p.results.OtherPotentialMentions = append(p.results.OtherPotentialMentions, word[1:])
|
||||
} else if strings.ContainsAny(word, ".-:") {
|
||||
// This word contains a character that may be the end of a sentence, so split further
|
||||
splitWords := strings.FieldsFunc(word, func(c rune) bool {
|
||||
return c == '.' || c == '-' || c == ':'
|
||||
})
|
||||
|
||||
for _, splitWord := range splitWords {
|
||||
if p.checkForMention(splitWord) {
|
||||
continue
|
||||
}
|
||||
if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
|
||||
p.results.OtherPotentialMentions = append(p.results.OtherPotentialMentions, splitWord[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ids, match := isKeywordMultibyte(p.keywords, word); match {
|
||||
p.addMentions(ids, KeywordMention)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *StandardMentionParser) Results() *MentionResults {
|
||||
return p.results
|
||||
}
|
||||
|
||||
// checkForMention checks if there is a mention to a specific user or to the keywords here / channel / all
|
||||
func (p *StandardMentionParser) checkForMention(word string) bool {
|
||||
var mentionType MentionType
|
||||
|
||||
switch strings.ToLower(word) {
|
||||
case "@here":
|
||||
p.results.HereMentioned = true
|
||||
mentionType = ChannelMention
|
||||
case "@channel":
|
||||
p.results.ChannelMentioned = true
|
||||
mentionType = ChannelMention
|
||||
case "@all":
|
||||
p.results.AllMentioned = true
|
||||
mentionType = ChannelMention
|
||||
default:
|
||||
mentionType = KeywordMention
|
||||
}
|
||||
|
||||
if ids, match := p.keywords[strings.ToLower(word)]; match {
|
||||
p.addMentions(ids, mentionType)
|
||||
return true
|
||||
}
|
||||
|
||||
// Case-sensitive check for first name
|
||||
if ids, match := p.keywords[word]; match {
|
||||
p.addMentions(ids, mentionType)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *StandardMentionParser) addMentions(ids []MentionableID, mentionType MentionType) {
|
||||
for _, id := range ids {
|
||||
if userID, ok := id.AsUserID(); ok {
|
||||
p.results.addMention(userID, mentionType)
|
||||
} else if groupID, ok := id.AsGroupID(); ok {
|
||||
p.results.addGroupMention(groupID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isKeywordMultibyte checks if a word containing a multibyte character contains a multibyte keyword
|
||||
func isKeywordMultibyte(keywords MentionKeywords, word string) ([]MentionableID, bool) {
|
||||
ids := []MentionableID{}
|
||||
match := false
|
||||
var multibyteKeywords []string
|
||||
for keyword := range keywords {
|
||||
if len(keyword) != utf8.RuneCountInString(keyword) {
|
||||
multibyteKeywords = append(multibyteKeywords, keyword)
|
||||
}
|
||||
}
|
||||
|
||||
if len(word) != utf8.RuneCountInString(word) {
|
||||
for _, key := range multibyteKeywords {
|
||||
if strings.Contains(word, key) {
|
||||
ids, match = keywords[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids, match
|
||||
}
|
433
server/channels/app/mention_parser_standard_test.go
Normal file
433
server/channels/app/mention_parser_standard_test.go
Normal file
@ -0,0 +1,433 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsKeywordMultibyte(t *testing.T) {
|
||||
id1 := model.NewId()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
Message string
|
||||
Attachments []*model.SlackAttachment
|
||||
Keywords map[string][]string
|
||||
Groups map[string]*model.Group
|
||||
Expected *MentionResults
|
||||
}{
|
||||
"MultibyteCharacter": {
|
||||
Message: "My name is 萌",
|
||||
Keywords: map[string][]string{"萌": {id1}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterWithNoUser": {
|
||||
Message: "My name is 萌",
|
||||
Keywords: map[string][]string{"萌": {}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: nil,
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterAtBeginningOfSentence": {
|
||||
Message: "이메일을 보내다.",
|
||||
Keywords: map[string][]string{"이메일": {id1}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterAtBeginningOfSentenceWithNoUser": {
|
||||
Message: "이메일을 보내다.",
|
||||
Keywords: map[string][]string{"이메일": {}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: nil,
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterInPartOfSentence": {
|
||||
Message: "我爱吃番茄炒饭",
|
||||
Keywords: map[string][]string{"番茄": {id1}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterInPartOfSentenceWithNoUser": {
|
||||
Message: "我爱吃番茄炒饭",
|
||||
Keywords: map[string][]string{"番茄": {}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: nil,
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterAtEndOfSentence": {
|
||||
Message: "こんにちは、世界",
|
||||
Keywords: map[string][]string{"世界": {id1}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterAtEndOfSentenceWithNoUser": {
|
||||
Message: "こんにちは、世界",
|
||||
Keywords: map[string][]string{"世界": {}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: nil,
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterTwiceInSentence": {
|
||||
Message: "石橋さんが石橋を渡る",
|
||||
Keywords: map[string][]string{"石橋": {id1}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"MultibyteCharacterTwiceInSentenceWithNoUser": {
|
||||
Message: "石橋さんが石橋を渡る",
|
||||
Keywords: map[string][]string{"石橋": {}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: nil,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
post := &model.Post{
|
||||
Message: tc.Message,
|
||||
Props: model.StringInterface{
|
||||
"attachments": tc.Attachments,
|
||||
},
|
||||
}
|
||||
|
||||
m := getExplicitMentions(post, mapsToMentionKeywords(tc.Keywords, tc.Groups))
|
||||
assert.EqualValues(t, tc.Expected, m)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForMentionUsers(t *testing.T) {
|
||||
id1 := model.NewId()
|
||||
id2 := model.NewId()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
Word string
|
||||
Attachments []*model.SlackAttachment
|
||||
Keywords map[string][]string
|
||||
Expected *MentionResults
|
||||
}{
|
||||
"Nobody": {
|
||||
Word: "nothing",
|
||||
Keywords: map[string][]string{},
|
||||
Expected: &MentionResults{},
|
||||
},
|
||||
"UppercaseUser1": {
|
||||
Word: "@User",
|
||||
Keywords: map[string][]string{"@user": {id1}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"LowercaseUser1": {
|
||||
Word: "@user",
|
||||
Keywords: map[string][]string{"@user": {id1}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"LowercaseUser2": {
|
||||
Word: "@user2",
|
||||
Keywords: map[string][]string{"@user2": {id2}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id2: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"UppercaseUser2": {
|
||||
Word: "@UsEr2",
|
||||
Keywords: map[string][]string{"@user2": {id2}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
id2: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"HereMention": {
|
||||
Word: "@here",
|
||||
Expected: &MentionResults{
|
||||
HereMentioned: true,
|
||||
},
|
||||
},
|
||||
"ChannelMention": {
|
||||
Word: "@channel",
|
||||
Expected: &MentionResults{
|
||||
ChannelMentioned: true,
|
||||
},
|
||||
},
|
||||
"AllMention": {
|
||||
Word: "@all",
|
||||
Expected: &MentionResults{
|
||||
AllMentioned: true,
|
||||
},
|
||||
},
|
||||
"UppercaseHere": {
|
||||
Word: "@HeRe",
|
||||
Expected: &MentionResults{
|
||||
HereMentioned: true,
|
||||
},
|
||||
},
|
||||
"UppercaseChannel": {
|
||||
Word: "@ChaNNel",
|
||||
Expected: &MentionResults{
|
||||
ChannelMentioned: true,
|
||||
},
|
||||
},
|
||||
"UppercaseAll": {
|
||||
Word: "@ALL",
|
||||
Expected: &MentionResults{
|
||||
AllMentioned: true,
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := makeStandardMentionParser(mapsToMentionKeywords(tc.Keywords, nil))
|
||||
p.checkForMention(tc.Word)
|
||||
|
||||
assert.EqualValues(t, tc.Expected, p.Results())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForMentionGroups(t *testing.T) {
|
||||
groupID1 := model.NewId()
|
||||
groupID2 := model.NewId()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
Word string
|
||||
Groups map[string]*model.Group
|
||||
Expected *MentionResults
|
||||
}{
|
||||
"No groups": {
|
||||
Word: "nothing",
|
||||
Groups: map[string]*model.Group{},
|
||||
Expected: &MentionResults{},
|
||||
},
|
||||
"No matching groups": {
|
||||
Word: "nothing",
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{},
|
||||
},
|
||||
"matching group with no @": {
|
||||
Word: "engineering",
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{},
|
||||
},
|
||||
"matching group with preceding @": {
|
||||
Word: "@engineering",
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
GroupMentions: map[string]MentionType{
|
||||
groupID1: GroupMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"matching upper case group with preceding @": {
|
||||
Word: "@Engineering",
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
GroupMentions: map[string]MentionType{
|
||||
groupID1: GroupMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := makeStandardMentionParser(mapsToMentionKeywords(nil, tc.Groups))
|
||||
p.checkForMention(tc.Word)
|
||||
|
||||
mr := p.Results()
|
||||
|
||||
assert.EqualValues(t, tc.Expected, mr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessText(t *testing.T) {
|
||||
userID1 := model.NewId()
|
||||
|
||||
groupID1 := model.NewId()
|
||||
groupID2 := model.NewId()
|
||||
|
||||
for name, tc := range map[string]struct {
|
||||
Text string
|
||||
Keywords map[string][]string
|
||||
Groups map[string]*model.Group
|
||||
Expected *MentionResults
|
||||
}{
|
||||
"Mention user in text": {
|
||||
Text: "hello user @user1",
|
||||
Keywords: map[string][]string{"@user1": {userID1}},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
userID1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Mention user after ending a sentence with full stop": {
|
||||
Text: "hello user.@user1",
|
||||
Keywords: map[string][]string{"@user1": {userID1}},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
userID1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Mention user after hyphen": {
|
||||
Text: "hello user-@user1",
|
||||
Keywords: map[string][]string{"@user1": {userID1}},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
userID1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Mention user after colon": {
|
||||
Text: "hello user:@user1",
|
||||
Keywords: map[string][]string{"@user1": {userID1}},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
userID1: KeywordMention,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Mention here after colon": {
|
||||
Text: "hello all:@here",
|
||||
Keywords: map[string][]string{},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
HereMentioned: true,
|
||||
},
|
||||
},
|
||||
"Mention all after hyphen": {
|
||||
Text: "hello all-@all",
|
||||
Keywords: map[string][]string{},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
AllMentioned: true,
|
||||
},
|
||||
},
|
||||
"Mention channel after full stop": {
|
||||
Text: "hello channel.@channel",
|
||||
Keywords: map[string][]string{},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
ChannelMentioned: true,
|
||||
},
|
||||
},
|
||||
"Mention other potential users or system calls": {
|
||||
Text: "hello @potentialuser and @otherpotentialuser",
|
||||
Keywords: map[string][]string{},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
OtherPotentialMentions: []string{"potentialuser", "otherpotentialuser"},
|
||||
},
|
||||
},
|
||||
"Mention a real user and another potential user": {
|
||||
Text: "@user1, you can use @systembot to get help",
|
||||
Keywords: map[string][]string{"@user1": {userID1}},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
userID1: KeywordMention,
|
||||
},
|
||||
OtherPotentialMentions: []string{"systembot"},
|
||||
},
|
||||
},
|
||||
"Mention a group": {
|
||||
Text: "@engineering",
|
||||
Keywords: map[string][]string{"@user1": {userID1}},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
GroupMentions: map[string]MentionType{groupID1: GroupMention},
|
||||
},
|
||||
},
|
||||
"Mention a real user and another potential user and a group": {
|
||||
Text: "@engineering @user1, you can use @systembot to get help from",
|
||||
Keywords: map[string][]string{"@user1": {userID1}},
|
||||
Groups: map[string]*model.Group{
|
||||
groupID1: {Id: groupID1, Name: model.NewString("engineering")},
|
||||
groupID2: {Id: groupID2, Name: model.NewString("developers")},
|
||||
},
|
||||
Expected: &MentionResults{
|
||||
Mentions: map[string]MentionType{
|
||||
userID1: KeywordMention,
|
||||
},
|
||||
GroupMentions: map[string]MentionType{groupID1: GroupMention},
|
||||
OtherPotentialMentions: []string{"systembot"},
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := makeStandardMentionParser(mapsToMentionKeywords(tc.Keywords, tc.Groups))
|
||||
p.ProcessText(tc.Text)
|
||||
|
||||
assert.EqualValues(t, tc.Expected, p.Results())
|
||||
})
|
||||
}
|
||||
}
|
91
server/channels/app/mention_results.go
Normal file
91
server/channels/app/mention_results.go
Normal file
@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
const (
|
||||
// Different types of mentions ordered by their priority from lowest to highest
|
||||
|
||||
// A placeholder that should never be used in practice
|
||||
NoMention MentionType = iota
|
||||
|
||||
// The post is in a GM
|
||||
GMMention
|
||||
|
||||
// The post is in a thread that the user has commented on
|
||||
ThreadMention
|
||||
|
||||
// The post is a comment on a thread started by the user
|
||||
CommentMention
|
||||
|
||||
// The post contains an at-channel, at-all, or at-here
|
||||
ChannelMention
|
||||
|
||||
// The post is a DM
|
||||
DMMention
|
||||
|
||||
// The post contains an at-mention for the user
|
||||
KeywordMention
|
||||
|
||||
// The post contains a group mention for the user
|
||||
GroupMention
|
||||
)
|
||||
|
||||
type MentionType int
|
||||
|
||||
type MentionResults struct {
|
||||
// Mentions maps the ID of each user that was mentioned to how they were mentioned.
|
||||
Mentions map[string]MentionType
|
||||
|
||||
// GroupMentions maps the ID of each group that was mentioned to how it was mentioned.
|
||||
GroupMentions map[string]MentionType
|
||||
|
||||
// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
|
||||
// a corresponding keyword.
|
||||
OtherPotentialMentions []string
|
||||
|
||||
// HereMentioned is true if the message contained @here.
|
||||
HereMentioned bool
|
||||
|
||||
// AllMentioned is true if the message contained @all.
|
||||
AllMentioned bool
|
||||
|
||||
// ChannelMentioned is true if the message contained @channel.
|
||||
ChannelMentioned bool
|
||||
}
|
||||
|
||||
func (m *MentionResults) isUserMentioned(userID string) bool {
|
||||
if _, ok := m.Mentions[userID]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := m.GroupMentions[userID]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return m.HereMentioned || m.AllMentioned || m.ChannelMentioned
|
||||
}
|
||||
|
||||
func (m *MentionResults) addMention(userID string, mentionType MentionType) {
|
||||
if m.Mentions == nil {
|
||||
m.Mentions = make(map[string]MentionType)
|
||||
}
|
||||
|
||||
if currentType, ok := m.Mentions[userID]; ok && currentType >= mentionType {
|
||||
return
|
||||
}
|
||||
|
||||
m.Mentions[userID] = mentionType
|
||||
}
|
||||
|
||||
func (m *MentionResults) removeMention(userID string) {
|
||||
delete(m.Mentions, userID)
|
||||
}
|
||||
|
||||
func (m *MentionResults) addGroupMention(groupID string) {
|
||||
if m.GroupMentions == nil {
|
||||
m.GroupMentions = make(map[string]MentionType)
|
||||
}
|
||||
|
||||
m.GroupMentions[groupID] = GroupMention
|
||||
}
|
64
server/channels/app/mention_results_test.go
Normal file
64
server/channels/app/mention_results_test.go
Normal file
@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddMention(t *testing.T) {
|
||||
t.Run("should initialize Mentions and store new mentions", func(t *testing.T) {
|
||||
m := &MentionResults{}
|
||||
|
||||
userID1 := model.NewId()
|
||||
userID2 := model.NewId()
|
||||
|
||||
m.addMention(userID1, KeywordMention)
|
||||
m.addMention(userID2, CommentMention)
|
||||
|
||||
assert.Equal(t, map[string]MentionType{
|
||||
userID1: KeywordMention,
|
||||
userID2: CommentMention,
|
||||
}, m.Mentions)
|
||||
})
|
||||
|
||||
t.Run("should replace existing mentions with higher priority ones", func(t *testing.T) {
|
||||
m := &MentionResults{}
|
||||
|
||||
userID1 := model.NewId()
|
||||
userID2 := model.NewId()
|
||||
|
||||
m.addMention(userID1, ThreadMention)
|
||||
m.addMention(userID2, DMMention)
|
||||
|
||||
m.addMention(userID1, ChannelMention)
|
||||
m.addMention(userID2, KeywordMention)
|
||||
|
||||
assert.Equal(t, map[string]MentionType{
|
||||
userID1: ChannelMention,
|
||||
userID2: KeywordMention,
|
||||
}, m.Mentions)
|
||||
})
|
||||
|
||||
t.Run("should not replace high priority mentions with low priority ones", func(t *testing.T) {
|
||||
m := &MentionResults{}
|
||||
|
||||
userID1 := model.NewId()
|
||||
userID2 := model.NewId()
|
||||
|
||||
m.addMention(userID1, KeywordMention)
|
||||
m.addMention(userID2, CommentMention)
|
||||
|
||||
m.addMention(userID1, DMMention)
|
||||
m.addMention(userID2, ThreadMention)
|
||||
|
||||
assert.Equal(t, map[string]MentionType{
|
||||
userID1: KeywordMention,
|
||||
userID2: CommentMention,
|
||||
}, m.Mentions)
|
||||
})
|
||||
}
|
@ -10,8 +10,6 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@ -126,14 +124,15 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
|
||||
var allActivityPushUserIds []string
|
||||
if channel.Type != model.ChannelTypeDirect {
|
||||
// Iterate through all groups that were mentioned and insert group members into the list of mentions or potential mentions
|
||||
for _, group := range mentions.GroupMentions {
|
||||
for groupID := range mentions.GroupMentions {
|
||||
group := groups[groupID]
|
||||
anyUsersMentionedByGroup, err := a.insertGroupMentions(group, channel, profileMap, mentions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !anyUsersMentionedByGroup {
|
||||
a.sendNoUsersNotifiedByGroupInChannel(c, sender, post, channel, group)
|
||||
a.sendNoUsersNotifiedByGroupInChannel(c, sender, post, channel, groups[groupID])
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,14 +164,14 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
|
||||
membershipsMutex := &sync.Mutex{}
|
||||
followersMutex := &sync.Mutex{}
|
||||
if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" {
|
||||
var rootMentions *ExplicitMentions
|
||||
var rootMentions *MentionResults
|
||||
if parentPostList != nil {
|
||||
rootPost := parentPostList.Posts[parentPostList.Order[0]]
|
||||
if rootPost.GetProp("from_webhook") != "true" {
|
||||
threadParticipants[rootPost.UserId] = true
|
||||
}
|
||||
if channel.Type != model.ChannelTypeDirect {
|
||||
rootMentions = getExplicitMentions(rootPost, keywords, groups)
|
||||
rootMentions = getExplicitMentions(rootPost, keywords)
|
||||
for id := range rootMentions.Mentions {
|
||||
threadParticipants[id] = true
|
||||
}
|
||||
@ -676,9 +675,9 @@ func (a *App) RemoveNotifications(c request.CTX, post *model.Post, channel *mode
|
||||
mentions, _ := a.getExplicitMentionsAndKeywords(c, post, channel, profileMap, groups, channelMemberNotifyPropsMap, nil)
|
||||
|
||||
userIDs := []string{}
|
||||
for _, group := range mentions.GroupMentions {
|
||||
for groupID := range mentions.GroupMentions {
|
||||
for page := 0; ; page++ {
|
||||
groupMemberPage, count, appErr := a.GetGroupMemberUsersPage(group.Id, page, 100, &model.ViewUsersRestrictions{Channels: []string{channel.Id}})
|
||||
groupMemberPage, count, appErr := a.GetGroupMemberUsersPage(groupID, page, 100, &model.ViewUsersRestrictions{Channels: []string{channel.Id}})
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
@ -752,10 +751,10 @@ func (a *App) RemoveNotifications(c request.CTX, post *model.Post, channel *mode
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) getExplicitMentionsAndKeywords(c request.CTX, post *model.Post, channel *model.Channel, profileMap map[string]*model.User, groups map[string]*model.Group, channelMemberNotifyPropsMap map[string]model.StringMap, parentPostList *model.PostList) (*ExplicitMentions, map[string][]string) {
|
||||
mentions := &ExplicitMentions{}
|
||||
func (a *App) getExplicitMentionsAndKeywords(c request.CTX, post *model.Post, channel *model.Channel, profileMap map[string]*model.User, groups map[string]*model.Group, channelMemberNotifyPropsMap map[string]model.StringMap, parentPostList *model.PostList) (*MentionResults, MentionKeywords) {
|
||||
mentions := &MentionResults{}
|
||||
var allowChannelMentions bool
|
||||
var keywords map[string][]string
|
||||
var keywords MentionKeywords
|
||||
|
||||
if channel.Type == model.ChannelTypeDirect {
|
||||
otherUserId := channel.GetOtherUserIdForDM(post.UserId)
|
||||
@ -770,9 +769,9 @@ func (a *App) getExplicitMentionsAndKeywords(c request.CTX, post *model.Post, ch
|
||||
}
|
||||
} else {
|
||||
allowChannelMentions = a.allowChannelMentions(c, post, len(profileMap))
|
||||
keywords = a.getMentionKeywordsInChannel(profileMap, allowChannelMentions, channelMemberNotifyPropsMap)
|
||||
keywords = a.getMentionKeywordsInChannel(profileMap, allowChannelMentions, channelMemberNotifyPropsMap, groups)
|
||||
|
||||
mentions = getExplicitMentions(post, keywords, groups)
|
||||
mentions = getExplicitMentions(post, keywords)
|
||||
|
||||
// Add a GM mention to all members of a GM channel
|
||||
if channel.Type == model.ChannelTypeGroup {
|
||||
@ -1045,141 +1044,39 @@ func splitAtFinal(items []string) (preliminary []string, final string) {
|
||||
return
|
||||
}
|
||||
|
||||
type ExplicitMentions struct {
|
||||
// Mentions contains the ID of each user that was mentioned and how they were mentioned.
|
||||
Mentions map[string]MentionType
|
||||
|
||||
// Contains a map of groups that were mentioned
|
||||
GroupMentions map[string]*model.Group
|
||||
|
||||
// OtherPotentialMentions contains a list of strings that looked like mentions, but didn't have
|
||||
// a corresponding keyword.
|
||||
OtherPotentialMentions []string
|
||||
|
||||
// HereMentioned is true if the message contained @here.
|
||||
HereMentioned bool
|
||||
|
||||
// AllMentioned is true if the message contained @all.
|
||||
AllMentioned bool
|
||||
|
||||
// ChannelMentioned is true if the message contained @channel.
|
||||
ChannelMentioned bool
|
||||
}
|
||||
|
||||
type MentionType int
|
||||
|
||||
const (
|
||||
// Different types of mentions ordered by their priority from lowest to highest
|
||||
|
||||
// A placeholder that should never be used in practice
|
||||
NoMention MentionType = iota
|
||||
|
||||
// The post is in a GM
|
||||
GMMention
|
||||
|
||||
// The post is in a thread that the user has commented on
|
||||
ThreadMention
|
||||
|
||||
// The post is a comment on a thread started by the user
|
||||
CommentMention
|
||||
|
||||
// The post contains an at-channel, at-all, or at-here
|
||||
ChannelMention
|
||||
|
||||
// The post is a DM
|
||||
DMMention
|
||||
|
||||
// The post contains an at-mention for the user
|
||||
KeywordMention
|
||||
|
||||
// The post contains a group mention for the user
|
||||
GroupMention
|
||||
)
|
||||
|
||||
func (m *ExplicitMentions) isUserMentioned(userID string) bool {
|
||||
if _, ok := m.Mentions[userID]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
if _, ok := m.GroupMentions[userID]; ok {
|
||||
return true
|
||||
}
|
||||
|
||||
return m.HereMentioned || m.AllMentioned || m.ChannelMentioned
|
||||
}
|
||||
|
||||
func (m *ExplicitMentions) addMention(userID string, mentionType MentionType) {
|
||||
if m.Mentions == nil {
|
||||
m.Mentions = make(map[string]MentionType)
|
||||
}
|
||||
|
||||
if currentType, ok := m.Mentions[userID]; ok && currentType >= mentionType {
|
||||
return
|
||||
}
|
||||
|
||||
m.Mentions[userID] = mentionType
|
||||
}
|
||||
|
||||
func (m *ExplicitMentions) addGroupMention(word string, groups map[string]*model.Group) bool {
|
||||
if strings.HasPrefix(word, "@") {
|
||||
word = word[1:]
|
||||
} else {
|
||||
// Only allow group mentions when mentioned directly with @group-name
|
||||
return false
|
||||
}
|
||||
|
||||
group, groupFound := groups[word]
|
||||
if !groupFound {
|
||||
group = groups[strings.ToLower(word)]
|
||||
}
|
||||
|
||||
if group == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if m.GroupMentions == nil {
|
||||
m.GroupMentions = make(map[string]*model.Group)
|
||||
}
|
||||
|
||||
if group.Name != nil {
|
||||
m.GroupMentions[*group.Name] = group
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *ExplicitMentions) addMentions(userIDs []string, mentionType MentionType) {
|
||||
for _, userID := range userIDs {
|
||||
m.addMention(userID, mentionType)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ExplicitMentions) removeMention(userID string) {
|
||||
delete(m.Mentions, userID)
|
||||
}
|
||||
|
||||
// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
|
||||
// users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
|
||||
func getExplicitMentions(post *model.Post, keywords map[string][]string, groups map[string]*model.Group) *ExplicitMentions {
|
||||
ret := &ExplicitMentions{}
|
||||
func getExplicitMentions(post *model.Post, keywords MentionKeywords) *MentionResults {
|
||||
parser := makeStandardMentionParser(keywords)
|
||||
|
||||
buf := ""
|
||||
mentionsEnabledFields := getMentionsEnabledFields(post)
|
||||
for _, message := range mentionsEnabledFields {
|
||||
// Parse the text as Markdown, combining adjacent Text nodes into a single string for processing
|
||||
markdown.Inspect(message, func(node any) bool {
|
||||
text, ok := node.(*markdown.Text)
|
||||
if !ok {
|
||||
ret.processText(buf, keywords, groups)
|
||||
// This node isn't a string so process any accumulated text in the buffer
|
||||
if buf != "" {
|
||||
parser.ProcessText(buf)
|
||||
}
|
||||
|
||||
buf = ""
|
||||
return true
|
||||
}
|
||||
|
||||
// This node is a string, so add it to buf and continue onto the next node to see if it's more text
|
||||
buf += text.Text
|
||||
return false
|
||||
})
|
||||
}
|
||||
ret.processText(buf, keywords, groups)
|
||||
|
||||
return ret
|
||||
// Process any left over text
|
||||
if buf != "" {
|
||||
parser.ProcessText(buf)
|
||||
}
|
||||
|
||||
return parser.Results()
|
||||
}
|
||||
|
||||
// Given a post returns the values of the fields in which mentions are possible.
|
||||
@ -1251,7 +1148,7 @@ func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team
|
||||
}
|
||||
for _, group := range groups {
|
||||
if group.Group.Name != nil {
|
||||
groupsMap[*group.Group.Name] = &group.Group
|
||||
groupsMap[group.Id] = &group.Group
|
||||
}
|
||||
}
|
||||
return groupsMap, nil
|
||||
@ -1263,7 +1160,7 @@ func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team
|
||||
}
|
||||
for _, group := range groups {
|
||||
if group.Name != nil {
|
||||
groupsMap[*group.Name] = group
|
||||
groupsMap[group.Id] = group
|
||||
}
|
||||
}
|
||||
|
||||
@ -1272,12 +1169,11 @@ func (a *App) getGroupsAllowedForReferenceInChannel(channel *model.Channel, team
|
||||
|
||||
// Given a map of user IDs to profiles, returns a list of mention
|
||||
// keywords for all users in the channel.
|
||||
func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, allowChannelMentions bool, channelMemberNotifyPropsMap map[string]model.StringMap) map[string][]string {
|
||||
keywords := make(map[string][]string)
|
||||
func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, allowChannelMentions bool, channelMemberNotifyPropsMap map[string]model.StringMap, groups map[string]*model.Group) MentionKeywords {
|
||||
keywords := make(MentionKeywords)
|
||||
|
||||
for _, profile := range profiles {
|
||||
addMentionKeywordsForUser(
|
||||
keywords,
|
||||
keywords.AddUser(
|
||||
profile,
|
||||
channelMemberNotifyPropsMap[profile.Id],
|
||||
a.GetStatusFromCache(profile.Id),
|
||||
@ -1285,12 +1181,14 @@ func (a *App) getMentionKeywordsInChannel(profiles map[string]*model.User, allow
|
||||
)
|
||||
}
|
||||
|
||||
keywords.AddGroupsMap(groups)
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
// insertGroupMentions adds group members in the channel to Mentions, adds group members not in the channel to OtherPotentialMentions
|
||||
// returns false if no group members present in the team that the channel belongs to
|
||||
func (a *App) insertGroupMentions(group *model.Group, channel *model.Channel, profileMap map[string]*model.User, mentions *ExplicitMentions) (bool, *model.AppError) {
|
||||
func (a *App) insertGroupMentions(group *model.Group, channel *model.Channel, profileMap map[string]*model.User, mentions *MentionResults) (bool, *model.AppError) {
|
||||
var err error
|
||||
var groupMembers []*model.User
|
||||
outOfChannelGroupMembers := []*model.User{}
|
||||
@ -1331,44 +1229,6 @@ func (a *App) insertGroupMentions(group *model.Group, channel *model.Channel, pr
|
||||
return isGroupOrDirect || len(groupMembers) > 0, nil
|
||||
}
|
||||
|
||||
// addMentionKeywordsForUser adds the mention keywords for a given user to the given keyword map. Returns the provided keyword map.
|
||||
func addMentionKeywordsForUser(keywords map[string][]string, profile *model.User, channelNotifyProps map[string]string, status *model.Status, allowChannelMentions bool) map[string][]string {
|
||||
userMention := "@" + strings.ToLower(profile.Username)
|
||||
keywords[userMention] = append(keywords[userMention], profile.Id)
|
||||
|
||||
// Add all the user's mention keys
|
||||
for _, k := range profile.GetMentionKeys() {
|
||||
// note that these are made lower case so that we can do a case insensitive check for them
|
||||
key := strings.ToLower(k)
|
||||
|
||||
if key != "" {
|
||||
keywords[key] = append(keywords[key], profile.Id)
|
||||
}
|
||||
}
|
||||
|
||||
// If turned on, add the user's case sensitive first name
|
||||
if profile.NotifyProps[model.FirstNameNotifyProp] == "true" && profile.FirstName != "" {
|
||||
keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
|
||||
}
|
||||
|
||||
// Add @channel and @all to keywords if user has them turned on and the server allows them
|
||||
if allowChannelMentions {
|
||||
// Ignore channel mentions if channel is muted and channel mention setting is default
|
||||
ignoreChannelMentions := channelNotifyProps[model.IgnoreChannelMentionsNotifyProp] == model.IgnoreChannelMentionsOn || (channelNotifyProps[model.MarkUnreadNotifyProp] == model.UserNotifyMention && channelNotifyProps[model.IgnoreChannelMentionsNotifyProp] == model.IgnoreChannelMentionsDefault)
|
||||
|
||||
if profile.NotifyProps[model.ChannelMentionsNotifyProp] == "true" && !ignoreChannelMentions {
|
||||
keywords["@channel"] = append(keywords["@channel"], profile.Id)
|
||||
keywords["@all"] = append(keywords["@all"], profile.Id)
|
||||
|
||||
if status != nil && status.Status == model.StatusOnline {
|
||||
keywords["@here"] = append(keywords["@here"], profile.Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keywords
|
||||
}
|
||||
|
||||
// Represents either an email or push notification and contains the fields required to send it to any user.
|
||||
type PostNotification struct {
|
||||
Channel *model.Channel
|
||||
@ -1418,127 +1278,6 @@ func (n *PostNotification) GetSenderName(userNameFormat string, overridesAllowed
|
||||
return n.Sender.GetDisplayNameWithPrefix(userNameFormat, "@")
|
||||
}
|
||||
|
||||
// checkForMention checks if there is a mention to a specific user or to the keywords here / channel / all
|
||||
func (m *ExplicitMentions) checkForMention(word string, keywords map[string][]string, groups map[string]*model.Group) bool {
|
||||
var mentionType MentionType
|
||||
|
||||
switch strings.ToLower(word) {
|
||||
case "@here":
|
||||
m.HereMentioned = true
|
||||
mentionType = ChannelMention
|
||||
case "@channel":
|
||||
m.ChannelMentioned = true
|
||||
mentionType = ChannelMention
|
||||
case "@all":
|
||||
m.AllMentioned = true
|
||||
mentionType = ChannelMention
|
||||
default:
|
||||
mentionType = KeywordMention
|
||||
}
|
||||
|
||||
m.addGroupMention(word, groups)
|
||||
|
||||
if ids, match := keywords[strings.ToLower(word)]; match {
|
||||
m.addMentions(ids, mentionType)
|
||||
return true
|
||||
}
|
||||
|
||||
// Case-sensitive check for first name
|
||||
if ids, match := keywords[word]; match {
|
||||
m.addMentions(ids, mentionType)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isKeywordMultibyte checks if a word containing a multibyte character contains a multibyte keyword
|
||||
func isKeywordMultibyte(keywords map[string][]string, word string) ([]string, bool) {
|
||||
ids := []string{}
|
||||
match := false
|
||||
var multibyteKeywords []string
|
||||
for keyword := range keywords {
|
||||
if len(keyword) != utf8.RuneCountInString(keyword) {
|
||||
multibyteKeywords = append(multibyteKeywords, keyword)
|
||||
}
|
||||
}
|
||||
|
||||
if len(word) != utf8.RuneCountInString(word) {
|
||||
for _, key := range multibyteKeywords {
|
||||
if strings.Contains(word, key) {
|
||||
ids, match = keywords[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ids, match
|
||||
}
|
||||
|
||||
// Processes text to filter mentioned users and other potential mentions
|
||||
func (m *ExplicitMentions) processText(text string, keywords map[string][]string, groups map[string]*model.Group) {
|
||||
systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
|
||||
|
||||
for _, word := range strings.FieldsFunc(text, func(c rune) bool {
|
||||
// Split on any whitespace or punctuation that can't be part of an at mention or emoji pattern
|
||||
return !(c == ':' || c == '.' || c == '-' || c == '_' || c == '@' || unicode.IsLetter(c) || unicode.IsNumber(c))
|
||||
}) {
|
||||
// skip word with format ':word:' with an assumption that it is an emoji format only
|
||||
if word[0] == ':' && word[len(word)-1] == ':' {
|
||||
continue
|
||||
}
|
||||
|
||||
word = strings.TrimLeft(word, ":.-_")
|
||||
|
||||
if m.checkForMention(word, keywords, groups) {
|
||||
continue
|
||||
}
|
||||
|
||||
foundWithoutSuffix := false
|
||||
wordWithoutSuffix := word
|
||||
|
||||
for wordWithoutSuffix != "" && strings.LastIndexAny(wordWithoutSuffix, ".-:_") == (len(wordWithoutSuffix)-1) {
|
||||
wordWithoutSuffix = wordWithoutSuffix[0 : len(wordWithoutSuffix)-1]
|
||||
|
||||
if m.checkForMention(wordWithoutSuffix, keywords, groups) {
|
||||
foundWithoutSuffix = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundWithoutSuffix {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
|
||||
// No need to bother about unicode as we are looking for ASCII characters.
|
||||
last := word[len(word)-1]
|
||||
switch last {
|
||||
// If the word is possibly at the end of a sentence, remove that character.
|
||||
case '.', '-', ':':
|
||||
word = word[:len(word)-1]
|
||||
}
|
||||
m.OtherPotentialMentions = append(m.OtherPotentialMentions, word[1:])
|
||||
} else if strings.ContainsAny(word, ".-:") {
|
||||
// This word contains a character that may be the end of a sentence, so split further
|
||||
splitWords := strings.FieldsFunc(word, func(c rune) bool {
|
||||
return c == '.' || c == '-' || c == ':'
|
||||
})
|
||||
|
||||
for _, splitWord := range splitWords {
|
||||
if m.checkForMention(splitWord, keywords, groups) {
|
||||
continue
|
||||
}
|
||||
if _, ok := systemMentions[splitWord]; !ok && strings.HasPrefix(splitWord, "@") {
|
||||
m.OtherPotentialMentions = append(m.OtherPotentialMentions, splitWord[1:])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ids, match := isKeywordMultibyte(keywords, word); match {
|
||||
m.addMentions(ids, KeywordMention)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) GetNotificationNameFormat(user *model.User) string {
|
||||
if !*a.Config().PrivacySettings.ShowFullName {
|
||||
return model.ShowUsername
|
||||
@ -1563,7 +1302,7 @@ type CRTNotifiers struct {
|
||||
Push model.StringArray
|
||||
}
|
||||
|
||||
func (c *CRTNotifiers) addFollowerToNotify(user *model.User, mentions *ExplicitMentions, channelMemberNotificationProps model.StringMap, channel *model.Channel) {
|
||||
func (c *CRTNotifiers) addFollowerToNotify(user *model.User, mentions *MentionResults, channelMemberNotificationProps model.StringMap, channel *model.Channel) {
|
||||
_, userWasMentioned := mentions.Mentions[user.Id]
|
||||
notifyDesktop, notifyPush, notifyEmail := shouldUserNotifyCRT(user, userWasMentioned)
|
||||
notifyChannelDesktop, notifyChannelPush := shouldChannelMemberNotifyCRT(channelMemberNotificationProps, userWasMentioned)
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -199,7 +199,7 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
|
||||
|
||||
// Validate recipients counts in case it's not DM
|
||||
if persistentNotification := post.GetPersistentNotification(); persistentNotification != nil && *persistentNotification && channel.Type != model.ChannelTypeDirect {
|
||||
err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *ExplicitMentions, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
|
||||
err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
|
||||
if maxRecipients := *a.Config().ServiceSettings.PersistentNotificationMaxRecipients; len(mentions.Mentions) > maxRecipients {
|
||||
return model.NewAppError("CreatePost", "api.post.post_priority.max_recipients_persistent_notification_post.request_error", map[string]any{"MaxRecipients": maxRecipients}, "", http.StatusBadRequest)
|
||||
} else if len(mentions.Mentions) == 0 {
|
||||
@ -1780,8 +1780,8 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P
|
||||
return 0, err
|
||||
}
|
||||
|
||||
keywords := addMentionKeywordsForUser(
|
||||
map[string][]string{},
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(
|
||||
user,
|
||||
map[string]string{},
|
||||
&model.Status{Status: model.StatusOnline}, // Assume the user is online since they would've triggered this
|
||||
@ -1820,9 +1820,11 @@ func (a *App) countThreadMentions(c request.CTX, user *model.User, post *model.P
|
||||
return 0, model.NewAppError("countThreadMentions", "app.channel.count_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
|
||||
}
|
||||
|
||||
keywords.AddGroupsMap(groups)
|
||||
|
||||
for _, p := range posts {
|
||||
if p.CreateAt >= timestamp {
|
||||
mentions := getExplicitMentions(p, keywords, groups)
|
||||
mentions := getExplicitMentions(p, keywords)
|
||||
if _, ok := mentions.Mentions[user.Id]; ok {
|
||||
count += 1
|
||||
}
|
||||
@ -1863,8 +1865,8 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
|
||||
return 0, 0, 0, err
|
||||
}
|
||||
|
||||
keywords := addMentionKeywordsForUser(
|
||||
map[string][]string{},
|
||||
keywords := MentionKeywords{}
|
||||
keywords.AddUser(
|
||||
user,
|
||||
channelMember.NotifyProps,
|
||||
&model.Status{Status: model.StatusOnline}, // Assume the user is online since they would've triggered this
|
||||
@ -1989,14 +1991,14 @@ func isCommentMention(user *model.User, post *model.Post, otherPosts map[string]
|
||||
return mentioned
|
||||
}
|
||||
|
||||
func isPostMention(user *model.User, post *model.Post, keywords map[string][]string, otherPosts map[string]*model.Post, mentionedByThread map[string]bool, checkForCommentMentions bool) bool {
|
||||
func isPostMention(user *model.User, post *model.Post, keywords MentionKeywords, otherPosts map[string]*model.Post, mentionedByThread map[string]bool, checkForCommentMentions bool) bool {
|
||||
// Prevent the user from mentioning themselves
|
||||
if post.UserId == user.Id && post.GetProp("from_webhook") != "true" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for keyword mentions
|
||||
mentions := getExplicitMentions(post, keywords, make(map[string]*model.Group))
|
||||
mentions := getExplicitMentions(post, keywords)
|
||||
if _, ok := mentions.Mentions[user.Id]; ok {
|
||||
return true
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ func (a *App) ResolvePersistentNotification(c request.CTX, post *model.Post, log
|
||||
}
|
||||
|
||||
stopNotifications := false
|
||||
if err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *ExplicitMentions, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
|
||||
if err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *MentionResults, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
|
||||
if mentions.isUserMentioned(loggedInUserID) {
|
||||
stopNotifications = true
|
||||
}
|
||||
@ -151,7 +151,7 @@ func (a *App) SendPersistentNotifications() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(post *model.Post, channel *model.Channel, team *model.Team, mentions *ExplicitMentions, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error) error {
|
||||
func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(post *model.Post, channel *model.Channel, team *model.Team, mentions *MentionResults, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error) error {
|
||||
channelsMap, teamsMap, err := a.channelTeamMapsForPosts(posts)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -171,7 +171,7 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
|
||||
}
|
||||
profileMap := channelProfileMap[channel.Id]
|
||||
|
||||
mentions := &ExplicitMentions{}
|
||||
mentions := &MentionResults{}
|
||||
// In DMs, only the "other" user can be mentioned
|
||||
if channel.Type == model.ChannelTypeDirect {
|
||||
otherUserId := channel.GetOtherUserIdForDM(post.UserId)
|
||||
@ -180,8 +180,11 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
|
||||
}
|
||||
} else {
|
||||
keywords := channelKeywords[channel.Id]
|
||||
mentions = getExplicitMentions(post, keywords, channelGroupMap[channel.Id])
|
||||
for _, group := range mentions.GroupMentions {
|
||||
keywords.AddGroupsMap(channelGroupMap[channel.Id])
|
||||
|
||||
mentions = getExplicitMentions(post, keywords)
|
||||
for groupID := range mentions.GroupMentions {
|
||||
group := channelGroupMap[channel.Id][groupID]
|
||||
_, err := a.insertGroupMentions(group, channel, profileMap, mentions)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to include mentions from group - %s for channel - %s", group.Id, channel.Id)
|
||||
@ -197,10 +200,10 @@ func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(pos
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model.Channel, teamsMap map[string]*model.Team) (map[string]map[string]*model.Group, map[string]model.UserMap, map[string]map[string][]string, map[string]map[string]model.StringMap, error) {
|
||||
func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model.Channel, teamsMap map[string]*model.Team) (map[string]map[string]*model.Group, map[string]model.UserMap, map[string]MentionKeywords, map[string]map[string]model.StringMap, error) {
|
||||
channelGroupMap := make(map[string]map[string]*model.Group, len(channelsMap))
|
||||
channelProfileMap := make(map[string]model.UserMap, len(channelsMap))
|
||||
channelKeywords := make(map[string]map[string][]string, len(channelsMap))
|
||||
channelKeywords := make(map[string]MentionKeywords, len(channelsMap))
|
||||
channelNotifyProps := make(map[string]map[string]model.StringMap, len(channelsMap))
|
||||
for _, c := range channelsMap {
|
||||
// In DM, notifications can't be send to any 3rd person.
|
||||
@ -210,8 +213,8 @@ func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model
|
||||
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
|
||||
}
|
||||
channelGroupMap[c.Id] = make(map[string]*model.Group, len(groups))
|
||||
for k, v := range groups {
|
||||
channelGroupMap[c.Id][k] = v
|
||||
for groupID, group := range groups {
|
||||
channelGroupMap[c.Id][groupID] = group
|
||||
}
|
||||
props, err := a.Srv().Store().Channel().GetAllChannelMembersNotifyPropsForChannel(c.Id, true)
|
||||
if err != nil {
|
||||
@ -225,14 +228,14 @@ func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model
|
||||
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
|
||||
}
|
||||
|
||||
channelKeywords[c.Id] = make(map[string][]string, len(profileMap))
|
||||
channelKeywords[c.Id] = make(MentionKeywords, len(profileMap))
|
||||
validProfileMap := make(map[string]*model.User, len(profileMap))
|
||||
for k, v := range profileMap {
|
||||
if v.IsBot {
|
||||
for userID, user := range profileMap {
|
||||
if user.IsBot {
|
||||
continue
|
||||
}
|
||||
validProfileMap[k] = v
|
||||
channelKeywords[c.Id]["@"+v.Username] = []string{k}
|
||||
validProfileMap[userID] = user
|
||||
channelKeywords[c.Id].AddUserKeyword(userID, "@"+user.Username)
|
||||
}
|
||||
channelProfileMap[c.Id] = validProfileMap
|
||||
}
|
||||
@ -273,7 +276,7 @@ func (a *App) channelTeamMapsForPosts(posts []*model.Post) (map[string]*model.Ch
|
||||
return channelsMap, teamsMap, nil
|
||||
}
|
||||
|
||||
func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Channel, team *model.Team, mentions *ExplicitMentions, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error {
|
||||
func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Channel, team *model.Team, mentions *MentionResults, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error {
|
||||
mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions))
|
||||
for id, v := range mentions.Mentions {
|
||||
// Don't send notification to post owner nor GM mentions
|
||||
|
Loading…
Reference in New Issue
Block a user