1use super::{Influencers, Muted, UserCounts, UserSearch, UserView};
2use crate::models::follow::{Followers, Following, Friends, UserFollows};
3use crate::types::{DynError, StreamReach, Timeframe};
4use std::collections::HashSet;
5
6use crate::db::kv::SortOrder;
7use crate::db::{get_neo4j_graph, queries, RedisOps};
8use crate::models::post::{PostStream, POST_REPLIES_PER_POST_KEY_PARTS};
9use serde::{Deserialize, Serialize};
10use tokio::task::spawn;
11use utoipa::ToSchema;
12
13pub const USER_MOSTFOLLOWED_KEY_PARTS: [&str; 2] = ["Users", "MostFollowed"];
14pub const USER_INFLUENCERS_KEY_PARTS: [&str; 2] = ["Users", "Influencers"];
15pub const CACHE_USER_RECOMMENDED_KEY_PARTS: [&str; 3] = ["Cache", "Users", "Recommended"];
16pub const CACHE_USER_RECOMMENDED_TTL: i64 = 12 * 60 * 60;
18
19#[derive(Deserialize, ToSchema, Debug, Clone, PartialEq)]
20#[serde(rename_all = "snake_case")]
21pub enum UserStreamSource {
22 Followers,
23 Following,
24 Friends,
25 Muted,
26 MostFollowed,
27 Influencers,
28 Recommended,
29 PostReplies,
30}
31
32pub struct UserStreamInput {
33 pub user_id: Option<String>,
34 pub skip: Option<usize>,
35 pub limit: Option<usize>,
36 pub source: UserStreamSource,
37 pub reach: Option<StreamReach>,
38 pub timeframe: Option<Timeframe>,
39 pub preview: Option<bool>,
40 pub author_id: Option<String>,
41 pub post_id: Option<String>,
42}
43
44#[derive(Serialize, Deserialize, ToSchema, Default, Debug)]
45pub struct UserStream(pub Vec<UserView>);
46
47impl RedisOps for UserStream {}
48
49impl UserStream {
50 pub fn extend(&mut self, user_stream: UserStream) {
51 self.0.extend(user_stream.0);
52 }
53
54 pub async fn get_by_id(
55 input: UserStreamInput,
56 viewer_id: Option<String>,
57 depth: Option<u8>,
58 ) -> Result<Option<Self>, DynError> {
59 let user_ids = Self::get_user_list_from_source(input).await?;
60 match user_ids {
61 Some(users) => Self::from_listed_user_ids(&users, viewer_id.as_deref(), depth).await,
62 None => Ok(None),
63 }
64 }
65
66 pub async fn get_from_username_search(
67 username: &str,
68 viewer_id: Option<&str>,
69 skip: Option<usize>,
70 limit: Option<usize>,
71 ) -> Result<Option<Self>, DynError> {
72 let user_ids = UserSearch::get_by_name(username, skip, limit)
73 .await?
74 .map(|result| result.0);
75
76 match user_ids {
77 Some(users) => Self::from_listed_user_ids(&users, viewer_id, None).await,
78 None => Ok(None),
79 }
80 }
81
82 pub async fn from_listed_user_ids(
83 user_ids: &[String],
84 viewer_id: Option<&str>,
85 depth: Option<u8>,
86 ) -> Result<Option<Self>, DynError> {
87 let viewer_id = viewer_id.map(|id| id.to_string());
91 let mut handles = Vec::with_capacity(user_ids.len());
92
93 for user_id in user_ids {
94 let user_id = user_id.clone();
95 let viewer_id = viewer_id.clone();
96 let handle =
97 spawn(
98 async move { UserView::get_by_id(&user_id, viewer_id.as_deref(), depth).await },
99 );
100 handles.push(handle);
101 }
102
103 let mut user_views = Vec::with_capacity(user_ids.len());
104
105 for handle in handles {
106 if let Some(user_view) = handle.await?? {
107 user_views.push(user_view);
108 }
109 }
110
111 match user_views.is_empty() {
112 true => Ok(None),
113 false => Ok(Some(Self(user_views))),
114 }
115 }
116
117 pub async fn add_to_most_followed_sorted_set(
119 user_id: &str,
120 counts: &UserCounts,
121 ) -> Result<(), DynError> {
122 Self::put_index_sorted_set(
123 &USER_MOSTFOLLOWED_KEY_PARTS,
124 &[(counts.followers as f64, user_id)],
125 None,
126 None,
127 )
128 .await
129 }
130
131 pub async fn add_to_influencers_sorted_set(
133 user_id: &str,
134 counts: &UserCounts,
135 ) -> Result<(), DynError> {
136 let score = (counts.tagged + counts.posts) as f64 * (counts.followers as f64).sqrt();
137 Self::put_index_sorted_set(&USER_INFLUENCERS_KEY_PARTS, &[(score, user_id)], None, None)
138 .await
139 }
140 pub async fn get_recommended_ids(
142 user_id: &str,
143 limit: Option<usize>,
144 ) -> Result<Option<Vec<String>>, DynError> {
145 let count = limit.unwrap_or(5) as isize;
146
147 if let Some(cached_data) = Self::try_get_cached_recommended(user_id, count).await? {
149 return Ok(Some(cached_data));
150 }
151
152 let mut result;
154 {
155 let graph = get_neo4j_graph()?;
156 let query = queries::get::recommend_users(user_id, 30);
158
159 let graph = graph.lock().await;
160 result = graph.execute(query).await?;
161 }
162
163 let mut user_ids = Vec::new();
164
165 while let Some(row) = result.next().await? {
166 if let Some(user_id) = row.get::<Option<String>>("recommended_user_id")? {
167 user_ids.push(user_id);
168 }
169 }
170
171 if user_ids.is_empty() {
172 Ok(None)
173 } else {
174 Self::cache_recommended_users(user_id, &user_ids).await?;
175 if let Some(limit) = limit {
176 user_ids.truncate(limit);
177 };
178 Ok(Some(user_ids))
179 }
180 }
181
182 async fn try_get_cached_recommended(
183 user_id: &str,
184 count: isize,
185 ) -> Result<Option<Vec<String>>, DynError> {
186 let key_parts = &["Cache", "Recommended", user_id];
187 Self::try_get_random_from_index_set(
188 key_parts,
189 count,
190 Some(CACHE_USER_RECOMMENDED_KEY_PARTS.join(":")),
191 )
192 .await
193 }
194
195 async fn cache_recommended_users(user_id: &str, user_ids: &[String]) -> Result<(), DynError> {
197 let values: Vec<&str> = user_ids.iter().map(|s| s.as_str()).collect();
198 Self::put_index_set(
200 &[user_id],
201 &values,
202 Some(CACHE_USER_RECOMMENDED_TTL),
203 Some(CACHE_USER_RECOMMENDED_KEY_PARTS.join(":")),
204 )
205 .await
206 }
207
208 async fn get_post_replies_ids(
209 post_id: Option<String>,
210 author_id: Option<String>,
211 ) -> Result<Option<Vec<String>>, DynError> {
212 let post_id = post_id
213 .ok_or("Post ID should be provided for user streams with source 'post_replies'")?;
214 let author_id = author_id
215 .ok_or("Author ID should be provided for user streams with source 'post_replies'")?;
216 let key_parts = [
217 &POST_REPLIES_PER_POST_KEY_PARTS[..],
218 &[author_id.as_str(), post_id.as_str()],
219 ]
220 .concat();
221 let replies = PostStream::try_from_index_sorted_set(
222 &key_parts,
223 None,
224 None,
225 None,
226 None,
227 SortOrder::Descending,
228 None,
229 )
230 .await?;
231
232 let unique_user_ids: HashSet<String> = if let Some(replies) = replies {
234 replies
235 .into_iter()
236 .filter_map(|reply| reply.0.split(':').next().map(|s| s.to_string()))
237 .collect()
238 } else {
239 return Ok(None);
241 };
242
243 Ok(Some(unique_user_ids.into_iter().collect()))
245 }
246
247 pub async fn get_user_list_from_source(
249 input: UserStreamInput,
250 ) -> Result<Option<Vec<String>>, DynError> {
251 let UserStreamInput {
252 user_id,
253 skip,
254 limit,
255 source,
256 reach,
257 timeframe,
258 preview,
259 author_id,
260 post_id,
261 } = input;
262 let user_ids =
263 match source {
264 UserStreamSource::Followers => Followers::get_by_id(
265 user_id
266 .ok_or(
267 "User ID should be provided for user streams with source 'followers'"
268 .to_string(),
269 )?
270 .as_str(),
271 skip,
272 limit,
273 )
274 .await?
275 .map(|u| u.0),
276 UserStreamSource::Following => Following::get_by_id(
277 user_id
278 .ok_or(
279 "User ID should be provided for user streams with source 'following'"
280 .to_string(),
281 )?
282 .as_str(),
283 skip,
284 limit,
285 )
286 .await?
287 .map(|u| u.0),
288 UserStreamSource::Friends => Friends::get_by_id(
289 user_id
290 .ok_or(
291 "User ID should be provided for user streams with source 'friends'"
292 .to_string(),
293 )?
294 .as_str(),
295 skip,
296 limit,
297 )
298 .await?
299 .map(|u| u.0),
300 UserStreamSource::Muted => Muted::get_by_id(
301 user_id
302 .ok_or(
303 "User ID should be provided for user streams with source 'muted'"
304 .to_string(),
305 )?
306 .as_str(),
307 skip,
308 limit,
309 )
310 .await?
311 .map(|u| u.0),
312 UserStreamSource::MostFollowed => Self::try_from_index_sorted_set(
313 &USER_MOSTFOLLOWED_KEY_PARTS,
314 None,
315 None,
316 skip,
317 limit,
318 SortOrder::Descending,
319 None,
320 )
321 .await?
322 .map(|set| set.into_iter().map(|(user_id, _score)| user_id).collect()),
323 UserStreamSource::Influencers => Influencers::get_influencers(
324 user_id.as_deref(),
325 Some(reach.unwrap_or(StreamReach::Wot(3))),
326 skip.unwrap_or(0),
327 limit.unwrap_or(10).min(100),
328 timeframe.unwrap_or(Timeframe::AllTime),
329 preview.unwrap_or(false),
330 )
331 .await?
332 .map(|result| {
333 result
334 .iter()
335 .map(|(influencer_id, _)| influencer_id.clone())
336 .collect()
337 }),
338 UserStreamSource::Recommended => UserStream::get_recommended_ids(
339 user_id
340 .ok_or(
341 "User ID should be provided for user streams with source 'recommended'"
342 .to_string(),
343 )?
344 .as_str(),
345 limit,
346 )
347 .await?,
348 UserStreamSource::PostReplies => {
349 UserStream::get_post_replies_ids(post_id, author_id).await?
350 }
351 };
352 Ok(user_ids)
353 }
354}