1use std::collections::HashSet;
2
3use crate::db::kv::SortOrder;
4use crate::types::{DynError, Pagination, StreamSorting, Timeframe};
5
6use crate::models::{
7 post::{PostStream, StreamSource},
8 tag::TagDetails,
9 user::{Influencers, UserStream},
10};
11use serde::{Deserialize, Serialize};
12use utoipa::ToSchema;
13
14use super::user::UserDetails;
15
16#[derive(PartialEq, Deserialize)]
17pub enum ViewType {
18 Full,
19 Partial,
20}
21
22#[derive(Serialize, ToSchema, Deserialize, Default, Debug)]
23pub struct Bootstrap {
24 pub users: UserStream,
25 pub posts: PostStream,
26 pub list: BootstrapList,
27}
28
29#[derive(Serialize, ToSchema, Deserialize, Default, Debug)]
30pub struct BootstrapList {
31 pub stream: Vec<String>,
32 pub influencers: Vec<String>,
33 pub recommended: Vec<String>,
34}
35
36impl Bootstrap {
37 pub async fn get_by_id(user_id: &str, view_type: ViewType) -> Result<Option<Self>, DynError> {
47 let mut bootstrap = Self::default();
48 let mut user_ids = HashSet::new();
49
50 let Some(_) = UserDetails::get_by_id(user_id).await? else {
52 return Ok(None);
53 };
54 user_ids.insert(user_id.to_string());
55
56 let is_full_view_type = view_type == ViewType::Full;
57
58 let post_stream_by_timeline =
59 get_post_stream_timeline(user_id, StreamSource::All, 20).await?;
60
61 let post_replies =
62 bootstrap.handle_post_stream(post_stream_by_timeline, &mut user_ids, view_type);
63
64 bootstrap.add_influencers(&mut user_ids).await?;
65 bootstrap
66 .add_recommended_users(&mut user_ids, user_id)
67 .await?;
68 if is_full_view_type {
72 bootstrap
73 .fetch_and_handle_replies(post_replies, &mut user_ids, user_id)
74 .await?;
75 }
76
77 bootstrap
79 .fetch_and_merge_users(&user_ids, Some(user_id))
80 .await?;
81
82 if is_full_view_type {
84 let missing_taggers = bootstrap.collect_missing_taggers(&user_ids);
85 if !missing_taggers.is_empty() {
86 bootstrap
87 .fetch_and_merge_users(&missing_taggers, Some(user_id))
88 .await?;
89 }
90 }
91 Ok(Some(bootstrap))
92 }
93
94 fn handle_post_stream(
102 &mut self,
103 post_stream: PostStream,
104 user_ids: &mut HashSet<String>,
105 view_type: ViewType,
106 ) -> Vec<(String, String)> {
107 let is_full_view_type = view_type == ViewType::Full;
108 let mut post_replies = Vec::with_capacity(post_stream.0.len());
109
110 for post_view in post_stream.0.iter() {
111 let author_id = post_view.details.author.clone();
112 let post_id = post_view.details.id.clone();
113
114 if is_full_view_type && post_view.counts.replies > 0 {
115 post_replies.push((author_id.clone(), post_id.clone()))
116 }
117 user_ids.insert(author_id.clone());
119 Self::insert_taggers_id(&post_view.tags, user_ids);
121 if is_full_view_type {
123 self.list.stream.push(format!("{author_id}:{post_id}"));
124 }
125 }
126 self.posts.extend(post_stream);
128 post_replies
129 }
130
131 fn collect_missing_taggers(&self, user_ids: &HashSet<String>) -> HashSet<String> {
138 let mut missing_taggers = HashSet::new();
139 for user in self.users.0.iter() {
140 user.tags
141 .iter()
142 .flat_map(|tags| tags.taggers.iter())
143 .for_each(|tagger| {
144 if !user_ids.contains(tagger) {
145 missing_taggers.insert(tagger.clone());
146 }
147 });
148 }
149 missing_taggers
150 }
151
152 fn insert_taggers_id(tag_details_list: &[TagDetails], users_list: &mut HashSet<String>) {
160 for tag_details in tag_details_list.iter() {
161 for tagger_pk in tag_details.taggers.iter() {
162 users_list.insert(tagger_pk.to_string());
163 }
164 }
165 }
166
167 async fn fetch_and_merge_users(
175 &mut self,
176 user_ids: &HashSet<String>,
177 viewer_id: Option<&str>,
178 ) -> Result<(), DynError> {
179 if user_ids.is_empty() {
180 return Ok(());
181 }
182 let user_ids_vec: Vec<String> = user_ids.iter().cloned().collect();
183 if let Some(user_stream) =
186 UserStream::from_listed_user_ids(&user_ids_vec, viewer_id, None).await?
187 {
188 self.users.extend(user_stream);
189 }
190 Ok(())
191 }
192
193 async fn fetch_and_handle_replies(
204 &mut self,
205 post_replies: Vec<(String, String)>,
206 user_ids: &mut HashSet<String>,
207 viewer_id: &str,
208 ) -> Result<(), DynError> {
209 for (author_id, post_id) in post_replies {
212 let reply_stream = get_post_stream_timeline(
213 viewer_id,
214 StreamSource::PostReplies { author_id, post_id },
215 3,
216 )
217 .await?;
218 self.handle_post_stream(reply_stream, user_ids, ViewType::Partial);
219 }
220 Ok(())
221 }
222
223 async fn add_influencers(&mut self, user_ids: &mut HashSet<String>) -> Result<(), DynError> {
230 if let Some(influencers) =
231 Influencers::get_influencers(None, None, 0, 0, Timeframe::Today, true).await?
232 {
233 influencers.0.into_iter().for_each(|(id, _)| {
234 self.list.influencers.push(id.clone());
235 user_ids.insert(id);
236 });
237 }
238 Ok(())
239 }
240
241 async fn add_recommended_users(
248 &mut self,
249 user_ids: &mut HashSet<String>,
250 viewer_id: &str,
251 ) -> Result<(), DynError> {
252 if let Some(recommended_users) = UserStream::get_recommended_ids(viewer_id, None).await? {
253 recommended_users.into_iter().for_each(|id| {
254 self.list.recommended.push(id.clone());
255 user_ids.insert(id);
256 });
257 }
258 Ok(())
259 }
260}
261
262async fn get_post_stream_timeline(
263 viewer_id: &str,
264 source: StreamSource,
265 limit: usize,
266) -> Result<PostStream, DynError> {
267 let pagination = Pagination {
268 skip: Some(0),
269 limit: Some(limit),
270 start: None,
271 end: None,
272 };
273 Ok(PostStream::get_posts(
274 source,
275 pagination,
276 SortOrder::default(),
277 StreamSorting::Timeline,
278 Some(viewer_id.to_string()),
279 None,
280 None,
281 )
282 .await?
283 .unwrap_or_default())
284}