github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/msg_grouper.go (about)

     1  package chat
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"time"
     7  
     8  	"github.com/keybase/client/go/chat/globals"
     9  	"github.com/keybase/client/go/chat/types"
    10  	"github.com/keybase/client/go/ephemeral"
    11  	"github.com/keybase/client/go/libkb"
    12  	"github.com/keybase/client/go/protocol/chat1"
    13  	"github.com/keybase/client/go/protocol/gregor1"
    14  	"github.com/keybase/client/go/protocol/keybase1"
    15  )
    16  
    17  type msgGrouper interface {
    18  	// matches indicates if the given message matches the current group
    19  	matches(context.Context, chat1.MessageUnboxed, []chat1.MessageUnboxed) bool
    20  	// makeCombined outputs a single message from a given group or nil
    21  	makeCombined(context.Context, []chat1.MessageUnboxed) *chat1.MessageUnboxed
    22  }
    23  
    24  func groupGeneric(ctx context.Context, msgs []chat1.MessageUnboxed, msgGrouper msgGrouper) (res []chat1.MessageUnboxed) {
    25  	var grouped []chat1.MessageUnboxed
    26  	addGrouped := func() {
    27  		if len(grouped) == 0 {
    28  			return
    29  		}
    30  		msg := msgGrouper.makeCombined(ctx, grouped)
    31  		if msg != nil {
    32  			res = append(res, *msg)
    33  		}
    34  		grouped = nil
    35  	}
    36  	for _, msg := range msgs {
    37  		if msgGrouper.matches(ctx, msg, grouped) {
    38  			grouped = append(grouped, msg)
    39  			continue
    40  		}
    41  		addGrouped()
    42  		// some match functions may depend on messages in grouped, so after we clear it
    43  		// this message might be a candidate to get grouped.
    44  		if msgGrouper.matches(ctx, msg, grouped) {
    45  			grouped = append(grouped, msg)
    46  		} else {
    47  			res = append(res, msg)
    48  		}
    49  	}
    50  	addGrouped()
    51  	return res
    52  }
    53  
    54  // group JOIN/LEAVE messages
    55  type joinLeaveGrouper struct {
    56  	uid gregor1.UID
    57  }
    58  
    59  var _ msgGrouper = (*joinLeaveGrouper)(nil)
    60  
    61  func newJoinLeaveGrouper(g *globals.Context, uid gregor1.UID, convID chat1.ConversationID,
    62  	dataSource types.InboxSourceDataSourceTyp) *joinLeaveGrouper {
    63  	return &joinLeaveGrouper{
    64  		uid: uid,
    65  	}
    66  }
    67  
    68  func (gr *joinLeaveGrouper) matches(ctx context.Context, msg chat1.MessageUnboxed, grouped []chat1.MessageUnboxed) bool {
    69  	if !msg.IsValid() || msg.Valid().ClientHeader.Sender.Eq(gr.uid) {
    70  		return false
    71  	}
    72  	body := msg.Valid().MessageBody
    73  	if !(body.IsType(chat1.MessageType_JOIN) || body.IsType(chat1.MessageType_LEAVE)) {
    74  		return false
    75  	}
    76  	for _, g := range grouped {
    77  		if g.Valid().SenderUsername == msg.Valid().SenderUsername {
    78  			return false
    79  		}
    80  	}
    81  	return true
    82  }
    83  
    84  func (gr *joinLeaveGrouper) makeCombined(ctx context.Context, grouped []chat1.MessageUnboxed) *chat1.MessageUnboxed {
    85  	var joiners, leavers []string
    86  	for _, j := range grouped {
    87  		if j.Valid().MessageBody.IsType(chat1.MessageType_JOIN) {
    88  			joiners = append(joiners, j.Valid().SenderUsername)
    89  		} else {
    90  			leavers = append(leavers, j.Valid().SenderUsername)
    91  		}
    92  	}
    93  	mvalid := grouped[0].Valid()
    94  	mvalid.ClientHeader.MessageType = chat1.MessageType_JOIN
    95  	mvalid.MessageBody = chat1.NewMessageBodyWithJoin(chat1.MessageJoin{
    96  		Joiners: joiners,
    97  		Leavers: leavers,
    98  	})
    99  	msg := chat1.NewMessageUnboxedWithValid(mvalid)
   100  	return &msg
   101  }
   102  
   103  // group BULKADDTOCONV system messages
   104  type bulkAddGrouper struct {
   105  	globals.Contextified
   106  	// uid set of active users
   107  	activeMap  map[string]struct{}
   108  	uid        gregor1.UID
   109  	convID     chat1.ConversationID
   110  	dataSource types.InboxSourceDataSourceTyp
   111  }
   112  
   113  var _ msgGrouper = (*bulkAddGrouper)(nil)
   114  
   115  func newBulkAddGrouper(g *globals.Context, uid gregor1.UID, convID chat1.ConversationID,
   116  	dataSource types.InboxSourceDataSourceTyp) *bulkAddGrouper {
   117  	return &bulkAddGrouper{
   118  		Contextified: globals.NewContextified(g),
   119  		uid:          uid,
   120  		convID:       convID,
   121  		dataSource:   dataSource,
   122  	}
   123  }
   124  
   125  func (gr *bulkAddGrouper) matches(ctx context.Context, msg chat1.MessageUnboxed, grouped []chat1.MessageUnboxed) bool {
   126  	if !msg.IsValid() {
   127  		return false
   128  	}
   129  	body := msg.Valid().MessageBody
   130  	if !body.IsType(chat1.MessageType_SYSTEM) {
   131  		return false
   132  	}
   133  	sysBod := msg.Valid().MessageBody.System()
   134  	typ, err := sysBod.SystemType()
   135  	return err == nil && typ == chat1.MessageSystemType_BULKADDTOCONV
   136  }
   137  
   138  func (gr *bulkAddGrouper) makeCombined(ctx context.Context, grouped []chat1.MessageUnboxed) *chat1.MessageUnboxed {
   139  	var filteredUsernames, usernames []string
   140  	for _, j := range grouped {
   141  		if j.Valid().MessageBody.IsType(chat1.MessageType_SYSTEM) {
   142  			body := j.Valid().MessageBody.System()
   143  			typ, err := body.SystemType()
   144  			if err == nil && typ == chat1.MessageSystemType_BULKADDTOCONV {
   145  				usernames = append(usernames, body.Bulkaddtoconv().Usernames...)
   146  			}
   147  		}
   148  	}
   149  
   150  	if gr.activeMap == nil && len(usernames) > 0 {
   151  		gr.activeMap = make(map[string]struct{})
   152  		allList, err := gr.G().ParticipantsSource.Get(ctx, gr.uid, gr.convID, gr.dataSource)
   153  		if err == nil {
   154  			for _, uid := range allList {
   155  				gr.activeMap[uid.String()] = struct{}{}
   156  			}
   157  		}
   158  	}
   159  
   160  	// filter the usernames for people that are actually part of the team
   161  	seen := make(map[string]bool)
   162  	for _, username := range usernames {
   163  		uid, err := gr.G().GetUPAKLoader().LookupUID(ctx, libkb.NewNormalizedUsername(username))
   164  		if err != nil {
   165  			continue
   166  		}
   167  		if _, ok := gr.activeMap[uid.String()]; ok && !seen[username] {
   168  			filteredUsernames = append(filteredUsernames, username)
   169  			seen[username] = true
   170  		}
   171  	}
   172  	if len(filteredUsernames) == 0 {
   173  		return nil
   174  	}
   175  
   176  	mvalid := grouped[0].Valid()
   177  	mvalid.ClientHeader.MessageType = chat1.MessageType_SYSTEM
   178  	mvalid.MessageBody = chat1.NewMessageBodyWithSystem(chat1.NewMessageSystemWithBulkaddtoconv(chat1.MessageSystemBulkAddToConv{
   179  		Usernames: filteredUsernames,
   180  	}))
   181  	msg := chat1.NewMessageUnboxedWithValid(mvalid)
   182  	return &msg
   183  }
   184  
   185  // group NEWCHANNEL system messages
   186  type channelGrouper struct {
   187  	uid gregor1.UID
   188  }
   189  
   190  var _ msgGrouper = (*channelGrouper)(nil)
   191  
   192  func newChannelGrouper(g *globals.Context, uid gregor1.UID, convID chat1.ConversationID,
   193  	dataSource types.InboxSourceDataSourceTyp) *channelGrouper {
   194  	return &channelGrouper{
   195  		uid: uid,
   196  	}
   197  }
   198  
   199  func (gr *channelGrouper) matches(ctx context.Context, msg chat1.MessageUnboxed, grouped []chat1.MessageUnboxed) bool {
   200  	if !msg.IsValid() {
   201  		return false
   202  	}
   203  	if len(grouped) > 0 && !grouped[0].SenderEq(msg) {
   204  		return false
   205  	}
   206  	body := msg.Valid().MessageBody
   207  	if !body.IsType(chat1.MessageType_SYSTEM) {
   208  		return false
   209  	}
   210  	sysBod := msg.Valid().MessageBody.System()
   211  	typ, err := sysBod.SystemType()
   212  	return err == nil && typ == chat1.MessageSystemType_NEWCHANNEL
   213  }
   214  
   215  func (gr *channelGrouper) makeCombined(ctx context.Context, grouped []chat1.MessageUnboxed) *chat1.MessageUnboxed {
   216  	if len(grouped) == 0 {
   217  		return nil
   218  	}
   219  
   220  	var convIDs []chat1.ConversationID
   221  	var mentions []chat1.ChannelNameMention
   222  	for _, msg := range grouped {
   223  		convIDs = append(convIDs, msg.Valid().MessageBody.System().Newchannel().ConvID)
   224  		mentions = append(mentions, msg.Valid().ChannelNameMentions...)
   225  	}
   226  
   227  	mvalid := grouped[0].Valid()
   228  	sysBod := mvalid.MessageBody.System().Newchannel()
   229  	sysBod.ConvIDs = convIDs
   230  	mvalid.ChannelNameMentions = mentions
   231  	mvalid.MessageBody = chat1.NewMessageBodyWithSystem(chat1.NewMessageSystemWithNewchannel(sysBod))
   232  	msg := chat1.NewMessageUnboxedWithValid(mvalid)
   233  	return &msg
   234  }
   235  
   236  // group ADDEDTOTEAM system messages
   237  type addedToTeamGrouper struct {
   238  	globals.Contextified
   239  	uid         gregor1.UID
   240  	ownUsername string
   241  }
   242  
   243  var _ msgGrouper = (*addedToTeamGrouper)(nil)
   244  
   245  func newAddedToTeamGrouper(g *globals.Context, uid gregor1.UID, convID chat1.ConversationID,
   246  	dataSource types.InboxSourceDataSourceTyp) *addedToTeamGrouper {
   247  	return &addedToTeamGrouper{
   248  		Contextified: globals.NewContextified(g),
   249  		uid:          uid,
   250  	}
   251  }
   252  
   253  func (gr *addedToTeamGrouper) matches(ctx context.Context, msg chat1.MessageUnboxed, grouped []chat1.MessageUnboxed) bool {
   254  	if !(msg.IsValid() && msg.Valid().ClientHeader.Sender.Eq(gr.uid)) {
   255  		return false
   256  	}
   257  	if len(grouped) > 0 && !grouped[0].SenderEq(msg) {
   258  		return false
   259  	}
   260  	body := msg.Valid().MessageBody
   261  	if !body.IsType(chat1.MessageType_SYSTEM) {
   262  		return false
   263  	}
   264  	sysBod := msg.Valid().MessageBody.System()
   265  	typ, err := sysBod.SystemType()
   266  	if !(err == nil && typ == chat1.MessageSystemType_ADDEDTOTEAM) {
   267  		return false
   268  	}
   269  	// We want to show a link to the bot settings
   270  	if sysBod.Addedtoteam().Role.IsRestrictedBot() {
   271  		return false
   272  	}
   273  	if gr.ownUsername == "" {
   274  		un, err := gr.G().GetUPAKLoader().LookupUsername(ctx, keybase1.UID(gr.uid.String()))
   275  		if err == nil {
   276  			gr.ownUsername = un.String()
   277  		}
   278  	}
   279  	if gr.ownUsername == sysBod.Addedtoteam().Addee {
   280  		return false
   281  	}
   282  	return true
   283  }
   284  
   285  func (gr *addedToTeamGrouper) makeCombined(ctx context.Context, grouped []chat1.MessageUnboxed) *chat1.MessageUnboxed {
   286  	usernames := map[string]struct{}{}
   287  	for _, j := range grouped {
   288  		if j.Valid().MessageBody.IsType(chat1.MessageType_SYSTEM) {
   289  			body := j.Valid().MessageBody.System()
   290  			typ, err := body.SystemType()
   291  			if err == nil && typ == chat1.MessageSystemType_ADDEDTOTEAM {
   292  				sysBod := body.Addedtoteam()
   293  				usernames[sysBod.Addee] = struct{}{}
   294  			}
   295  		}
   296  	}
   297  	if len(usernames) == 0 {
   298  		return nil
   299  	}
   300  
   301  	bulkAdds := make([]string, 0, len(usernames))
   302  	for username := range usernames {
   303  		bulkAdds = append(bulkAdds, username)
   304  	}
   305  
   306  	mvalid := grouped[0].Valid()
   307  	mvalid.ClientHeader.MessageType = chat1.MessageType_SYSTEM
   308  	mvalid.MessageBody = chat1.NewMessageBodyWithSystem(chat1.NewMessageSystemWithAddedtoteam(chat1.MessageSystemAddedToTeam{
   309  		BulkAdds: bulkAdds,
   310  		Adder:    mvalid.MessageBody.System().Addedtoteam().Adder,
   311  	}))
   312  	msg := chat1.NewMessageUnboxedWithValid(mvalid)
   313  	return &msg
   314  }
   315  
   316  // group duplicate errors
   317  type errGrouper struct{}
   318  
   319  var _ msgGrouper = (*errGrouper)(nil)
   320  
   321  func newErrGrouper(*globals.Context, gregor1.UID, chat1.ConversationID,
   322  	types.InboxSourceDataSourceTyp) *errGrouper {
   323  	return &errGrouper{}
   324  }
   325  
   326  func (gr *errGrouper) matches(ctx context.Context, msg chat1.MessageUnboxed, grouped []chat1.MessageUnboxed) bool {
   327  	if !msg.IsError() {
   328  		return false
   329  	} else if msg.Error().IsEphemeralError() && msg.Error().IsEphemeralExpired(time.Now()) {
   330  		return false
   331  	}
   332  	if len(grouped) > 0 && !grouped[0].SenderEq(msg) {
   333  		return false
   334  	}
   335  	return true
   336  }
   337  
   338  func (gr *errGrouper) makeCombined(ctx context.Context, grouped []chat1.MessageUnboxed) *chat1.MessageUnboxed {
   339  	if len(grouped) == 0 {
   340  		return nil
   341  	}
   342  
   343  	merr := grouped[0].Error()
   344  	if grouped[0].IsEphemeral() {
   345  		merr.ErrMsg = ephemeral.PluralizeErrorMessage(merr.ErrMsg, len(grouped))
   346  	} else if len(grouped) > 1 {
   347  		merr.ErrMsg = fmt.Sprintf("%s (occurred %d times)", merr.ErrMsg, len(grouped))
   348  	}
   349  	msg := chat1.NewMessageUnboxedWithError(merr)
   350  	return &msg
   351  }