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 }