diff --git a/bridge/config/config.go b/bridge/config/config.go index 21010dbf..eb34912d 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -117,6 +117,7 @@ type Protocol struct { ShowEmbeds bool // discord SkipTLSVerify bool // IRC, mattermost StripNick bool // all protocols + SyncTopic bool // slack Team string // mattermost Token string // gitter, slack, discord, api Topic string // zulip diff --git a/bridge/slack/handlers.go b/bridge/slack/handlers.go index 035c5af5..89c800da 100644 --- a/bridge/slack/handlers.go +++ b/bridge/slack/handlers.go @@ -116,6 +116,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { return b.GetBool(noSendJoinConfig) case sPinnedItem, sUnpinnedItem: 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). @@ -136,7 +141,6 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool { if len(ev.Files) > 0 { return b.filesCached(ev.Files) } - return false } @@ -201,6 +205,7 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) rmsg.Username = sSystemUser rmsg.Event = config.EventJoinLeave case sChannelTopic, sChannelPurpose: + b.populateChannels() rmsg.Event = config.EventTopicChange case sMessageChanged: rmsg.Text = ev.SubMessage.Text diff --git a/bridge/slack/helpers.go b/bridge/slack/helpers.go index b0fdaba1..39fbcea7 100644 --- a/bridge/slack/helpers.go +++ b/bridge/slack/helpers.go @@ -262,12 +262,28 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config } var ( - mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`) - channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`) - variableRE = regexp.MustCompile(``) - urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`) + mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`) + channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`) + variableRE = 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 func (b *Bslack) replaceMention(text string) string { replaceFunc := func(match string) string { diff --git a/bridge/slack/helpers_test.go b/bridge/slack/helpers_test.go new file mode 100644 index 00000000..c9ff647d --- /dev/null +++ b/bridge/slack/helpers_test.go @@ -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) + } +} diff --git a/bridge/slack/slack.go b/bridge/slack/slack.go index d054ae81..a38bbb53 100644 --- a/bridge/slack/slack.go +++ b/bridge/slack/slack.go @@ -281,8 +281,14 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) { return "", nil } - // Handle message deletions. 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 { return msg.ID, err } @@ -315,6 +321,49 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) { 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) { if msg.Event != config.EventMsgDelete { return false, nil diff --git a/gateway/gateway.go b/gateway/gateway.go index 72a0f6a6..2b8bdfa9 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -267,8 +267,10 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM return brMsgIDs } - // only relay topic change when configured - if msg.Event == config.EventTopicChange && !gw.Bridges[dest.Account].GetBool("ShowTopicChange") { + // only relay topic change when used in some way on other side + if msg.Event == config.EventTopicChange && + !gw.Bridges[dest.Account].GetBool("ShowTopicChange") && + !gw.Bridges[dest.Account].GetBool("SyncTopic") { return brMsgIDs } diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index b51f351b..0d9a8a84 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -765,11 +765,16 @@ ShowJoinPart=false #OPTIONAL (default 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 #OPTIONAL (default 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 ###################################################################