Skip to content

Commit

Permalink
Add setting per-autolink for allowing bot posts (#160)
Browse files Browse the repository at this point in the history
Co-authored-by: Hanzei <[email protected]>
  • Loading branch information
mickmister and hanzei authored Mar 31, 2022
1 parent e73e585 commit 0d3ca5f
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 31 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ Below is an example of regexp patterns used for autolinking at https://community
3. Using autolinking to create group mentions. Use (note that clicking the resulting at-mention redirects to a broken page):
- Pattern: `@customgroup*`
- Template: `[@customgroup]( \\* @user1 @user2 @user3 \\* )`

4. For servers with multiple domains (like community and community-daily on the [public Mattermost Server](https://community.mattermost)), a substitution of absolute conversation links to relative links is recommended to prevent issues in the mobile app. Add one pattern for each domain used:
- Pattern: `https://community\\.mattermost\\.com/(?P\u003cteamname\u003e(?a-zA-Z0-9]+)/(?P\u003cid\u003e[a-zA-Z0-9]+)`
- Template: `[<jump to convo>](/${teamname}/pl/${id})/${id})`
Expand All @@ -149,7 +149,7 @@ The `/autolink` commands allow the users to easily edit the configurations.
disable \<*linkref*> | Disable the link | `/autolink disable Visa`
add \<*linkref*> | Creates a new link with the name specified in the command | `/autolink add Visa`
delete \<*linkref*> | Delete the link | `/autolink delete Visa`
set \<*linkref*> \<*field*> *value* | Sets a link's field to a value <br> *Fields* - <br> <ul><li>Template - Sets the Template field</li><li>Pattern - Sets the Pattern field </li> <li> WordMatch - If true uses the [\b word boundaries](https://www.regular-expressions.info/wordboundaries.html) </li> <li> Scope - Sets the Scope field (`team` or `team/channel` or a whitespace-separated list thereof) </li> | <br> `/autolink set Visa Pattern (?P<VISA>(?P<part1>4\d{3})[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))` <br><br> `/autolink set Visa Template VISA XXXX-XXXX-XXXX-$LastFour` <br><br> `/autolink set Visa WordMatch true` <br><br> `/autolink set Visa Scope team/townsquare` <br><br>
set \<*linkref*> \<*field*> *value* | Sets a link's field to a value <br> *Fields* - <br> <ul><li>Template - Sets the Template field</li><li>Pattern - Sets the Pattern field </li> <li> WordMatch - If true uses the [\b word boundaries](https://www.regular-expressions.info/wordboundaries.html) </li> <li> ProcessBotPosts - If true applies changes to posts made by bot accounts. </li> <li> Scope - Sets the Scope field (`team` or `team/channel` or a whitespace-separated list thereof) </li> | <br> `/autolink set Visa Pattern (?P<VISA>(?P<part1>4\d{3})[ -]?(?P<part2>\d{4})[ -]?(?P<part3>\d{4})[ -]?(?P<LastFour>[0-9]{4}))` <br><br> `/autolink set Visa Template VISA XXXX-XXXX-XXXX-$LastFour` <br><br> `/autolink set Visa WordMatch true` <br><br> `/autolink set Visa ProcessBotPosts true` <br><br> `/autolink set Visa Scope team/townsquare` <br><br>


## Development
Expand Down
5 changes: 5 additions & 0 deletions server/autolink/autolink.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Autolink struct {
WordMatch bool `json:"WordMatch"`
DisableNonWordPrefix bool `json:"DisableNonWordPrefix"`
DisableNonWordSuffix bool `json:"DisableNonWordSuffix"`
ProcessBotPosts bool `json:"ProcessBotPosts"`

template string
re *regexp.Regexp
Expand All @@ -25,6 +26,7 @@ func (l Autolink) Equals(x Autolink) bool {
if l.Disabled != x.Disabled ||
l.DisableNonWordPrefix != x.DisableNonWordPrefix ||
l.DisableNonWordSuffix != x.DisableNonWordSuffix ||
l.ProcessBotPosts != x.ProcessBotPosts ||
l.Name != x.Name ||
l.Pattern != x.Pattern ||
len(l.Scope) != len(x.Scope) ||
Expand Down Expand Up @@ -148,6 +150,9 @@ func (l Autolink) ToMarkdown(i int) string {
if l.DisableNonWordSuffix {
text += fmt.Sprintf(" - DisableNonWordSuffix: `%v`\n", l.DisableNonWordSuffix)
}
if l.ProcessBotPosts {
text += fmt.Sprintf(" - ProcessBotPosts: `%v`\n", l.ProcessBotPosts)
}
if len(l.Scope) != 0 {
text += fmt.Sprintf(" - Scope: `%v`\n", l.Scope)
}
Expand Down
12 changes: 10 additions & 2 deletions server/autolinkplugin/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const (
optPattern = "Pattern"
optScope = "Scope"
optDisabled = "Disabled"
optProcessBotPosts = "ProcessBotPosts"
optDisableNonWordPrefix = "DisableNonWordPrefix"
optDisableNonWordSuffix = "DisableNonWordSuffix"
optWordMatch = "WordMatch"
Expand All @@ -44,7 +45,8 @@ const helpText = "###### Mattermost Autolink Plugin Administration\n" +
"/autolink set Visa Template VISA XXXX-XXXX-XXXX-$LastFour\n" +
"/autolink set Visa WordMatch true\n" +
"/autolink set Visa Scope team/townsquare\n" +
"/autolink test Vi 4356-7891-2345-1111 -- (4111222233334444)\n" +
"/autolink set Visa ProcessBotPosts true\n" +
"/autolink test Visa 4356-7891-2345-1111 -- (4111222233334444)\n" +
"/autolink enable Visa\n" +
"```\n" +
""
Expand Down Expand Up @@ -198,9 +200,15 @@ func executeSet(p *Plugin, c *plugin.Context, header *model.CommandArgs, args ..
return responsef("%v", e)
}
l.Disabled = boolValue
case optProcessBotPosts:
boolValue, e := parseBoolArg(value)
if e != nil {
return responsef("%v", e)
}
l.ProcessBotPosts = boolValue
default:
return responsef("%q is not a supported field, must be one of %q", fieldName,
[]string{optName, optDisabled, optPattern, optTemplate, optScope, optDisableNonWordPrefix, optDisableNonWordSuffix, optWordMatch})
[]string{optName, optDisabled, optPattern, optTemplate, optScope, optDisableNonWordPrefix, optDisableNonWordSuffix, optWordMatch, optProcessBotPosts})
}

err = saveConfigLinks(p, links)
Expand Down
5 changes: 5 additions & 0 deletions server/autolinkplugin/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ func getAutoCompleteData() *model.AutocompleteData {
Hint: "",
Item: "WordMatch",
},
{
HelpText: "If true applies changes to posts created by bot accounts.",
Hint: "",
Item: "ProcessBotPosts",
},
{
HelpText: "team/channel the autolink applies to",
Hint: "",
Expand Down
55 changes: 29 additions & 26 deletions server/autolinkplugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,6 @@ func (p *Plugin) inScope(scope []string, channelName string, teamName string) bo
return false
}

func (p *Plugin) isBotUser(userID string) (bool, *model.AppError) {
user, appErr := p.API.GetUser(userID)
if appErr != nil {
p.API.LogError("failed to check if message for rewriting was send by a bot", "error", appErr)
return false, appErr
}

return user.IsBot, nil
}

func (p *Plugin) ProcessPost(c *plugin.Context, post *model.Post) (*model.Post, string) {
conf := p.getConfig()
postText := post.Message
Expand All @@ -142,6 +132,9 @@ func (p *Plugin) ProcessPost(c *plugin.Context, post *model.Post) (*model.Post,
}
}

var author *model.User
var authorErr *model.AppError

markdown.Inspect(post.Message, func(node interface{}) bool {
switch node.(type) {
// never descend into the text content of a link/image
Expand Down Expand Up @@ -181,9 +174,33 @@ func (p *Plugin) ProcessPost(c *plugin.Context, post *model.Post) (*model.Post,
newText := origText

for _, link := range conf.Links {
if p.inScope(link.Scope, channelName, teamName) {
newText = link.Replace(newText)
if !p.inScope(link.Scope, channelName, teamName) {
continue
}

out := link.Replace(newText)
if out == newText {
continue
}

if !link.ProcessBotPosts {
if author == nil && authorErr == nil {
author, authorErr = p.API.GetUser(post.UserId)
if authorErr != nil {
// NOTE: Not sure how we want to handle errors here, we can either:
// * assume that occasional rewrites of Bot messges are ok
// * assume that occasional not rewriting of all messages is ok
// Let's assume for now that former is a lesser evil and carry on.
p.API.LogError("failed to check if message for rewriting was send by a bot", "error", authorErr)
}
}

if author != nil && author.IsBot {
continue
}
}

newText = out
}
if origText != newText {
postText = postText[:startPos] + newText + postText[endPos:]
Expand All @@ -194,20 +211,6 @@ func (p *Plugin) ProcessPost(c *plugin.Context, post *model.Post) (*model.Post,
return true
})
if post.Message != postText {
isBot, appErr := p.isBotUser(post.UserId)
if appErr != nil {
// NOTE: Not sure how we want to handle errors here, we can either:
// * assume that occasional rewrites of Bot messges are ok
// * assume that occasional not rewriting of all messages is ok
// Let's assume for now that former is a lesser evil and carry on.
} else if isBot {
// We intentionally use a single if/else block so that the code is
// more readable and does not rely on hidden side effect of
// isBot==false when appErr!=nil.
p.API.LogDebug("not rewriting message from bot", "userID", post.UserId)
return nil, ""
}

post.Message = postText
}

Expand Down
48 changes: 47 additions & 1 deletion server/autolinkplugin/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,53 @@ func TestBotMessagesAreNotRewriten(t *testing.T) {
post := &model.Post{Message: "Welcome to Mattermost!"}
rpost, _ := p.MessageWillBePosted(&plugin.Context{}, post)

assert.Nil(t, rpost)
assert.Equal(t, post.Message, rpost.Message)
}

func TestBotMessagesAreRewritenWhenConfigAllows(t *testing.T) {
conf := Config{
Links: []autolink.Autolink{{
Pattern: "(Mattermost)",
Template: "[Mattermost](https://mattermost.com)",
ProcessBotPosts: true,
}},
}

testChannel := model.Channel{
Name: "TestChanel",
}

testTeam := model.Team{
Name: "TestTeam",
}

api := &plugintest.API{}

api.On("LoadPluginConfiguration", mock.AnythingOfType("*autolinkplugin.Config")).Return(func(dest interface{}) error {
*dest.(*Config) = conf
return nil
}).Once()
api.On("UnregisterCommand", mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return((*model.AppError)(nil)).Once()

api.On("GetChannel", mock.AnythingOfType("string")).Return(&testChannel, nil).Once()
api.On("GetTeam", mock.AnythingOfType("string")).Return(&testTeam, nil).Once()
api.On("LogDebug", mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string"))

testUser := model.User{
IsBot: true,
}
api.On("GetUser", mock.AnythingOfType("string")).Return(&testUser, nil).Once()

p := New()
p.SetAPI(api)
err := p.OnConfigurationChange()
require.NoError(t, err)

post := &model.Post{Message: "Welcome to Mattermost!"}
rpost, _ := p.MessageWillBePosted(&plugin.Context{}, post)
require.NotNil(t, rpost)

assert.Equal(t, "Welcome to [Mattermost](https://mattermost.com)!", rpost.Message)
}

func TestHashtags(t *testing.T) {
Expand Down

0 comments on commit 0d3ca5f

Please sign in to comment.