Skip to content

Commit 047e895

Browse files
Added guards to prevent lingering cases of replies sent on GM/DM channels
1 parent 40d4d49 commit 047e895

7 files changed

Lines changed: 580 additions & 54 deletions

File tree

server/command_test.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/stretchr/testify/require"
2626

2727
"github.com/mattermost/mattermost-plugin-jira/server/enterprise"
28+
"github.com/mattermost/mattermost-plugin-jira/server/utils/kvstore"
2829
"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
2930
)
3031

@@ -48,7 +49,7 @@ var _ UserStore = (*mockUserStoreKV)(nil)
4849
func (store mockUserStoreKV) LoadConnection(instanceID, mattermostUserID types.ID) (*Connection, error) {
4950
connection, ok := store.connections[mattermostUserID]
5051
if !ok {
51-
return nil, errors.Errorf("TESTING connection %q %q not found", instanceID, mattermostUserID)
52+
return nil, errors.Wrapf(kvstore.ErrNotFound, "TESTING connection %q %q", instanceID, mattermostUserID)
5253
}
5354
return connection, nil
5455
}
@@ -99,6 +100,14 @@ func getMockUserStoreKV() mockUserStoreKV {
99100
}
100101
}
101102

103+
func mockUserStoreKVWithConnected(connectedUserIDs ...types.ID) mockUserStoreKV {
104+
store := getMockUserStoreKV()
105+
for _, id := range connectedUserIDs {
106+
store.connections[id] = &Connection{User: jira.User{AccountID: "test-AccountID"}}
107+
}
108+
return store
109+
}
110+
102111
type mockInstanceStoreKV struct {
103112
mockInstanceStore
104113
kv *sync.Map

server/subscribe.go

Lines changed: 105 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/mattermost/mattermost/server/public/model"
2121

2222
"github.com/mattermost/mattermost-plugin-jira/server/utils"
23+
"github.com/mattermost/mattermost-plugin-jira/server/utils/kvstore"
2324
"github.com/mattermost/mattermost-plugin-jira/server/utils/types"
2425
)
2526

@@ -40,6 +41,7 @@ const (
4041
CommentVisibility = "commentVisibility"
4142
TeamFilter = "teamField"
4243
CommentVisibilityGroupType = "group"
44+
maxDMGMChannelMembers = 200
4345
)
4446

4547
type FieldFilter struct {
@@ -444,13 +446,66 @@ func (p *Plugin) removeChannelSubscription(instanceID types.ID, subscriptionID s
444446
})
445447
}
446448

449+
func isDirectOrGroupChannel(channel *model.Channel) bool {
450+
return channel.Type == model.ChannelTypeDirect || channel.Type == model.ChannelTypeGroup
451+
}
452+
453+
// channelHasConnectedMember reports whether any non-bot member of channel is still
454+
// connected to instanceID. Channel types other than DM/GM are always allowed, since
455+
// they are not tied to any single user's connection.
456+
func (p *Plugin) channelHasConnectedMember(instanceID types.ID, channel *model.Channel) (bool, error) {
457+
if !isDirectOrGroupChannel(channel) {
458+
return true, nil
459+
}
460+
461+
botUserID := p.getConfig().botUserID
462+
members, err := p.client.Channel.ListMembers(channel.Id, 0, maxDMGMChannelMembers)
463+
if err != nil {
464+
return false, err
465+
}
466+
467+
for _, member := range members {
468+
if member.UserId == botUserID {
469+
continue
470+
}
471+
472+
connection, err := p.userStore.LoadConnection(instanceID, types.ID(member.UserId))
473+
if err != nil {
474+
if errors.Cause(err) != kvstore.ErrNotFound {
475+
return false, err
476+
}
477+
continue
478+
}
479+
480+
// A missing connection can also read back as an empty one rather than an error.
481+
if connection.JiraAccountID() != "" {
482+
return true, nil
483+
}
484+
}
485+
486+
return false, nil
487+
}
488+
489+
// subscriptionIDsForChannel returns the IDs of subscriptions targeting channelID.
490+
// ByID is scanned directly because the IDByChannelID index can drift from it, e.g.
491+
// for data written before the index existed.
492+
func subscriptionIDsForChannel(subs *ChannelSubscriptions, channelID string) []string {
493+
var subIDs []string
494+
for id, sub := range subs.ByID {
495+
if sub.ChannelID == channelID {
496+
subIDs = append(subIDs, id)
497+
}
498+
}
499+
return subIDs
500+
}
501+
447502
func (p *Plugin) removeSubscriptionsForChannel(instanceID types.ID, channelID string) error {
448503
subs, err := p.getSubscriptions(instanceID)
449504
if err != nil {
450505
return err
451506
}
452507

453-
if subs.Channel.IDByChannelID[channelID].Len() == 0 {
508+
if len(subscriptionIDsForChannel(subs.Channel, channelID)) == 0 {
454509
return nil
455510
}
456511

@@ -461,8 +516,7 @@ func (p *Plugin) removeSubscriptionsForChannel(instanceID types.ID, channelID st
461516
return nil, err
462517
}
463518

464-
subIDs := subs.Channel.IDByChannelID[channelID]
465-
for _, subID := range subIDs.Elems() {
519+
for _, subID := range subscriptionIDsForChannel(subs.Channel, channelID) {
466520
if sub, ok := subs.Channel.ByID[subID]; ok {
467521
subs.Channel.remove(&sub)
468522
}
@@ -477,22 +531,62 @@ func (p *Plugin) removeSubscriptionsForChannel(instanceID types.ID, channelID st
477531
})
478532
}
479533

534+
// cleanupDMSubscriptionsOnDisconnect removes channel subscriptions targeting any DM or
535+
// GM the disconnecting user belongs to, once no member of that channel remains connected
536+
// to instanceID. Must be called after the user's connection has been deleted.
480537
func (p *Plugin) cleanupDMSubscriptionsOnDisconnect(instanceID types.ID, mattermostUserID string) {
481-
conf := p.getConfig()
482-
dmChannel, err := p.client.Channel.GetDirect(mattermostUserID, conf.botUserID)
538+
subs, err := p.getSubscriptions(instanceID)
483539
if err != nil {
484-
p.client.Log.Warn("Failed to get DM channel for subscription cleanup on disconnect",
540+
p.client.Log.Warn("Failed to load subscriptions for DM/GM cleanup on disconnect",
485541
"mattermostUserID", mattermostUserID,
486542
"instanceID", string(instanceID),
487543
"error", err.Error())
488544
return
489545
}
490546

491-
if err := p.removeSubscriptionsForChannel(instanceID, dmChannel.Id); err != nil {
492-
p.client.Log.Warn("Failed to clean up DM subscriptions on disconnect",
493-
"mattermostUserID", mattermostUserID,
494-
"instanceID", string(instanceID),
495-
"error", err.Error())
547+
channelIDs := map[string]bool{}
548+
for _, sub := range subs.Channel.ByID {
549+
channelIDs[sub.ChannelID] = true
550+
}
551+
552+
for channelID := range channelIDs {
553+
channel, err := p.client.Channel.Get(channelID)
554+
if err != nil {
555+
p.client.Log.Warn("Failed to get channel for DM/GM subscription cleanup on disconnect",
556+
"channelID", channelID,
557+
"instanceID", string(instanceID),
558+
"error", err.Error())
559+
continue
560+
}
561+
562+
if !isDirectOrGroupChannel(channel) {
563+
continue
564+
}
565+
566+
if _, err := p.client.Channel.GetMember(channelID, mattermostUserID); err != nil {
567+
// The disconnecting user isn't a member of this DM/GM.
568+
continue
569+
}
570+
571+
hasConnectedMember, err := p.channelHasConnectedMember(instanceID, channel)
572+
if err != nil {
573+
p.client.Log.Warn("Failed to check for connected members during DM/GM subscription cleanup",
574+
"channelID", channelID,
575+
"instanceID", string(instanceID),
576+
"error", err.Error())
577+
continue
578+
}
579+
if hasConnectedMember {
580+
continue
581+
}
582+
583+
if err := p.removeSubscriptionsForChannel(instanceID, channelID); err != nil {
584+
p.client.Log.Warn("Failed to clean up DM/GM subscriptions on disconnect",
585+
"mattermostUserID", mattermostUserID,
586+
"channelID", channelID,
587+
"instanceID", string(instanceID),
588+
"error", err.Error())
589+
}
496590
}
497591
}
498592

0 commit comments

Comments
 (0)