use super::{Influencers, Muted, UserCounts, UserSearch, UserView};
use crate::models::follow::{Followers, Following, Friends, UserFollows};
use crate::types::{DynError, StreamReach, Timeframe};
use std::collections::HashSet;
use crate::db::kv::SortOrder;
use crate::db::{get_neo4j_graph, queries, RedisOps};
use crate::models::post::{PostStream, POST_REPLIES_PER_POST_KEY_PARTS};
use serde::{Deserialize, Serialize};
use tokio::task::spawn;
use utoipa::ToSchema;
pub const USER_MOSTFOLLOWED_KEY_PARTS: [&str; 2] = ["Users", "MostFollowed"];
pub const USER_INFLUENCERS_KEY_PARTS: [&str; 2] = ["Users", "Influencers"];
pub const CACHE_USER_RECOMMENDED_KEY_PARTS: [&str; 3] = ["Cache", "Users", "Recommended"];
pub const CACHE_USER_RECOMMENDED_TTL: i64 = 12 * 60 * 60;
#[derive(Deserialize, ToSchema, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum UserStreamSource {
Followers,
Following,
Friends,
Muted,
MostFollowed,
Influencers,
Recommended,
PostReplies,
}
pub struct UserStreamInput {
pub user_id: Option<String>,
pub skip: Option<usize>,
pub limit: Option<usize>,
pub source: UserStreamSource,
pub reach: Option<StreamReach>,
pub timeframe: Option<Timeframe>,
pub preview: Option<bool>,
pub author_id: Option<String>,
pub post_id: Option<String>,
}
#[derive(Serialize, Deserialize, ToSchema, Default, Debug)]
pub struct UserStream(pub Vec<UserView>);
impl RedisOps for UserStream {}
impl UserStream {
pub fn extend(&mut self, user_stream: UserStream) {
self.0.extend(user_stream.0);
}
pub async fn get_by_id(
input: UserStreamInput,
viewer_id: Option<String>,
depth: Option<u8>,
) -> Result<Option<Self>, DynError> {
let user_ids = Self::get_user_list_from_source(input).await?;
match user_ids {
Some(users) => Self::from_listed_user_ids(&users, viewer_id.as_deref(), depth).await,
None => Ok(None),
}
}
pub async fn get_from_username_search(
username: &str,
viewer_id: Option<&str>,
skip: Option<usize>,
limit: Option<usize>,
) -> Result<Option<Self>, DynError> {
let user_ids = UserSearch::get_by_name(username, skip, limit)
.await?
.map(|result| result.0);
match user_ids {
Some(users) => Self::from_listed_user_ids(&users, viewer_id, None).await,
None => Ok(None),
}
}
pub async fn from_listed_user_ids(
user_ids: &[String],
viewer_id: Option<&str>,
depth: Option<u8>,
) -> Result<Option<Self>, DynError> {
let viewer_id = viewer_id.map(|id| id.to_string());
let mut handles = Vec::with_capacity(user_ids.len());
for user_id in user_ids {
let user_id = user_id.clone();
let viewer_id = viewer_id.clone();
let handle =
spawn(
async move { UserView::get_by_id(&user_id, viewer_id.as_deref(), depth).await },
);
handles.push(handle);
}
let mut user_views = Vec::with_capacity(user_ids.len());
for handle in handles {
if let Some(user_view) = handle.await?? {
user_views.push(user_view);
}
}
match user_views.is_empty() {
true => Ok(None),
false => Ok(Some(Self(user_views))),
}
}
pub async fn add_to_most_followed_sorted_set(
user_id: &str,
counts: &UserCounts,
) -> Result<(), DynError> {
Self::put_index_sorted_set(
&USER_MOSTFOLLOWED_KEY_PARTS,
&[(counts.followers as f64, user_id)],
None,
None,
)
.await
}
pub async fn add_to_influencers_sorted_set(
user_id: &str,
counts: &UserCounts,
) -> Result<(), DynError> {
let score = (counts.tagged + counts.posts) as f64 * (counts.followers as f64).sqrt();
Self::put_index_sorted_set(&USER_INFLUENCERS_KEY_PARTS, &[(score, user_id)], None, None)
.await
}
pub async fn get_recommended_ids(
user_id: &str,
limit: Option<usize>,
) -> Result<Option<Vec<String>>, DynError> {
let count = limit.unwrap_or(5) as isize;
if let Some(cached_data) = Self::try_get_cached_recommended(user_id, count).await? {
return Ok(Some(cached_data));
}
let mut result;
{
let graph = get_neo4j_graph()?;
let query = queries::get::recommend_users(user_id, 30);
let graph = graph.lock().await;
result = graph.execute(query).await?;
}
let mut user_ids = Vec::new();
while let Some(row) = result.next().await? {
if let Some(user_id) = row.get::<Option<String>>("recommended_user_id")? {
user_ids.push(user_id);
}
}
if user_ids.is_empty() {
Ok(None)
} else {
Self::cache_recommended_users(user_id, &user_ids).await?;
if let Some(limit) = limit {
user_ids.truncate(limit);
};
Ok(Some(user_ids))
}
}
async fn try_get_cached_recommended(
user_id: &str,
count: isize,
) -> Result<Option<Vec<String>>, DynError> {
let key_parts = &["Cache", "Recommended", user_id];
Self::try_get_random_from_index_set(
key_parts,
count,
Some(CACHE_USER_RECOMMENDED_KEY_PARTS.join(":")),
)
.await
}
async fn cache_recommended_users(user_id: &str, user_ids: &[String]) -> Result<(), DynError> {
let values: Vec<&str> = user_ids.iter().map(|s| s.as_str()).collect();
Self::put_index_set(
&[user_id],
&values,
Some(CACHE_USER_RECOMMENDED_TTL),
Some(CACHE_USER_RECOMMENDED_KEY_PARTS.join(":")),
)
.await
}
async fn get_post_replies_ids(
post_id: Option<String>,
author_id: Option<String>,
) -> Result<Option<Vec<String>>, DynError> {
let post_id = post_id
.ok_or("Post ID should be provided for user streams with source 'post_replies'")?;
let author_id = author_id
.ok_or("Author ID should be provided for user streams with source 'post_replies'")?;
let key_parts = [
&POST_REPLIES_PER_POST_KEY_PARTS[..],
&[author_id.as_str(), post_id.as_str()],
]
.concat();
let replies = PostStream::try_from_index_sorted_set(
&key_parts,
None,
None,
None,
None,
SortOrder::Descending,
None,
)
.await?;
let unique_user_ids: HashSet<String> = if let Some(replies) = replies {
replies
.into_iter()
.filter_map(|reply| reply.0.split(':').next().map(|s| s.to_string()))
.collect()
} else {
return Ok(None);
};
Ok(Some(unique_user_ids.into_iter().collect()))
}
pub async fn get_user_list_from_source(
input: UserStreamInput,
) -> Result<Option<Vec<String>>, DynError> {
let UserStreamInput {
user_id,
skip,
limit,
source,
reach,
timeframe,
preview,
author_id,
post_id,
} = input;
let user_ids =
match source {
UserStreamSource::Followers => Followers::get_by_id(
user_id
.ok_or(
"User ID should be provided for user streams with source 'followers'"
.to_string(),
)?
.as_str(),
skip,
limit,
)
.await?
.map(|u| u.0),
UserStreamSource::Following => Following::get_by_id(
user_id
.ok_or(
"User ID should be provided for user streams with source 'following'"
.to_string(),
)?
.as_str(),
skip,
limit,
)
.await?
.map(|u| u.0),
UserStreamSource::Friends => Friends::get_by_id(
user_id
.ok_or(
"User ID should be provided for user streams with source 'friends'"
.to_string(),
)?
.as_str(),
skip,
limit,
)
.await?
.map(|u| u.0),
UserStreamSource::Muted => Muted::get_by_id(
user_id
.ok_or(
"User ID should be provided for user streams with source 'muted'"
.to_string(),
)?
.as_str(),
skip,
limit,
)
.await?
.map(|u| u.0),
UserStreamSource::MostFollowed => Self::try_from_index_sorted_set(
&USER_MOSTFOLLOWED_KEY_PARTS,
None,
None,
skip,
limit,
SortOrder::Descending,
None,
)
.await?
.map(|set| set.into_iter().map(|(user_id, _score)| user_id).collect()),
UserStreamSource::Influencers => Influencers::get_influencers(
user_id.as_deref(),
Some(reach.unwrap_or(StreamReach::Wot(3))),
skip.unwrap_or(0),
limit.unwrap_or(10).min(100),
timeframe.unwrap_or(Timeframe::AllTime),
preview.unwrap_or(false),
)
.await?
.map(|result| {
result
.iter()
.map(|(influencer_id, _)| influencer_id.clone())
.collect()
}),
UserStreamSource::Recommended => UserStream::get_recommended_ids(
user_id
.ok_or(
"User ID should be provided for user streams with source 'recommended'"
.to_string(),
)?
.as_str(),
limit,
)
.await?,
UserStreamSource::PostReplies => {
UserStream::get_post_replies_ids(post_id, author_id).await?
}
};
Ok(user_ids)
}
}