Skip to main content

tuwunel_api_client/sync/v5/rooms/
heroes.rs

1use std::cmp::Ordering;
2
3use futures::{Stream, StreamExt, future::join};
4use ruma::{
5	MxcUri, OwnedMxcUri, OwnedUserId, RoomId, UserId,
6	api::client::sync::sync_events::v5::{
7		DisplayName,
8		response::{Hero, Heroes},
9	},
10};
11use tuwunel_core::utils::{BoolExt, ReadyExt, TryFutureExtExt, stream::WidebandExt};
12use tuwunel_service::Services;
13
14const MAX_HEROES: usize = 5;
15
16#[tracing::instrument(name = "heroes", level = "trace", skip_all)]
17pub(super) async fn calculate_heroes(
18	services: &Services,
19	sender_user: &UserId,
20	room_id: &RoomId,
21	room_name: Option<&DisplayName>,
22	room_avatar: Option<&MxcUri>,
23) -> (Option<Heroes>, Option<DisplayName>, Option<OwnedMxcUri>) {
24	let heroes: Heroes = room_name
25		.is_none()
26		.then_async(|| hero_members(services, sender_user, room_id))
27		.await
28		.unwrap_or_default();
29
30	let hero_name = match heroes.len().cmp(&(1_usize)) {
31		| Ordering::Less => None,
32		| Ordering::Equal => Some(
33			heroes[0]
34				.name
35				.clone()
36				.unwrap_or_else(|| heroes[0].user_id.as_str().into()),
37		),
38		| Ordering::Greater => {
39			let firsts = heroes[1..]
40				.iter()
41				.map(|h| {
42					h.name
43						.clone()
44						.unwrap_or_else(|| h.user_id.as_str().into())
45				})
46				.collect::<Vec<_>>()
47				.join(", ");
48
49			let last = heroes[0]
50				.name
51				.clone()
52				.unwrap_or_else(|| heroes[0].user_id.as_str().into());
53
54			Some(format!("{firsts} and {last}")).map(Into::into)
55		},
56	};
57
58	let heroes_avatar = (room_avatar.is_none() && room_name.is_none())
59		.then(|| {
60			heroes
61				.first()
62				.and_then(|hero| hero.avatar.clone())
63		})
64		.flatten();
65
66	(Some(heroes), hero_name, heroes_avatar)
67}
68
69async fn hero_members(services: &Services, sender_user: &UserId, room_id: &RoomId) -> Heroes {
70	let members = services
71		.state_cache
72		.room_members(room_id)
73		.ready_filter(|&member| member != sender_user)
74		.map(ToOwned::to_owned);
75
76	first_heroes(members, async |user_id| {
77		let content = services
78			.state_accessor
79			.get_member(room_id, &user_id)
80			.ok()
81			.await?;
82
83		let name = content
84			.displayname
85			.is_none()
86			.then_async(|| services.profile.displayname(&user_id).ok());
87
88		let avatar = content
89			.avatar_url
90			.is_none()
91			.then_async(|| services.profile.avatar_url(&user_id).ok());
92
93		let (name, avatar) = join(name, avatar).await;
94		let name = name
95			.unwrap_or(content.displayname)
96			.map(Into::into);
97
98		let hero = Hero {
99			user_id,
100			avatar: avatar.unwrap_or(content.avatar_url),
101			name,
102		};
103
104		Some(hero)
105	})
106	.collect()
107	.await
108}
109
110/// Yields the first heroes `lookup` finds, in member order.
111///
112/// A room's name and avatar derive from this list, so which members lead it
113/// must not depend on which lookup finished first.
114fn first_heroes<Members, Lookup, Found>(
115	members: Members,
116	lookup: Lookup,
117) -> impl Stream<Item = Hero> + Send
118where
119	Members: Stream<Item = OwnedUserId> + Send,
120	Lookup: Fn(OwnedUserId) -> Found + Send,
121	Found: Future<Output = Option<Hero>> + Send,
122{
123	members
124		.widen_filter_map(MAX_HEROES, lookup)
125		.take(MAX_HEROES)
126}
127
128#[cfg(test)]
129mod tests {
130	use ruma::user_id;
131	use tokio::task::yield_now;
132	use tuwunel_core::utils::IterStream;
133
134	use super::*;
135
136	#[tokio::test]
137	async fn first_heroes_keep_member_order_past_slow_and_failed_lookups() {
138		let members = [
139			user_id!("@a:example.com"),
140			user_id!("@b:example.com"),
141			user_id!("@c:example.com"),
142			user_id!("@d:example.com"),
143			user_id!("@e:example.com"),
144			user_id!("@f:example.com"),
145			user_id!("@g:example.com"),
146		];
147
148		let [slow, failed, ..] = members;
149		let heroes: Vec<_> =
150			first_heroes(members.map(ToOwned::to_owned).stream(), async |user_id| {
151				if user_id == slow {
152					yield_now().await;
153				}
154
155				user_id
156					.ne(failed)
157					.then_some(Hero { user_id, avatar: None, name: None })
158			})
159			.map(|hero| hero.user_id)
160			.collect()
161			.await;
162
163		let expected = [members[0], members[2], members[3], members[4], members[5]];
164
165		assert_eq!(heroes, expected.map(ToOwned::to_owned));
166	}
167}