diff --git a/README.md b/README.md index 537d18e0..1bcbf974 100644 --- a/README.md +++ b/README.md @@ -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 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) +* Preserves threading when possible * [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) * [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups) diff --git a/bridge/config/config.go b/bridge/config/config.go index 0e36d960..5ccf6041 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -35,6 +35,7 @@ type Message struct { Event string `json:"event"` Protocol string `json:"protocol"` Gateway string `json:"gateway"` + ParentID string `json:"parent_id"` Timestamp time.Time `json:"timestamp"` ID string `json:"id"` Extra map[string][]interface{} @@ -98,6 +99,7 @@ type Protocol struct { NoTLS bool // mattermost Password string // IRC,mattermost,XMPP,matrix PrefixMessagesWithNick bool // mattemost, slack + PreserveThreading bool // slack Protocol string // all protocols QuoteDisable bool // telegram QuoteFormat string // telegram diff --git a/bridge/slack/handlers.go b/bridge/slack/handlers.go index b6400a80..1428cba2 100644 --- a/bridge/slack/handlers.go +++ b/bridge/slack/handlers.go @@ -171,8 +171,10 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er Account: b.Account, ID: "slack " + ev.Timestamp, Extra: map[string][]interface{}{}, + ParentID: ev.ThreadTimestamp, } + if b.useChannelID { rmsg.Channel = "ID:" + channelInfo.ID } diff --git a/bridge/slack/slack.go b/bridge/slack/slack.go index 2022ebce..d2d118b2 100644 --- a/bridge/slack/slack.go +++ b/bridge/slack/slack.go @@ -310,6 +310,10 @@ func (b *Bslack) prepareMessageParameters(msg *config.Message) *slack.PostMessag params.Username = msg.Username params.LinkNames = 1 // replace mentions params.IconURL = config.GetIconURL(msg, b.GetString(iconURLConfig)) + msgFields := strings.Fields(msg.ParentID) + if len(msgFields) >= 2 { + params.ThreadTimestamp = msgFields[1] + } if msg.Avatar != "" { params.IconURL = msg.Avatar } diff --git a/gateway/gateway.go b/gateway/gateway.go index 5bfb00e2..c1905a9b 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -83,6 +83,25 @@ func New(cfg config.Gateway, r *Router) *Gateway { 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 { br := gw.Router.getBridge(cfg.Account) if br == nil { @@ -206,6 +225,20 @@ func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []con 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 { var brMsgIDs []*BrMsgID @@ -242,6 +275,13 @@ func (gw *Gateway) handleMessage(msg config.Message, dest *bridge.Bridge) []*BrM 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 origmsg := msg 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) + msg.Channel = channel.Name msg.Avatar = gw.modifyAvatar(origmsg, dest) msg.Username = gw.modifyUsername(origmsg, dest) - msg.ID = "" - if res, ok := gw.Messages.Get(origmsg.ID); 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 { - msg.ID = id.ID - } - } - } + + msg.ID = gw.getDestMsgID(origmsg.ID, dest, channel) + // for api we need originchannel as channel if dest.Protocol == "api" { msg.Channel = originchannel } + + msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel) + if msg.ParentID == "" { + msg.ParentID = canonicalParentMsgID + } + mID, err := dest.Send(msg) if err != nil { flog.Error(err) } + // append the message ID (mID) from this bridge (dest) to our brMsgIDs slice if mID != "" { flog.Debugf("mID %s: %s", dest.Account, mID) diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index 76875aca..441a333c 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -659,6 +659,12 @@ StripNick=false #OPTIONAL (default 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 ###################################################################