1use std::collections::{BTreeMap, BTreeSet};
2use std::time::Instant;
3
4use crate::discord::ids::{
5 Id,
6 marker::{ChannelMarker, GuildMarker, RoleMarker, UserMarker},
7};
8use crate::discord::{ActivityInfo, MemberInfo, PresenceStatus, RoleInfo};
9
10use crate::discord::state::{
11 DiscordState, MAX_RECENT_MEMBER_GUILDS, TYPING_INDICATOR_TTL, is_fallback_identity,
12};
13
14#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct TypingUserState {
16 pub user_id: Id<UserMarker>,
17 pub display_name: Option<String>,
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct GuildMemberState {
22 pub user_id: Id<UserMarker>,
23 pub display_name: String,
24 pub username: Option<String>,
27 pub is_bot: bool,
28 pub avatar_url: Option<String>,
29 pub role_ids: Vec<Id<RoleMarker>>,
30 pub status: PresenceStatus,
31}
32
33#[cfg(test)]
34#[allow(dead_code)]
35impl GuildMemberState {
36 pub(crate) fn test(user_id: Id<UserMarker>, display_name: impl Into<String>) -> Self {
37 Self {
38 user_id,
39 display_name: display_name.into(),
40 username: None,
41 is_bot: false,
42 avatar_url: None,
43 role_ids: Vec::new(),
44 status: PresenceStatus::Offline,
45 }
46 }
47}
48
49#[derive(Clone, Debug, Eq, PartialEq)]
50pub struct RoleState {
51 pub id: Id<RoleMarker>,
52 pub name: String,
53 pub color: Option<u32>,
54 pub position: i64,
55 pub hoist: bool,
56 pub permissions: u64,
59}
60
61impl DiscordState {
62 pub fn typing_users(&self, channel_id: Id<ChannelMarker>) -> Vec<TypingUserState> {
63 let now = Instant::now();
64 let Some(channel_typers) = self.presence.typing.get(&channel_id) else {
65 return Vec::new();
66 };
67 let mut fresh: Vec<(Id<UserMarker>, Instant, Option<String>)> = channel_typers
68 .iter()
69 .filter(|(_, indicator)| now.duration_since(indicator.started) <= TYPING_INDICATOR_TTL)
70 .map(|(user_id, indicator)| {
71 (*user_id, indicator.started, indicator.display_name.clone())
72 })
73 .collect();
74 fresh.sort_by_key(|(_, started, _)| std::cmp::Reverse(*started));
77 fresh
78 .into_iter()
79 .map(|(user_id, _, display_name)| TypingUserState {
80 user_id,
81 display_name,
82 })
83 .collect()
84 }
85
86 pub fn user_presence(&self, user_id: Id<UserMarker>) -> Option<PresenceStatus> {
87 self.user_presence_for_guild(None, user_id)
88 }
89
90 pub fn user_presence_for_guild(
91 &self,
92 guild_id: Option<Id<GuildMarker>>,
93 user_id: Id<UserMarker>,
94 ) -> Option<PresenceStatus> {
95 guild_id
96 .and_then(|guild_id| {
97 self.presence
98 .guild_user_presences
99 .get(&(guild_id, user_id))
100 .copied()
101 })
102 .or_else(|| self.presence.user_presences.get(&user_id).copied())
103 }
104
105 pub fn user_activities(&self, user_id: Id<UserMarker>) -> &[ActivityInfo] {
106 self.user_activities_for_guild(None, user_id)
107 }
108
109 pub fn user_activities_for_guild(
110 &self,
111 guild_id: Option<Id<GuildMarker>>,
112 user_id: Id<UserMarker>,
113 ) -> &[ActivityInfo] {
114 guild_id
115 .and_then(|guild_id| {
116 self.presence
117 .guild_user_activities
118 .get(&(guild_id, user_id))
119 })
120 .or_else(|| self.presence.user_activities.get(&user_id))
121 .map(Vec::as_slice)
122 .unwrap_or_default()
123 }
124
125 pub fn members_for_guild(&self, guild_id: Id<GuildMarker>) -> Vec<&GuildMemberState> {
126 self.guild_details
127 .members
128 .get(&guild_id)
129 .map(|map| map.values().collect())
130 .unwrap_or_default()
131 }
132
133 pub fn roles_for_guild(&self, guild_id: Id<GuildMarker>) -> Vec<&RoleState> {
134 self.guild_details
135 .roles
136 .get(&guild_id)
137 .map(|map| map.values().collect())
138 .unwrap_or_default()
139 }
140
141 pub fn member_role_color(
142 &self,
143 guild_id: Id<GuildMarker>,
144 user_id: Id<UserMarker>,
145 ) -> Option<u32> {
146 let member = self.guild_details.members.get(&guild_id)?.get(&user_id)?;
147 let roles = self.guild_details.roles.get(&guild_id)?;
148 selected_member_role_color(member, roles)
149 }
150
151 pub fn member_display_name(
152 &self,
153 guild_id: Id<GuildMarker>,
154 user_id: Id<UserMarker>,
155 ) -> Option<&str> {
156 self.guild_details
157 .members
158 .get(&guild_id)
159 .and_then(|members| members.get(&user_id))
160 .map(|member| member.display_name.as_str())
161 }
162
163 pub fn member_has_known_name(
164 &self,
165 guild_id: Id<GuildMarker>,
166 user_id: Id<UserMarker>,
167 ) -> bool {
168 self.guild_details
169 .members
170 .get(&guild_id)
171 .and_then(|members| members.get(&user_id))
172 .map(|member| member.username.is_some())
173 .unwrap_or(false)
174 }
175
176 pub(in crate::discord) fn update_user_activities(
177 &mut self,
178 user_id: Id<UserMarker>,
179 activities: &[ActivityInfo],
180 ) {
181 if activities.is_empty() {
182 self.presence.user_activities.remove(&user_id);
183 } else {
184 self.presence
185 .user_activities
186 .insert(user_id, activities.to_vec());
187 }
188 }
189
190 pub(in crate::discord) fn update_guild_user_activities(
191 &mut self,
192 guild_id: Id<GuildMarker>,
193 user_id: Id<UserMarker>,
194 activities: &[ActivityInfo],
195 ) {
196 let key = (guild_id, user_id);
197 if activities.is_empty() {
198 self.presence.guild_user_activities.remove(&key);
199 } else {
200 self.presence
201 .guild_user_activities
202 .insert(key, activities.to_vec());
203 }
204 }
205
206 pub(in crate::discord) fn update_cached_guild_activities_for_user(
207 &mut self,
208 user_id: Id<UserMarker>,
209 activities: &[ActivityInfo],
210 ) {
211 let guild_ids: Vec<_> = self
212 .presence
213 .guild_user_activities
214 .keys()
215 .filter_map(|(guild_id, activity_user_id)| {
216 (*activity_user_id == user_id).then_some(*guild_id)
217 })
218 .collect();
219 for guild_id in guild_ids {
220 self.update_guild_user_activities(guild_id, user_id, activities);
221 }
222 }
223
224 pub(in crate::discord) fn upsert_guild_member(
225 &mut self,
226 guild_id: Id<GuildMarker>,
227 member: &MemberInfo,
228 ) -> bool {
229 let was_known = self
230 .guild_details
231 .members
232 .get(&guild_id)
233 .is_some_and(|members| members.contains_key(&member.user_id));
234 let previous_status = self
235 .guild_details
236 .members
237 .get(&guild_id)
238 .and_then(|members| members.get(&member.user_id))
239 .map(|member| member.status);
240 let preserve_current_user_roles = self.session.current_user_id == Some(member.user_id)
241 && member.role_ids.is_empty()
242 && is_fallback_identity(member.username.as_deref(), &member.display_name);
243 let protected_role_ids = preserve_current_user_roles
244 .then(|| {
245 self.guild_details
246 .current_user_role_ids
247 .get(&guild_id)
248 .cloned()
249 })
250 .flatten();
251
252 let entry = self.guild_details.members.entry(guild_id).or_default();
253 upsert_member(entry, member, previous_status);
254
255 if self.session.current_user_id == Some(member.user_id) {
256 if let Some(cached_role_ids) = protected_role_ids {
257 if let Some(current_member) = entry.get_mut(&member.user_id) {
258 current_member.role_ids = cached_role_ids.clone();
259 }
260 self.guild_details
261 .current_user_role_ids
262 .insert(guild_id, cached_role_ids);
263 } else if let Some(current_member) = entry.get(&member.user_id) {
264 self.guild_details
265 .current_user_role_ids
266 .insert(guild_id, current_member.role_ids.clone());
267 }
268 }
269
270 was_known
271 }
272
273 pub(in crate::discord) fn refresh_current_user_role_cache(&mut self) {
274 let Some(current_user_id) = self.session.current_user_id else {
275 return;
276 };
277 for (guild_id, members) in &self.guild_details.members {
278 if let Some(member) = members.get(¤t_user_id) {
279 self.guild_details
280 .current_user_role_ids
281 .insert(*guild_id, member.role_ids.clone());
282 }
283 }
284 }
285
286 pub(in crate::discord) fn current_user_role_ids_for_guild(
287 &self,
288 guild_id: Id<GuildMarker>,
289 ) -> Option<&[Id<RoleMarker>]> {
290 self.guild_details
291 .current_user_role_ids
292 .get(&guild_id)
293 .map(Vec::as_slice)
294 .or_else(|| {
295 let current_user_id = self.session.current_user_id?;
296 self.guild_details
297 .members
298 .get(&guild_id)
299 .and_then(|members| members.get(¤t_user_id))
300 .map(|member| member.role_ids.as_slice())
301 })
302 }
303
304 pub(in crate::discord) fn record_selected_member_guild(
305 &mut self,
306 guild_id: Option<Id<GuildMarker>>,
307 ) {
308 if let Some(guild_id) = guild_id {
309 self.guild_details
310 .member_cache_guild_order
311 .retain(|existing| *existing != guild_id);
312 self.guild_details
313 .member_cache_guild_order
314 .push_back(guild_id);
315 }
316 self.prune_member_cache(guild_id);
317 }
318
319 fn prune_member_cache(&mut self, selected_guild_id: Option<Id<GuildMarker>>) {
320 let mut keep_guilds: BTreeSet<Id<GuildMarker>> = self
321 .guild_details
322 .member_cache_guild_order
323 .iter()
324 .rev()
325 .take(MAX_RECENT_MEMBER_GUILDS)
326 .copied()
327 .collect();
328 if let Some(selected_guild_id) = selected_guild_id {
329 keep_guilds.insert(selected_guild_id);
330 }
331 self.guild_details
332 .member_cache_guild_order
333 .retain(|guild_id| keep_guilds.contains(guild_id));
334
335 let current_user_id = self.session.current_user_id;
336 let message_authors = self.message_author_ids_by_guild();
337 self.guild_details.members.retain(|guild_id, members| {
338 if keep_guilds.contains(guild_id) {
339 return true;
340 }
341 members.retain(|user_id, _| {
342 current_user_id == Some(*user_id)
343 || message_authors
344 .get(guild_id)
345 .is_some_and(|authors| authors.contains(user_id))
346 });
347 !members.is_empty()
348 });
349 self.prune_presence_activity_cache();
350 }
351
352 fn message_author_ids_by_guild(&self) -> BTreeMap<Id<GuildMarker>, BTreeSet<Id<UserMarker>>> {
353 let mut authors: BTreeMap<Id<GuildMarker>, BTreeSet<Id<UserMarker>>> = BTreeMap::new();
354 for message in self
355 .message_cache
356 .messages
357 .values()
358 .chain(self.message_cache.pinned_messages.values())
359 .flat_map(|messages| messages.iter())
360 {
361 if let Some(guild_id) = message.guild_id {
362 authors
363 .entry(guild_id)
364 .or_default()
365 .insert(message.author_id);
366 }
367 collect_nested_message_authors(&mut authors, message.guild_id, &message.reply);
368 }
369 authors
370 }
371
372 fn prune_presence_activity_cache(&mut self) {
373 let retained_pairs = self.retained_guild_presence_keys();
374 self.presence
375 .guild_user_presences
376 .retain(|key, _| retained_pairs.contains(key));
377 self.presence
378 .guild_user_activities
379 .retain(|key, _| retained_pairs.contains(key));
380
381 let retained_users = self.retained_presence_user_ids();
382 self.presence
383 .user_presences
384 .retain(|user_id, _| retained_users.contains(user_id));
385 self.presence
386 .user_activities
387 .retain(|user_id, _| retained_users.contains(user_id));
388 }
389
390 fn retained_presence_user_ids(&self) -> BTreeSet<Id<UserMarker>> {
391 let mut retained = BTreeSet::new();
392 if let Some(current_user_id) = self.session.current_user_id {
393 retained.insert(current_user_id);
394 }
395 for members in self.guild_details.members.values() {
396 retained.extend(members.keys().copied());
397 }
398 for channel in self
399 .navigation
400 .channels
401 .values()
402 .filter(|channel| channel.guild_id.is_none())
403 {
404 retained.extend(channel.recipients.iter().map(|recipient| recipient.user_id));
405 }
406 for profile_key in self.profiles.user_profiles.keys() {
407 retained.insert(profile_key.user_id);
408 }
409 retained
410 }
411
412 fn retained_guild_presence_keys(&self) -> BTreeSet<(Id<GuildMarker>, Id<UserMarker>)> {
413 let mut retained = BTreeSet::new();
414 for (guild_id, members) in &self.guild_details.members {
415 retained.extend(members.keys().map(|user_id| (*guild_id, *user_id)));
416 }
417 retained
418 }
419}
420
421fn collect_nested_message_authors(
422 authors: &mut BTreeMap<Id<GuildMarker>, BTreeSet<Id<UserMarker>>>,
423 guild_id: Option<Id<GuildMarker>>,
424 reply: &Option<crate::discord::ReplyInfo>,
425) {
426 let (Some(guild_id), Some(reply)) = (guild_id, reply) else {
427 return;
428 };
429 if let Some(author_id) = reply.author_id {
430 authors.entry(guild_id).or_default().insert(author_id);
431 }
432}
433
434pub(in crate::discord) fn upsert_member(
435 map: &mut BTreeMap<Id<UserMarker>, GuildMemberState>,
436 member: &MemberInfo,
437 previous_status: Option<PresenceStatus>,
438) {
439 let status = previous_status.unwrap_or(PresenceStatus::Unknown);
440
441 let is_fallback = is_fallback_identity(member.username.as_deref(), &member.display_name);
442 let existing_complete = is_fallback
443 .then(|| map.get(&member.user_id))
444 .flatten()
445 .filter(|e| e.username.is_some());
446 let (display_name, username, avatar_url) = match existing_complete {
447 Some(existing) => (
448 existing.display_name.clone(),
449 existing.username.clone(),
450 existing.avatar_url.clone(),
451 ),
452 None => (
453 member.display_name.clone(),
454 member.username.clone(),
455 member.avatar_url.clone(),
456 ),
457 };
458
459 map.insert(
460 member.user_id,
461 GuildMemberState {
462 user_id: member.user_id,
463 display_name,
464 username,
465 is_bot: member.is_bot,
466 avatar_url,
467 role_ids: member.role_ids.clone(),
468 status,
469 },
470 );
471}
472
473pub(in crate::discord) fn role_map(roles: &[RoleInfo]) -> BTreeMap<Id<RoleMarker>, RoleState> {
474 roles
475 .iter()
476 .map(|role| (role.id, role_state(role)))
477 .collect()
478}
479
480pub(in crate::discord) fn role_state(role: &RoleInfo) -> RoleState {
481 RoleState {
482 id: role.id,
483 name: role.name.clone(),
484 color: role.color,
485 position: role.position,
486 hoist: role.hoist,
487 permissions: role.permissions,
488 }
489}
490
491pub(in crate::discord) fn selected_member_role_color(
492 member: &GuildMemberState,
493 roles: &BTreeMap<Id<RoleMarker>, RoleState>,
494) -> Option<u32> {
495 selected_role_ids_color(&member.role_ids, roles)
496}
497
498pub(in crate::discord) fn selected_role_ids_color(
499 role_ids: &[Id<RoleMarker>],
500 roles: &BTreeMap<Id<RoleMarker>, RoleState>,
501) -> Option<u32> {
502 role_ids
503 .iter()
504 .filter_map(|role_id| roles.get(role_id))
505 .filter(|role| role.color.is_some_and(|color| color != 0))
506 .min_by(|left, right| role_display_order(left, right))
507 .and_then(|role| role.color)
508}
509
510fn role_display_order(left: &RoleState, right: &RoleState) -> std::cmp::Ordering {
511 right
512 .position
513 .cmp(&left.position)
514 .then(left.id.get().cmp(&right.id.get()))
515}