Preserve threading between Slack instances (#529)

* Opportunistically preserve Slack threading when parent thread in cache. [#529]

* Removed slack-specific processing from gateway.

* Added docs.

* Add option to enable threading, with default to off.

* Did cleanup on @42wim's comments.

* Update gateway/gateway.go

Co-Authored-By: patcon <patrick.c.connolly@gmail.com>

* Suggestion from @42wim :)

* Suggestions from @42wim.

* More suggestions.
This commit is contained in:
Patrick Connolly 2018-11-07 16:14:31 +08:00 committed by Wim
parent 5666821e7b
commit a20b7895a9
6 changed files with 66 additions and 11 deletions

View File

@ -37,6 +37,7 @@ Minecraft server chat support via [MatterLink](https://github.com/elytra/MatterL
* [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols) * [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
* [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols) * [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
* [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes) * [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
* Preserves threading when possible
* [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling) * [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
* [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing) * [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
* [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups) * [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)

View File

@ -35,6 +35,7 @@ type Message struct {
Event string `json:"event"` Event string `json:"event"`
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Gateway string `json:"gateway"` Gateway string `json:"gateway"`
ParentID string `json:"parent_id"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
ID string `json:"id"` ID string `json:"id"`
Extra map[string][]interface{} Extra map[string][]interface{}
@ -98,6 +99,7 @@ type Protocol struct {
NoTLS bool // mattermost NoTLS bool // mattermost
Password string // IRC,mattermost,XMPP,matrix Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack PrefixMessagesWithNick bool // mattemost, slack
PreserveThreading bool // slack
Protocol string // all protocols Protocol string // all protocols
QuoteDisable bool // telegram QuoteDisable bool // telegram
QuoteFormat string // telegram QuoteFormat string // telegram

View File

@ -171,8 +171,10 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
Account: b.Account, Account: b.Account,
ID: "slack " + ev.Timestamp, ID: "slack " + ev.Timestamp,
Extra: map[string][]interface{}{}, Extra: map[string][]interface{}{},
ParentID: ev.ThreadTimestamp,
} }
if b.useChannelID { if b.useChannelID {
rmsg.Channel = "ID:" + channelInfo.ID rmsg.Channel = "ID:" + channelInfo.ID
} }

View File

@ -310,6 +310,10 @@ func (b *Bslack) prepareMessageParameters(msg *config.Message) *slack.PostMessag
params.Username = msg.Username params.Username = msg.Username
params.LinkNames = 1 // replace mentions params.LinkNames = 1 // replace mentions
params.IconURL = config.GetIconURL(msg, b.GetString(iconURLConfig)) params.IconURL = config.GetIconURL(msg, b.GetString(iconURLConfig))
msgFields := strings.Fields(msg.ParentID)
if len(msgFields) >= 2 {
params.ThreadTimestamp = msgFields[1]
}
if msg.Avatar != "" { if msg.Avatar != "" {
params.IconURL = msg.Avatar params.IconURL = msg.Avatar
} }

View File

@ -83,6 +83,25 @@ func New(cfg config.Gateway, r *Router) *Gateway {
return gw return gw
} }
// Find the canonical ID that the message is keyed under in cache
func (gw *Gateway) FindCanonicalMsgID(mID string) string {
if gw.Messages.Contains(mID) {
return mID
}
// If not keyed, iterate through cache for downstream, and infer upstream.
for _, mid := range gw.Messages.Keys() {
v, _ := gw.Messages.Peek(mid)
ids := v.([]*BrMsgID)
for _, downstreamMsgObj := range ids {
if mID == downstreamMsgObj.ID {
return mid.(string)
}
}
}
return ""
}
func (gw *Gateway) AddBridge(cfg *config.Bridge) error { func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
br := gw.Router.getBridge(cfg.Account) br := gw.Router.getBridge(cfg.Account)
if br == nil { if br == nil {
@ -206,6 +225,20 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con
return channels return channels
} }
func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel config.ChannelInfo) string {
if res, ok := gw.Messages.Get(msgID); ok {
IDs := res.([]*BrMsgID)
for _, id := range IDs {
// check protocol, bridge name and channelname
// for people that reuse the same bridge multiple times. see #342
if dest.Protocol == id.br.Protocol && dest.Name == id.br.Name && channel.ID == id.ChannelID {
return id.ID
}
}
}
return ""
}
func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID { func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrMsgID {
var brMsgIDs []*BrMsgID var brMsgIDs []*BrMsgID
@ -242,6 +275,13 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
return brMsgIDs return brMsgIDs
} }
// Get the ID of the parent message in thread
var canonicalParentMsgID string
if msg.ParentID != "" && (gw.Config.General.PreserveThreading || dest.GetBool("PreserveThreading")) {
thisParentMsgID := dest.Protocol + " " + msg.ParentID
canonicalParentMsgID = gw.FindCanonicalMsgID(thisParentMsgID)
}
originchannel := msg.Channel originchannel := msg.Channel
origmsg := msg origmsg := msg
channels := gw.getDestChannel(&msg, *dest) channels := gw.getDestChannel(&msg, *dest)
@ -258,28 +298,28 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM
} }
} }
flog.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name) flog.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, originchannel, dest.Account, channel.Name)
msg.Channel = channel.Name msg.Channel = channel.Name
msg.Avatar = gw.modifyAvatar(origmsg, dest) msg.Avatar = gw.modifyAvatar(origmsg, dest)
msg.Username = gw.modifyUsername(origmsg, dest) msg.Username = gw.modifyUsername(origmsg, dest)
msg.ID = ""
if res, ok := gw.Messages.Get(origmsg.ID); ok { msg.ID = gw.getDestMsgID(origmsg.ID, dest, channel)
IDs := res.([]*BrMsgID)
for _, id := range IDs {
// check protocol, bridge name and channelname
// for people that reuse the same bridge multiple times. see #342
if dest.Protocol == id.br.Protocol && dest.Name == id.br.Name && channel.ID == id.ChannelID {
msg.ID = id.ID
}
}
}
// for api we need originchannel as channel // for api we need originchannel as channel
if dest.Protocol == "api" { if dest.Protocol == "api" {
msg.Channel = originchannel msg.Channel = originchannel
} }
msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel)
if msg.ParentID == "" {
msg.ParentID = canonicalParentMsgID
}
mID, err := dest.Send(msg) mID, err := dest.Send(msg)
if err != nil { if err != nil {
flog.Error(err) flog.Error(err)
} }
// append the message ID (mID) from this bridge (dest) to our brMsgIDs slice // append the message ID (mID) from this bridge (dest) to our brMsgIDs slice
if mID != "" { if mID != "" {
flog.Debugf("mID %s: %s", dest.Account, mID) flog.Debugf("mID %s: %s", dest.Account, mID)

View File

@ -659,6 +659,12 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
#Opportunistically preserve threaded replies between Slack channels.
#This only works if the parent message is still in the cache.
#Cache is flushed between restarts.
#OPTIONAL (default false)
PreserveThreading=false
################################################################### ###################################################################
#discord section #discord section
################################################################### ###################################################################