Sync channel topics between Slack bridges (#585)

Added logic to allow for configurable synchronisation of topics and purposes of channels between Slack bridges.
This commit is contained in:
Patrick Connolly 2018-11-26 17:47:04 +08:00 committed by Duco van Amstel
parent 5ed7abdbeb
commit f5659d455d
7 changed files with 123 additions and 9 deletions

View File

@ -117,6 +117,7 @@ type Protocol struct {
ShowEmbeds bool // discord ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost SkipTLSVerify bool // IRC, mattermost
StripNick bool // all protocols StripNick bool // all protocols
SyncTopic bool // slack
Team string // mattermost Team string // mattermost
Token string // gitter, slack, discord, api Token string // gitter, slack, discord, api
Topic string // zulip Topic string // zulip

View File

@ -116,6 +116,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
return b.GetBool(noSendJoinConfig) return b.GetBool(noSendJoinConfig)
case sPinnedItem, sUnpinnedItem: case sPinnedItem, sUnpinnedItem:
return true return true
case sChannelTopic, sChannelPurpose:
// Skip the event if our bot/user account changed the topic/purpose
if ev.User == b.si.User.ID {
return true
}
} }
// Skip any messages that we made ourselves or from 'slackbot' (see #527). // Skip any messages that we made ourselves or from 'slackbot' (see #527).
@ -136,7 +141,6 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
if len(ev.Files) > 0 { if len(ev.Files) > 0 {
return b.filesCached(ev.Files) return b.filesCached(ev.Files)
} }
return false return false
} }
@ -201,6 +205,7 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
rmsg.Username = sSystemUser rmsg.Username = sSystemUser
rmsg.Event = config.EventJoinLeave rmsg.Event = config.EventJoinLeave
case sChannelTopic, sChannelPurpose: case sChannelTopic, sChannelPurpose:
b.populateChannels()
rmsg.Event = config.EventTopicChange rmsg.Event = config.EventTopicChange
case sMessageChanged: case sMessageChanged:
rmsg.Text = ev.SubMessage.Text rmsg.Text = ev.SubMessage.Text

View File

@ -262,12 +262,28 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
} }
var ( var (
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`) mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`) channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`) variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`) urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`)
topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
) )
func (b *Bslack) extractTopicOrPurpose(text string) (string, string) {
r := topicOrPurposeRE.FindStringSubmatch(text)
if len(r) == 5 {
action, updateType, extracted := r[2], r[3], r[4]
switch action {
case "set":
return updateType, extracted
case "cleared":
return updateType, ""
}
}
b.Log.Warnf("Encountered channel topic or purpose change message with unexpected format: %s", text)
return "unknown", ""
}
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users // @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (b *Bslack) replaceMention(text string) string { func (b *Bslack) replaceMention(text string) string {
replaceFunc := func(match string) string { replaceFunc := func(match string) string {

View File

@ -0,0 +1,36 @@
package bslack
import (
"io/ioutil"
"testing"
"github.com/42wim/matterbridge/bridge"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestExtractTopicOrPurpose(t *testing.T) {
testcases := map[string]struct {
input string
wantChangeType string
wantOutput string
}{
"success - topic type": {"@someone set channel topic: foo bar", "topic", "foo bar"},
"success - purpose type": {"@someone set channel purpose: foo bar", "purpose", "foo bar"},
"success - one line": {"@someone set channel topic: foo bar", "topic", "foo bar"},
"success - multi-line": {"@someone set channel topic: foo\nbar", "topic", "foo\nbar"},
"success - cleared": {"@someone cleared channel topic", "topic", ""},
"error - unhandled": {"some unmatched message", "unknown", ""},
}
logger := logrus.New()
logger.SetOutput(ioutil.Discard)
cfg := &bridge.Config{Log: logger.WithFields(nil)}
b := newBridge(cfg)
for name, tc := range testcases {
gotChangeType, gotOutput := b.extractTopicOrPurpose(tc.input)
assert.Equalf(t, tc.wantChangeType, gotChangeType, "This testcase failed: %s", name)
assert.Equalf(t, tc.wantOutput, gotOutput, "This testcase failed: %s", name)
}
}

View File

@ -281,8 +281,14 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
return "", nil return "", nil
} }
// Handle message deletions.
var handled bool var handled bool
// Handle topic/purpose updates.
if handled, err = b.handleTopicOrPurpose(&msg, channelInfo); handled {
return "", err
}
// Handle message deletions.
if handled, err = b.deleteMessage(&msg, channelInfo); handled { if handled, err = b.deleteMessage(&msg, channelInfo); handled {
return msg.ID, err return msg.ID, err
} }
@ -315,6 +321,49 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
return b.postMessage(&msg, messageParameters, channelInfo) return b.postMessage(&msg, messageParameters, channelInfo)
} }
func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
var updateFunc func(channelID string, value string) (*slack.Channel, error)
incomingChangeType, text := b.extractTopicOrPurpose(msg.Text)
switch incomingChangeType {
case "topic":
updateFunc = b.rtm.SetTopicOfConversation
case "purpose":
updateFunc = b.rtm.SetPurposeOfConversation
default:
b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType)
return true, nil
}
for {
_, err := updateFunc(channelInfo.ID, text)
if err == nil {
return true, nil
}
if err = b.handleRateLimit(err); err != nil {
return true, err
}
}
}
// handles updating topic/purpose and determining whether to further propagate update messages.
func (b *Bslack) handleTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
if msg.Event != config.EventTopicChange {
return false, nil
}
if b.GetBool("SyncTopic") {
return b.updateTopicOrPurpose(msg, channelInfo)
}
// Pass along to normal message handlers.
if b.GetBool("ShowTopicChange") {
return false, nil
}
// Swallow message as handled no-op.
return true, nil
}
func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) { func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
if msg.Event != config.EventMsgDelete { if msg.Event != config.EventMsgDelete {
return false, nil return false, nil

View File

@ -267,8 +267,10 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
return brMsgIDs return brMsgIDs
} }
// only relay topic change when configured // only relay topic change when used in some way on other side
if msg.Event == config.EventTopicChange && !gw.Bridges[dest.Account].GetBool("ShowTopicChange") { if msg.Event == config.EventTopicChange &&
!gw.Bridges[dest.Account].GetBool("ShowTopicChange") &&
!gw.Bridges[dest.Account].GetBool("SyncTopic") {
return brMsgIDs return brMsgIDs
} }

View File

@ -765,11 +765,16 @@ ShowJoinPart=false
#OPTIONAL (default false) #OPTIONAL (default false)
StripNick=false StripNick=false
#Enable to show topic changes from other bridges #Enable to show topic/purpose changes from other bridges
#Only works hiding/show topic changes from slack bridge for now #Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
#Enable to sync topic/purpose changes from other bridges
#Only works syncing topic changes from slack bridge for now
#OPTIONAL (default false)
SyncTopic=false
################################################################### ###################################################################
#telegram section #telegram section
################################################################### ###################################################################