1use crate::db::kv::SortOrder;
2use crate::db::{queries, retrieve_from_graph, RedisOps};
3use crate::types::routes::HotTagsInputDTO;
4use crate::types::{DynError, StreamReach, Timeframe};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::ops::Deref;
8use utoipa::ToSchema;
9
10use super::global::{HotTagsTaggers, Taggers};
11use super::TaggedType;
12
13pub const HOT_TAGS_CACHE_PREFIX: &str = "Cache";
14pub const POST_HOT_TAGS: [&str; 3] = ["Tags", "Post", "Hot"];
15
16#[derive(Deserialize, Serialize, ToSchema, Debug, Clone)]
17pub struct HotTag {
18 pub label: String,
19 pub taggers_id: Taggers,
20 pub tagged_count: u64,
21 pub taggers_count: usize,
22}
23
24#[derive(Serialize, Deserialize, Debug, ToSchema, Default, Clone)]
26pub struct HotTags(pub Vec<HotTag>);
27
28impl RedisOps for HotTags {}
29
30impl Deref for HotTags {
32 type Target = Vec<HotTag>;
33
34 fn deref(&self) -> &Self::Target {
35 &self.0
36 }
37}
38
39impl FromIterator<HotTag> for HotTags {
42 fn from_iter<I: IntoIterator<Item = HotTag>>(iter: I) -> Self {
43 HotTags(iter.into_iter().collect())
44 }
45}
46
47impl HotTags {
48 pub async fn get_hot_tags(
56 user_id: Option<String>,
57 reach: Option<StreamReach>,
58 hot_tags_input: &HotTagsInputDTO,
59 ) -> Result<Option<HotTags>, DynError> {
60 match user_id {
61 Some(user_id) => {
62 HotTags::get_hot_tags_by_reach(
63 user_id,
64 reach.unwrap_or(StreamReach::Following),
65 hot_tags_input,
66 )
67 .await
68 }
69 None => HotTags::get_global_hot_tags(hot_tags_input).await,
70 }
71 }
72
73 async fn get_hot_tags_by_reach(
82 user_id: String,
83 reach: StreamReach,
84 hot_tags_input: &HotTagsInputDTO,
85 ) -> Result<Option<HotTags>, DynError> {
86 let query = queries::get::get_hot_tags_by_reach(user_id.as_str(), reach, hot_tags_input);
87 retrieve_from_graph::<HotTags>(query, "hot_tags").await
88 }
89
90 async fn get_global_hot_tags(
99 hot_tags_input: &HotTagsInputDTO,
100 ) -> Result<Option<HotTags>, DynError> {
101 let cached_hot_tags = HotTags::get_from_global_cache(hot_tags_input).await?;
102
103 if let Some(hot_tags) = &cached_hot_tags {
104 if hot_tags.0.is_empty() {
105 return Ok(None);
106 }
107 return Ok(cached_hot_tags);
108 }
109
110 let hot_tag_input = HotTagsInputDTO::new(
111 hot_tags_input.timeframe.clone(),
112 100,
113 0,
114 20,
115 hot_tags_input.tagged_type.clone(),
116 );
117 let query = queries::get::get_global_hot_tags(&hot_tag_input);
118 let result = retrieve_from_graph::<HotTags>(query, "hot_tags").await?;
119
120 let hot_tags = match result {
121 Some(hot_tags) => hot_tags,
122 None => return Ok(None),
123 };
124 if !hot_tags.is_empty() {
125 HotTags::set_to_global_cache(hot_tags.clone(), hot_tags_input).await?;
126 }
127
128 HotTags::get_from_global_cache(hot_tags_input).await
129 }
130
131 async fn get_from_global_cache(
141 hot_tags_input: &HotTagsInputDTO,
142 ) -> Result<Option<HotTags>, DynError> {
143 let timeframe = hot_tags_input.timeframe.to_string();
144 let hot_tag_key_parts = Self::build_hot_tags_key_parts(&timeframe);
145
146 let hot_tag_taggers = Taggers::get_from_index(&timeframe).await?;
147
148 let hot_tags_score = HotTags::try_from_index_sorted_set(
149 &hot_tag_key_parts,
150 None,
151 None,
152 Some(hot_tags_input.skip),
153 Some(hot_tags_input.limit),
154 SortOrder::Descending,
155 Some(HOT_TAGS_CACHE_PREFIX),
156 )
157 .await?;
158
159 let (hot_tags_score, hot_tag_taggers) = match (hot_tags_score, hot_tag_taggers) {
160 (Some(score_list), Some(taggers)) => {
161 if score_list.is_empty() {
163 return Ok(Some(HotTags(Vec::new())));
164 }
165 (score_list, taggers)
166 }
167 _ => return Ok(None),
168 };
169
170 let mut hot_tags = Vec::with_capacity(hot_tags_score.len());
171
172 for (label, score) in hot_tags_score {
173 if let Some(taggers) = hot_tag_taggers.get(&label) {
174 let taggers_id: Vec<String> =
176 Taggers::get_taggers_by_pagination(taggers, 0, hot_tags_input.taggers_limit);
177 hot_tags.push(HotTag {
178 label,
179 taggers_id: Taggers(taggers_id),
180 tagged_count: score as u64,
181 taggers_count: taggers.len(),
182 });
183 }
184 }
185 Ok(Some(HotTags(hot_tags)))
186 }
187
188 async fn set_to_global_cache(
198 hot_tags_list: HotTags,
199 hot_tags_input: &HotTagsInputDTO,
200 ) -> Result<(), DynError> {
201 let timeframe = hot_tags_input.timeframe.to_string();
202 let hot_tag_key_parts = Self::build_hot_tags_key_parts(&timeframe);
203
204 let mut hot_tags_score = Vec::with_capacity(hot_tags_list.len());
205
206 let taggers: HashMap<String, Taggers> = hot_tags_list
207 .iter()
208 .map(|tag| {
209 hot_tags_score.push((tag.tagged_count as f64, tag.label.as_str()));
210 (tag.label.clone(), tag.taggers_id.clone())
211 })
212 .collect();
213
214 Taggers::put_to_index(HotTagsTaggers(taggers), &hot_tags_input.timeframe).await?;
215
216 HotTags::put_index_sorted_set(
218 &hot_tag_key_parts,
219 &hot_tags_score,
220 Some(HOT_TAGS_CACHE_PREFIX),
221 Some(hot_tags_input.timeframe.to_cache_period()),
222 )
223 .await?;
224 Ok(())
225 }
226
227 fn build_hot_tags_key_parts(timeframe: &str) -> Vec<&str> {
232 [&POST_HOT_TAGS[..], &[timeframe]].concat()
233 }
234
235 pub async fn reindex() -> Result<(), DynError> {
239 let all_timeframe_input =
240 HotTagsInputDTO::new(Timeframe::AllTime, 100, 0, 20, Some(TaggedType::Post));
241 HotTags::get_global_hot_tags(&all_timeframe_input).await?;
242
243 let month_timeframe_input =
244 HotTagsInputDTO::new(Timeframe::ThisMonth, 100, 0, 20, Some(TaggedType::Post));
245 HotTags::get_global_hot_tags(&month_timeframe_input).await?;
246 Ok(())
247 }
248}