- [Rust docker build](https://shaneutt.com/blog/rust-fast-small-docker-image-builds/)
- [Zurb mentions](https://github.com/zurb/tribute)
- [TippyJS](https://github.com/atomiks/tippyjs)
+- [SQL function indexes](https://sorentwo.com/2013/12/30/let-postgres-do-the-work.html)
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
-use lemmy_db::user::User_;
+use lemmy_db::source::user::User_;
use lemmy_utils::settings::Settings;
use serde::{Deserialize, Serialize};
use actix_web::web::Data;
use lemmy_apub::{ApubLikeableType, ApubObjectType};
use lemmy_db::{
- comment::*,
comment_report::*,
comment_view::*,
- moderator::*,
- post::*,
- site_view::*,
- user::*,
+ source::{comment::*, moderator::*, post::*, user::*},
+ views::site_view::SiteView,
Crud,
Likeable,
ListingType,
// Don't do a downvote if site has downvotes disabled
if data.score == -1 {
- let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
- if !site.enable_downvotes {
+ let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
+ if !site_view.site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into());
}
}
use anyhow::Context;
use lemmy_apub::ActorType;
use lemmy_db::{
- comment::Comment,
comment_view::CommentQueryBuilder,
- community::*,
- community_view::*,
diesel_option_overwrite,
- moderator::*,
naive_now,
- post::Post,
- site::*,
- user_view::*,
+ source::{comment::Comment, community::*, moderator::*, post::Post, site::*},
+ views::{
+ community_follower_view::CommunityFollowerView,
+ community_moderator_view::CommunityModeratorView,
+ community_view::{CommunityQueryBuilder, CommunityView},
+ user_view::UserViewSafe,
+ },
ApubObject,
Bannable,
Crud,
.unwrap_or(1);
let res = GetCommunityResponse {
- community: community_view,
+ community_view,
moderators,
online,
};
})
.await??;
- Ok(CommunityResponse {
- community: community_view,
- })
+ Ok(CommunityResponse { community_view })
}
}
let edit_id = data.edit_id;
let mods: Vec<i32> = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, edit_id)
- .map(|v| v.into_iter().map(|m| m.user_id).collect())
+ .map(|v| v.into_iter().map(|m| m.moderator.id).collect())
})
.await??;
if !mods.contains(&user.id) {
})
.await??;
- let res = CommunityResponse {
- community: community_view,
- };
+ let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::EditCommunity);
})
.await??;
- let res = CommunityResponse {
- community: community_view,
- };
+ let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::DeleteCommunity);
})
.await??;
- let res = CommunityResponse {
- community: community_view,
- };
+ let res = CommunityResponse { community_view };
send_community_websocket(&res, context, websocket_id, UserOperation::RemoveCommunity);
let page = data.page;
let limit = data.limit;
let communities = blocking(context.pool(), move |conn| {
- CommunityQueryBuilder::create(conn)
+ CommunityQueryBuilder::create(conn, user_id)
.sort(&sort)
- .for_user(user_id)
.show_nsfw(show_nsfw)
.page(page)
.limit(limit)
// For now, just assume that remote follows are accepted.
// Otherwise, the subscribed will be null
if !community.local {
- community_view.subscribed = Some(data.follow);
+ community_view.subscribed = data.follow;
}
- Ok(CommunityResponse {
- community: community_view,
- })
+ Ok(CommunityResponse { community_view })
}
}
let user_id = data.user_id;
let user_view = blocking(context.pool(), move |conn| {
- UserView::get_user_secure(conn, user_id)
+ UserViewSafe::read(conn, user_id)
})
.await??;
let res = BanFromCommunityResponse {
- user: user_view,
+ user_view,
banned: data.ban,
};
})
.await??;
- let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
+ let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
let creator_index = admins
.iter()
- .position(|r| r.id == site_creator_id)
+ .position(|r| r.user.id == site_creator_id)
.context(location_info!())?;
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
// Make sure user is the creator, or an admin
- if user.id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user.id) {
+ if user.id != read_community.creator_id
+ && !admins.iter().map(|a| a.user.id).any(|x| x == user.id)
+ {
return Err(APIError::err("not_an_admin").into());
}
.await??;
let creator_index = community_mods
.iter()
- .position(|r| r.user_id == data.user_id)
+ .position(|r| r.moderator.id == data.user_id)
.context(location_info!())?;
let creator_user = community_mods.remove(creator_index);
community_mods.insert(0, creator_user);
// TODO: this should probably be a bulk operation
for cmod in &community_mods {
let community_moderator_form = CommunityModeratorForm {
- community_id: cmod.community_id,
- user_id: cmod.user_id,
+ community_id: cmod.community.id,
+ user_id: cmod.moderator.id,
};
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
// Return the jwt
Ok(GetCommunityResponse {
- community: community_view,
+ community_view,
moderators,
online: 0,
})
websocket_id: Option<ConnectionId>,
op: UserOperation,
) {
+ // TODO is there any way around this?
// Strip out the user id and subscribed when sending to others
- let mut res_sent = res.clone();
- res_sent.community.user_id = None;
- res_sent.community.subscribed = None;
+ // let mut res_sent = res.clone();
+ // res_sent.community_view.user_id = None;
+ // res_sent.community.subscribed = None;
context.chat_server().do_send(SendCommunityRoomMessage {
op,
- response: res_sent,
- community_id: res.community.id,
+ response: res.to_owned(),
+ community_id: res.community_view.community.id,
websocket_id,
});
}
use crate::claims::Claims;
use actix_web::{web, web::Data};
use lemmy_db::{
- community::{Community, CommunityModerator},
- community_view::CommunityUserBanView,
- post::Post,
- user::User_,
+ source::{
+ community::{Community, CommunityModerator},
+ post::Post,
+ user::User_,
+ },
+ views::community_user_ban_view::CommunityUserBanView,
Crud,
DbPool,
};
use lemmy_apub::{ApubLikeableType, ApubObjectType};
use lemmy_db::{
comment_view::*,
- community_view::*,
- moderator::*,
naive_now,
- post::*,
post_report::*,
- post_view::*,
- site_view::*,
+ source::{moderator::*, post::*},
+ views::{
+ community_moderator_view::CommunityModeratorView,
+ community_view::CommunityView,
+ post_view::{PostQueryBuilder, PostView},
+ site_view::SiteView,
+ },
Crud,
Likeable,
ListingType,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
};
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePost,
})
.await??;
- let community_id = post_view.community_id;
+ let community_id = post_view.community.id;
let community = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, user_id)
})
.await??;
- let community_id = post_view.community_id;
+ let community_id = post_view.community.id;
let moderators = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_community(conn, community_id)
})
// Return the jwt
Ok(GetPostResponse {
- post: post_view,
+ post_view,
comments,
community,
moderators,
let community_id = data.community_id;
let community_name = data.community_name.to_owned();
let posts = match blocking(context.pool(), move |conn| {
- PostQueryBuilder::create(conn)
+ PostQueryBuilder::create(conn, user_id)
.listing_type(&type_)
.sort(&sort)
.show_nsfw(show_nsfw)
.for_community_id(community_id)
.for_community_name(community_name)
- .my_user_id(user_id)
.page(page)
.limit(limit)
.list()
// Don't do a downvote if site has downvotes disabled
if data.score == -1 {
- let site = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
- if !site.enable_downvotes {
+ let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
+ if !site_view.site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into());
}
}
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
};
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::DeletePost,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::RemovePost,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::LockPost,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::StickyPost,
})
.await??;
- Ok(PostResponse { post: post_view })
+ Ok(PostResponse { post_view })
}
}
let user_id = user.id;
let post_id = data.post_id;
- let post = blocking(context.pool(), move |conn| {
+ let post_view = blocking(context.pool(), move |conn| {
PostView::read(&conn, post_id, None)
})
.await??;
- check_community_ban(user_id, post.community_id, context.pool()).await?;
+ check_community_ban(user_id, post_view.community.id, context.pool()).await?;
let report_form = PostReportForm {
creator_id: user_id,
post_id,
- original_post_name: post.name,
- original_post_url: post.url,
- original_post_body: post.body,
+ original_post_name: post_view.post.name,
+ original_post_url: post_view.post.url,
+ original_post_body: post_view.post.body,
reason: data.reason.to_owned(),
};
context.chat_server().do_send(SendModRoomMessage {
op: UserOperation::CreatePostReport,
response: report,
- community_id: post.community_id,
+ community_id: post_view.community.id,
websocket_id,
});
use anyhow::Context;
use lemmy_apub::fetcher::search_by_apub_id;
use lemmy_db::{
- category::*,
+ aggregates::site_aggregates::SiteAggregates,
comment_view::*,
- community_view::*,
diesel_option_overwrite,
- moderator::*,
moderator_views::*,
naive_now,
- post_view::*,
- site::*,
- site_view::*,
- user_view::*,
+ source::{category::*, moderator::*, site::*},
+ views::{
+ community_view::CommunityQueryBuilder,
+ post_view::PostQueryBuilder,
+ site_view::SiteView,
+ user_view::{UserQueryBuilder, UserViewSafe},
+ },
Crud,
SearchType,
SortType,
None
};
- let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
+ let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
// Make sure the site creator is the top admin
if let Some(site_view) = site_view.to_owned() {
- let site_creator_id = site_view.creator_id;
+ let site_creator_id = site_view.creator.id;
// TODO investigate why this is sometimes coming back null
// Maybe user_.admin isn't being set to true?
- if let Some(creator_index) = admins.iter().position(|r| r.id == site_creator_id) {
+ if let Some(creator_index) = admins.iter().position(|r| r.user.id == site_creator_id) {
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
}
}
- let banned = blocking(context.pool(), move |conn| UserView::banned(conn)).await??;
+ let banned = blocking(context.pool(), move |conn| UserViewSafe::banned(conn)).await??;
let online = context
.chat_server()
u
});
+ let counts = blocking(context.pool(), move |conn| SiteAggregates::read(conn)).await??;
+
Ok(GetSiteResponse {
site: site_view,
admins,
version: version::VERSION.to_string(),
my_user,
federated_instances: linked_instances(context.pool()).await?,
+ counts,
})
}
}
match type_ {
SearchType::Posts => {
posts = blocking(context.pool(), move |conn| {
- PostQueryBuilder::create(conn)
+ PostQueryBuilder::create(conn, user_id)
.sort(&sort)
.show_nsfw(true)
.for_community_id(community_id)
.for_community_name(community_name)
.search_term(q)
- .my_user_id(user_id)
.page(page)
.limit(limit)
.list()
}
SearchType::Communities => {
communities = blocking(context.pool(), move |conn| {
- CommunityQueryBuilder::create(conn)
+ CommunityQueryBuilder::create(conn, None)
.sort(&sort)
.search_term(q)
.page(page)
}
SearchType::All => {
posts = blocking(context.pool(), move |conn| {
- PostQueryBuilder::create(conn)
+ PostQueryBuilder::create(conn, user_id)
.sort(&sort)
.show_nsfw(true)
.for_community_id(community_id)
.for_community_name(community_name)
.search_term(q)
- .my_user_id(user_id)
.page(page)
.limit(limit)
.list()
let sort = SortType::from_str(&data.sort)?;
communities = blocking(context.pool(), move |conn| {
- CommunityQueryBuilder::create(conn)
+ CommunityQueryBuilder::create(conn, None)
.sort(&sort)
.search_term(q)
.page(page)
}
SearchType::Url => {
posts = blocking(context.pool(), move |conn| {
- PostQueryBuilder::create(conn)
+ PostQueryBuilder::create(conn, None)
.sort(&sort)
.show_nsfw(true)
.for_community_id(community_id)
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
- let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
+ let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
let creator_index = admins
.iter()
- .position(|r| r.id == site_view.creator_id)
+ .position(|r| r.user.id == site_view.creator.id)
.context(location_info!())?;
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
- let banned = blocking(context.pool(), move |conn| UserView::banned(conn)).await??;
+ let banned = blocking(context.pool(), move |conn| UserViewSafe::banned(conn)).await??;
+
+ let counts = blocking(context.pool(), move |conn| SiteAggregates::read(conn)).await??;
Ok(GetSiteResponse {
site: Some(site_view),
version: version::VERSION.to_string(),
my_user: Some(user),
federated_instances: linked_instances(context.pool()).await?,
+ counts,
})
}
}
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
// Only let admins read this
- let admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
- let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
+ let admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
+ let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.user.id).collect();
if !admin_ids.contains(&user.id) {
return Err(APIError::err("not_an_admin").into());
use chrono::Duration;
use lemmy_apub::ApubObjectType;
use lemmy_db::{
- comment::*,
comment_report::CommentReportView,
comment_view::*,
- community::*,
- community_view::*,
diesel_option_overwrite,
- moderator::*,
naive_now,
- password_reset_request::*,
- post::*,
post_report::PostReportView,
- post_view::*,
- private_message::*,
private_message_view::*,
- site::*,
- site_view::*,
- user::*,
- user_mention::*,
+ source::{
+ comment::*,
+ community::*,
+ moderator::*,
+ password_reset_request::*,
+ post::*,
+ private_message::*,
+ site::*,
+ user::*,
+ user_mention::*,
+ },
user_mention_view::*,
- user_view::*,
+ views::{
+ community_follower_view::CommunityFollowerView,
+ community_moderator_view::CommunityModeratorView,
+ post_view::PostQueryBuilder,
+ site_view::SiteView,
+ user_view::{UserViewDangerous, UserViewSafe},
+ },
Crud,
Followable,
Joinable,
let data: &Register = &self;
// Make sure site has open registration
- if let Ok(site) = blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
- let site: SiteView = site;
- if !site.open_registration {
+ if let Ok(site_view) = blocking(context.pool(), move |conn| SiteView::read(conn)).await? {
+ if !site_view.site.open_registration {
return Err(APIError::err("registration_closed").into());
}
}
// Make sure there are no admins
let any_admins = blocking(context.pool(), move |conn| {
- UserView::admins(conn).map(|a| a.is_empty())
+ UserViewSafe::admins(conn).map(|a| a.is_empty())
})
.await??;
if data.admin && !any_admins {
};
let user_id = user.map(|u| u.id);
- let user_fun = move |conn: &'_ _| {
- match user_id {
- // if there's a logged in user and it's the same id as the user whose details are being
- // requested we need to use get_user_dangerous so it returns their email or other sensitive
- // data hidden when viewing users other than yourself
- Some(auth_user_id) => {
- if user_details_id == auth_user_id {
- UserView::get_user_dangerous(conn, auth_user_id)
- } else {
- UserView::get_user_secure(conn, user_details_id)
- }
- }
- None => UserView::get_user_secure(conn, user_details_id),
+
+ let (user_view, user_dangerous) = if let Some(auth_user_id) = user_id {
+ if user_details_id == auth_user_id {
+ (
+ None,
+ Some(
+ blocking(context.pool(), move |conn| {
+ UserViewDangerous::read(conn, auth_user_id)
+ })
+ .await??,
+ ),
+ )
+ } else {
+ (
+ Some(
+ blocking(context.pool(), move |conn| {
+ UserViewSafe::read(conn, user_details_id)
+ })
+ .await??,
+ ),
+ None,
+ )
}
+ } else {
+ (
+ Some(
+ blocking(context.pool(), move |conn| {
+ UserViewSafe::read(conn, user_details_id)
+ })
+ .await??,
+ ),
+ None,
+ )
};
- let user_view = blocking(context.pool(), user_fun).await??;
-
let page = data.page;
let limit = data.limit;
let saved_only = data.saved_only;
let community_id = data.community_id;
let (posts, comments) = blocking(context.pool(), move |conn| {
- let mut posts_query = PostQueryBuilder::create(conn)
+ let mut posts_query = PostQueryBuilder::create(conn, user_id)
.sort(&sort)
.show_nsfw(show_nsfw)
.saved_only(saved_only)
.for_community_id(community_id)
- .my_user_id(user_id)
.page(page)
.limit(limit);
// Return the jwt
Ok(GetUserDetailsResponse {
+ // TODO need to figure out dangerous user view here
user: user_view,
+ user_dangerous,
follows,
moderates,
comments,
})
.await??;
- let mut admins = blocking(context.pool(), move |conn| UserView::admins(conn)).await??;
+ let mut admins = blocking(context.pool(), move |conn| UserViewSafe::admins(conn)).await??;
let creator_index = admins
.iter()
- .position(|r| r.id == site_creator_id)
+ .position(|r| r.user.id == site_creator_id)
.context(location_info!())?;
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
let user_id = data.user_id;
let user_view = blocking(context.pool(), move |conn| {
- UserView::get_user_secure(conn, user_id)
+ UserViewSafe::read(conn, user_id)
})
.await??;
};
use anyhow::Context;
use lemmy_db::{
- comment::{Comment, CommentLike, CommentLikeForm},
comment_view::CommentView,
- post::Post,
+ source::{
+ comment::{Comment, CommentLike, CommentLikeForm},
+ post::Post,
+ },
Likeable,
};
use lemmy_structs::{blocking, comment::CommentResponse, send_local_notifs};
use crate::activities::receive::get_actor_as_user;
use activitystreams::activity::{Dislike, Like};
use lemmy_db::{
- comment::{Comment, CommentLike},
comment_view::CommentView,
+ source::comment::{Comment, CommentLike},
Likeable,
};
use lemmy_structs::{blocking, comment::CommentResponse};
base::{AnyBase, ExtendsExt},
};
use anyhow::Context;
-use lemmy_db::{community::Community, community_view::CommunityView, ApubObject};
+use lemmy_db::{source::community::Community, views::community_view::CommunityView, ApubObject};
use lemmy_structs::{blocking, community::CommunityResponse};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendCommunityRoomMessage, LemmyContext, UserOperation};
let community_id = deleted_community.id;
let res = CommunityResponse {
- community: blocking(context.pool(), move |conn| {
+ community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
- let community_id = res.community.id;
+ let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
let community_id = removed_community.id;
let res = CommunityResponse {
- community: blocking(context.pool(), move |conn| {
+ community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
- let community_id = res.community.id;
+ let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
let community_id = deleted_community.id;
let res = CommunityResponse {
- community: blocking(context.pool(), move |conn| {
+ community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
- let community_id = res.community.id;
+ let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res,
let community_id = removed_community.id;
let res = CommunityResponse {
- community: blocking(context.pool(), move |conn| {
+ community_view: blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, None)
})
.await??,
};
- let community_id = res.community.id;
+ let community_id = res.community_view.community.id;
context.chat_server().do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
error::DomainError,
};
use anyhow::{anyhow, Context};
-use lemmy_db::user::User_;
+use lemmy_db::source::user::User_;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use log::debug;
};
use anyhow::Context;
use lemmy_db::{
- post::{Post, PostLike, PostLikeForm},
- post_view::PostView,
+ source::post::{Post, PostLike, PostLikeForm},
+ views::post_view::PostView,
Likeable,
};
use lemmy_structs::{blocking, post::PostResponse};
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePost,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
use crate::activities::receive::get_actor_as_user;
use activitystreams::activity::{Dislike, Like};
use lemmy_db::{
- post::{Post, PostLike},
- post_view::PostView,
+ source::post::{Post, PostLike},
+ views::post_view::PostView,
Likeable,
};
use lemmy_structs::{blocking, post::PostResponse};
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::CreatePostLike,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
post: res,
})
.await??;
- let res = PostResponse { post: post_view };
+ let res = PostResponse { post_view };
context.chat_server().do_send(SendPost {
op: UserOperation::EditPost,
public,
};
use anyhow::{anyhow, Context};
-use lemmy_db::{private_message::PrivateMessage, private_message_view::PrivateMessageView};
+use lemmy_db::{private_message_view::PrivateMessageView, source::private_message::PrivateMessage};
use lemmy_structs::{blocking, user::PrivateMessageResponse};
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::{messages::SendUserRoomMessage, LemmyContext, UserOperation};
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
- let to_and_cc = get_activity_to_and_cc(activity)?;
+ let to_and_cc = get_activity_to_and_cc(activity);
if to_and_cc.len() != 1 {
return Err(anyhow!("Private message can only be addressed to one user").into());
}
};
use anyhow::anyhow;
use itertools::Itertools;
-use lemmy_db::{comment::Comment, community::Community, post::Post, user::User_, Crud, DbPool};
+use lemmy_db::{
+ source::{comment::Comment, community::Community, post::Post, user::User_},
+ Crud,
+ DbPool,
+};
use lemmy_structs::{blocking, WebFingerResponse};
use lemmy_utils::{
request::{retry, RecvError},
};
use anyhow::Context;
use itertools::Itertools;
-use lemmy_db::{community::Community, community_view::CommunityFollowerView, DbPool};
+use lemmy_db::{
+ source::community::Community,
+ views::community_follower_view::CommunityFollowerView,
+ DbPool,
+};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
.await??;
let inboxes = inboxes
.into_iter()
- .filter(|i| !i.user_local)
+ .filter(|i| !i.follower.local)
.map(|u| -> Result<Url, LemmyError> {
- let url = Url::parse(&u.user_actor_id)?;
+ let url = Url::parse(&u.follower.actor_id)?;
let domain = url.domain().context(location_info!())?;
let port = if let Some(port) = url.port() {
format!(":{}", port)
prelude::*,
public,
};
-use lemmy_db::{community::Community, post::Post, user::User_, Crud};
+use lemmy_db::{
+ source::{community::Community, post::Post, user::User_},
+ Crud,
+};
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
},
prelude::*,
};
-use lemmy_db::{private_message::PrivateMessage, user::User_, Crud};
+use lemmy_db::{
+ source::{private_message::PrivateMessage, user::User_},
+ Crud,
+};
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
object::ObjectExt,
};
use lemmy_db::{
- community::{Community, CommunityFollower, CommunityFollowerForm},
- user::User_,
+ source::{
+ community::{Community, CommunityFollower, CommunityFollowerForm},
+ user::User_,
+ },
ApubObject,
DbPool,
Followable,
WorkerConfig,
};
use itertools::Itertools;
-use lemmy_db::{community::Community, user::User_, DbPool};
+use lemmy_db::{
+ source::{community::Community, user::User_},
+ DbPool,
+};
use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
use log::{debug, warn};
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use diesel::PgConnection;
-use lemmy_db::{category::Category, Crud};
+use lemmy_db::{source::category::Category, Crud};
use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
use chrono::NaiveDateTime;
use diesel::result::Error::NotFound;
use lemmy_db::{
- comment::Comment,
comment_view::CommentView,
- community::{Community, CommunityModerator, CommunityModeratorForm},
- community_view::CommunityView,
naive_now,
- post::Post,
- post_view::PostView,
- user::User_,
- user_view::UserView,
+ source::{
+ comment::Comment,
+ community::{Community, CommunityModerator, CommunityModeratorForm},
+ post::Post,
+ user::User_,
+ },
+ views::{community_view::CommunityView, post_view::PostView, user_view::UserViewSafe},
ApubObject,
Joinable,
SearchType,
response.users = vec![
blocking(context.pool(), move |conn| {
- UserView::get_user_secure(conn, user.id)
+ UserViewSafe::read(conn, user.id)
})
.await??,
];
};
use actix_web::{body::Body, web, web::Path, HttpResponse};
use diesel::result::Error::NotFound;
-use lemmy_db::{comment::Comment, Crud};
+use lemmy_db::{source::comment::Comment, Crud};
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
collection::{CollectionExt, OrderedCollection, UnorderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
-use lemmy_db::{community::Community, community_view::CommunityFollowerView, post::Post};
+use lemmy_db::{
+ source::{community::Community, post::Post},
+ views::community_follower_view::CommunityFollowerView,
+};
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use crate::APUB_JSON_CONTENT_TYPE;
use actix_web::{body::Body, web, HttpResponse};
-use lemmy_db::activity::Activity;
+use lemmy_db::source::activity::Activity;
use lemmy_structs::blocking;
use lemmy_utils::{settings::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
};
use actix_web::{body::Body, web, HttpResponse};
use diesel::result::Error::NotFound;
-use lemmy_db::post::Post;
+use lemmy_db::source::post::Post;
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
collection::{CollectionExt, OrderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
-use lemmy_db::user::User_;
+use lemmy_db::source::user::User_;
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::{anyhow, Context};
use lemmy_db::{
- community::{Community, CommunityFollower, CommunityFollowerForm},
- community_view::CommunityUserBanView,
- user::User_,
+ source::{
+ community::{Community, CommunityFollower, CommunityFollowerForm},
+ user::User_,
+ },
+ views::community_user_ban_view::CommunityUserBanView,
ApubObject,
DbPool,
Followable,
Community::read_from_name(&conn, &path)
})
.await??;
- let to_and_cc = get_activity_to_and_cc(&activity)?;
+ let to_and_cc = get_activity_to_and_cc(&activity);
if !to_and_cc.contains(&&community.actor_id()?) {
return Err(anyhow!("Activity delivered to wrong community").into());
}
};
use actix_web::HttpRequest;
use anyhow::{anyhow, Context};
-use lemmy_db::{activity::Activity, community::Community, user::User_, ApubObject, DbPool};
+use lemmy_db::{
+ source::{activity::Activity, community::Community, user::User_},
+ ApubObject,
+ DbPool,
+};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
}
}
-pub(crate) fn get_activity_to_and_cc<T, Kind>(activity: &T) -> Result<Vec<Url>, LemmyError>
+pub(crate) fn get_activity_to_and_cc<T, Kind>(activity: &T) -> Vec<Url>
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
.collect();
to_and_cc.append(&mut cc);
}
- Ok(to_and_cc)
+ to_and_cc
}
pub(crate) fn is_addressed_to_public<T, Kind>(activity: &T) -> Result<(), LemmyError>
where
T: AsBase<Kind> + AsObject<Kind> + ActorAndObjectRefExt,
{
- let to_and_cc = get_activity_to_and_cc(activity)?;
+ let to_and_cc = get_activity_to_and_cc(activity);
if to_and_cc.contains(&public()) {
Ok(())
} else {
};
use anyhow::Context;
use diesel::result::Error::NotFound;
-use lemmy_db::{comment::Comment, post::Post, site::Site, ApubObject, Crud};
+use lemmy_db::{
+ source::{comment::Comment, post::Post, site::Site},
+ ApubObject,
+ Crud,
+};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use activitystreams::{activity::ActorAndObject, prelude::*};
use actix_web::{web, HttpRequest, HttpResponse};
use anyhow::Context;
-use lemmy_db::{community::Community, ApubObject, DbPool};
+use lemmy_db::{source::community::Community, ApubObject, DbPool};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
let activity_any_base = activity.clone().into_any_base()?;
let mut res: Option<HttpResponse> = None;
- let to_and_cc = get_activity_to_and_cc(&activity)?;
+ let to_and_cc = get_activity_to_and_cc(&activity);
// Handle community first, so in case the sender is banned by the community, it will error out.
// If we handled the user receive first, the activity would be inserted to the database before the
// community could check for bans.
use anyhow::{anyhow, Context};
use diesel::NotFound;
use lemmy_db::{
- community::{Community, CommunityFollower},
- private_message::PrivateMessage,
- user::User_,
+ source::{
+ community::{Community, CommunityFollower},
+ private_message::PrivateMessage,
+ user::User_,
+ },
ApubObject,
Followable,
};
User_::read_from_name(&conn, &username)
})
.await??;
- let to_and_cc = get_activity_to_and_cc(&activity)?;
+ let to_and_cc = get_activity_to_and_cc(&activity);
// TODO: we should also accept activities that are sent to community followers
if !to_and_cc.contains(&&user.actor_id()?) {
return Err(anyhow!("Activity delivered to wrong user").into());
context: &LemmyContext,
activity: &UserAcceptedActivities,
) -> Result<(), LemmyError> {
- let to_and_cc = get_activity_to_and_cc(activity)?;
+ let to_and_cc = get_activity_to_and_cc(activity);
// Check if it is addressed directly to any local user
if is_addressed_to_local_user(&to_and_cc, context.pool()).await? {
return Ok(());
};
use activitystreams_ext::{Ext1, Ext2};
use anyhow::{anyhow, Context};
-use lemmy_db::{activity::Activity, user::User_, DbPool};
+use lemmy_db::{
+ source::{activity::Activity, user::User_},
+ DbPool,
+};
use lemmy_structs::blocking;
use lemmy_utils::{location_info, settings::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
};
use anyhow::{anyhow, Context};
use lemmy_db::{
- comment::{Comment, CommentForm},
- community::Community,
- post::Post,
- user::User_,
+ source::{
+ comment::{Comment, CommentForm},
+ community::Community,
+ post::Post,
+ user::User_,
+ },
Crud,
DbPool,
};
use activitystreams_ext::Ext2;
use anyhow::Context;
use lemmy_db::{
- community::{Community, CommunityForm},
- community_view::CommunityModeratorView,
naive_now,
+ source::community::{Community, CommunityForm},
+ views::community_moderator_view::CommunityModeratorView,
DbPool,
};
use lemmy_structs::blocking;
CommunityModeratorView::for_community(&conn, id)
})
.await??;
- let moderators: Vec<String> = moderators.into_iter().map(|m| m.user_actor_id).collect();
+ let moderators: Vec<String> = moderators
+ .into_iter()
+ .map(|m| m.moderator.actor_id)
+ .collect();
let mut group = ApObject::new(Group::new());
group
use activitystreams_ext::Ext1;
use anyhow::Context;
use lemmy_db::{
- community::Community,
- post::{Post, PostForm},
- user::User_,
+ source::{
+ community::Community,
+ post::{Post, PostForm},
+ user::User_,
+ },
Crud,
DbPool,
};
};
use anyhow::Context;
use lemmy_db::{
- private_message::{PrivateMessage, PrivateMessageForm},
- user::User_,
+ source::{
+ private_message::{PrivateMessage, PrivateMessageForm},
+ user::User_,
+ },
Crud,
DbPool,
};
use anyhow::Context;
use lemmy_db::{
naive_now,
- user::{UserForm, User_},
+ source::user::{UserForm, User_},
ApubObject,
DbPool,
};
--- /dev/null
+use crate::schema::community_aggregates;
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
+#[table_name = "community_aggregates"]
+pub struct CommunityAggregates {
+ pub id: i32,
+ pub community_id: i32,
+ pub subscribers: i64,
+ pub posts: i64,
+ pub comments: i64,
+}
+
+impl CommunityAggregates {
+ pub fn read(conn: &PgConnection, community_id: i32) -> Result<Self, Error> {
+ community_aggregates::table
+ .filter(community_aggregates::community_id.eq(community_id))
+ .first::<Self>(conn)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{
+ aggregates::community_aggregates::CommunityAggregates,
+ source::{
+ comment::{Comment, CommentForm},
+ community::{Community, CommunityFollower, CommunityFollowerForm, CommunityForm},
+ post::{Post, PostForm},
+ user::{UserForm, User_},
+ },
+ tests::establish_unpooled_connection,
+ Crud,
+ Followable,
+ ListingType,
+ SortType,
+ };
+
+ #[test]
+ fn test_crud() {
+ let conn = establish_unpooled_connection();
+
+ let new_user = UserForm {
+ name: "thommy_community_agg".into(),
+ preferred_username: None,
+ password_encrypted: "nope".into(),
+ email: None,
+ matrix_user_id: None,
+ avatar: None,
+ banner: None,
+ admin: false,
+ banned: Some(false),
+ published: None,
+ updated: None,
+ show_nsfw: false,
+ theme: "browser".into(),
+ default_sort_type: SortType::Hot as i16,
+ default_listing_type: ListingType::Subscribed as i16,
+ lang: "browser".into(),
+ show_avatars: true,
+ send_notifications_to_email: false,
+ actor_id: None,
+ bio: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ };
+
+ let inserted_user = User_::create(&conn, &new_user).unwrap();
+
+ let another_user = UserForm {
+ name: "jerry_community_agg".into(),
+ preferred_username: None,
+ password_encrypted: "nope".into(),
+ email: None,
+ matrix_user_id: None,
+ avatar: None,
+ banner: None,
+ admin: false,
+ banned: Some(false),
+ published: None,
+ updated: None,
+ show_nsfw: false,
+ theme: "browser".into(),
+ default_sort_type: SortType::Hot as i16,
+ default_listing_type: ListingType::Subscribed as i16,
+ lang: "browser".into(),
+ show_avatars: true,
+ send_notifications_to_email: false,
+ actor_id: None,
+ bio: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ };
+
+ let another_inserted_user = User_::create(&conn, &another_user).unwrap();
+
+ let new_community = CommunityForm {
+ name: "TIL_community_agg".into(),
+ creator_id: inserted_user.id,
+ title: "nada".to_owned(),
+ description: None,
+ category_id: 1,
+ nsfw: false,
+ removed: None,
+ deleted: None,
+ updated: None,
+ actor_id: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ published: None,
+ icon: None,
+ banner: None,
+ };
+
+ let inserted_community = Community::create(&conn, &new_community).unwrap();
+
+ let another_community = CommunityForm {
+ name: "TIL_community_agg_2".into(),
+ creator_id: inserted_user.id,
+ title: "nada".to_owned(),
+ description: None,
+ category_id: 1,
+ nsfw: false,
+ removed: None,
+ deleted: None,
+ updated: None,
+ actor_id: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ published: None,
+ icon: None,
+ banner: None,
+ };
+
+ let another_inserted_community = Community::create(&conn, &another_community).unwrap();
+
+ let first_user_follow = CommunityFollowerForm {
+ community_id: inserted_community.id,
+ user_id: inserted_user.id,
+ pending: false,
+ };
+
+ CommunityFollower::follow(&conn, &first_user_follow).unwrap();
+
+ let second_user_follow = CommunityFollowerForm {
+ community_id: inserted_community.id,
+ user_id: another_inserted_user.id,
+ pending: false,
+ };
+
+ CommunityFollower::follow(&conn, &second_user_follow).unwrap();
+
+ let another_community_follow = CommunityFollowerForm {
+ community_id: another_inserted_community.id,
+ user_id: inserted_user.id,
+ pending: false,
+ };
+
+ CommunityFollower::follow(&conn, &another_community_follow).unwrap();
+
+ let new_post = PostForm {
+ name: "A test post".into(),
+ url: None,
+ body: None,
+ creator_id: inserted_user.id,
+ community_id: inserted_community.id,
+ removed: None,
+ deleted: None,
+ locked: None,
+ stickied: None,
+ nsfw: false,
+ updated: None,
+ embed_title: None,
+ embed_description: None,
+ embed_html: None,
+ thumbnail_url: None,
+ ap_id: None,
+ local: true,
+ published: None,
+ };
+
+ let inserted_post = Post::create(&conn, &new_post).unwrap();
+
+ let comment_form = CommentForm {
+ content: "A test comment".into(),
+ creator_id: inserted_user.id,
+ post_id: inserted_post.id,
+ removed: None,
+ deleted: None,
+ read: None,
+ parent_id: None,
+ published: None,
+ updated: None,
+ ap_id: None,
+ local: true,
+ };
+
+ let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
+
+ let child_comment_form = CommentForm {
+ content: "A test comment".into(),
+ creator_id: inserted_user.id,
+ post_id: inserted_post.id,
+ removed: None,
+ deleted: None,
+ read: None,
+ parent_id: Some(inserted_comment.id),
+ published: None,
+ updated: None,
+ ap_id: None,
+ local: true,
+ };
+
+ let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
+
+ let community_aggregates_before_delete =
+ CommunityAggregates::read(&conn, inserted_community.id).unwrap();
+
+ assert_eq!(2, community_aggregates_before_delete.subscribers);
+ assert_eq!(1, community_aggregates_before_delete.posts);
+ assert_eq!(2, community_aggregates_before_delete.comments);
+
+ // Test the other community
+ let another_community_aggs =
+ CommunityAggregates::read(&conn, another_inserted_community.id).unwrap();
+ assert_eq!(1, another_community_aggs.subscribers);
+ assert_eq!(0, another_community_aggs.posts);
+ assert_eq!(0, another_community_aggs.comments);
+
+ // Unfollow test
+ CommunityFollower::unfollow(&conn, &second_user_follow).unwrap();
+ let after_unfollow = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
+ assert_eq!(1, after_unfollow.subscribers);
+
+ // Follow again just for the later tests
+ CommunityFollower::follow(&conn, &second_user_follow).unwrap();
+ let after_follow_again = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
+ assert_eq!(2, after_follow_again.subscribers);
+
+ // Remove a parent comment (the comment count should also be 0)
+ Post::delete(&conn, inserted_post.id).unwrap();
+ let after_parent_post_delete = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
+ assert_eq!(0, after_parent_post_delete.comments);
+ assert_eq!(0, after_parent_post_delete.posts);
+
+ // Remove the 2nd user
+ User_::delete(&conn, another_inserted_user.id).unwrap();
+ let after_user_delete = CommunityAggregates::read(&conn, inserted_community.id).unwrap();
+ assert_eq!(1, after_user_delete.subscribers);
+
+ // This should delete all the associated rows, and fire triggers
+ let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
+ assert_eq!(1, user_num_deleted);
+
+ // Should be none found, since the creator was deleted
+ let after_delete = CommunityAggregates::read(&conn, inserted_community.id);
+ assert!(after_delete.is_err());
+ }
+}
--- /dev/null
+pub mod community_aggregates;
+pub mod post_aggregates;
+pub mod site_aggregates;
+pub mod user_aggregates;
--- /dev/null
+use crate::schema::post_aggregates;
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
+#[table_name = "post_aggregates"]
+pub struct PostAggregates {
+ pub id: i32,
+ pub post_id: i32,
+ pub comments: i64,
+ pub score: i64,
+ pub upvotes: i64,
+ pub downvotes: i64,
+ pub newest_comment_time: chrono::NaiveDateTime,
+}
+
+impl PostAggregates {
+ pub fn read(conn: &PgConnection, post_id: i32) -> Result<Self, Error> {
+ post_aggregates::table
+ .filter(post_aggregates::post_id.eq(post_id))
+ .first::<Self>(conn)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{
+ aggregates::post_aggregates::PostAggregates,
+ source::{
+ comment::{Comment, CommentForm},
+ community::{Community, CommunityForm},
+ post::{Post, PostForm, PostLike, PostLikeForm},
+ user::{UserForm, User_},
+ },
+ tests::establish_unpooled_connection,
+ Crud,
+ Likeable,
+ ListingType,
+ SortType,
+ };
+
+ #[test]
+ fn test_crud() {
+ let conn = establish_unpooled_connection();
+
+ let new_user = UserForm {
+ name: "thommy_community_agg".into(),
+ preferred_username: None,
+ password_encrypted: "nope".into(),
+ email: None,
+ matrix_user_id: None,
+ avatar: None,
+ banner: None,
+ admin: false,
+ banned: Some(false),
+ published: None,
+ updated: None,
+ show_nsfw: false,
+ theme: "browser".into(),
+ default_sort_type: SortType::Hot as i16,
+ default_listing_type: ListingType::Subscribed as i16,
+ lang: "browser".into(),
+ show_avatars: true,
+ send_notifications_to_email: false,
+ actor_id: None,
+ bio: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ };
+
+ let inserted_user = User_::create(&conn, &new_user).unwrap();
+
+ let another_user = UserForm {
+ name: "jerry_community_agg".into(),
+ preferred_username: None,
+ password_encrypted: "nope".into(),
+ email: None,
+ matrix_user_id: None,
+ avatar: None,
+ banner: None,
+ admin: false,
+ banned: Some(false),
+ published: None,
+ updated: None,
+ show_nsfw: false,
+ theme: "browser".into(),
+ default_sort_type: SortType::Hot as i16,
+ default_listing_type: ListingType::Subscribed as i16,
+ lang: "browser".into(),
+ show_avatars: true,
+ send_notifications_to_email: false,
+ actor_id: None,
+ bio: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ };
+
+ let another_inserted_user = User_::create(&conn, &another_user).unwrap();
+
+ let new_community = CommunityForm {
+ name: "TIL_community_agg".into(),
+ creator_id: inserted_user.id,
+ title: "nada".to_owned(),
+ description: None,
+ category_id: 1,
+ nsfw: false,
+ removed: None,
+ deleted: None,
+ updated: None,
+ actor_id: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ published: None,
+ icon: None,
+ banner: None,
+ };
+
+ let inserted_community = Community::create(&conn, &new_community).unwrap();
+
+ let new_post = PostForm {
+ name: "A test post".into(),
+ url: None,
+ body: None,
+ creator_id: inserted_user.id,
+ community_id: inserted_community.id,
+ removed: None,
+ deleted: None,
+ locked: None,
+ stickied: None,
+ nsfw: false,
+ updated: None,
+ embed_title: None,
+ embed_description: None,
+ embed_html: None,
+ thumbnail_url: None,
+ ap_id: None,
+ local: true,
+ published: None,
+ };
+
+ let inserted_post = Post::create(&conn, &new_post).unwrap();
+
+ let comment_form = CommentForm {
+ content: "A test comment".into(),
+ creator_id: inserted_user.id,
+ post_id: inserted_post.id,
+ removed: None,
+ deleted: None,
+ read: None,
+ parent_id: None,
+ published: None,
+ updated: None,
+ ap_id: None,
+ local: true,
+ };
+
+ let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
+
+ let child_comment_form = CommentForm {
+ content: "A test comment".into(),
+ creator_id: inserted_user.id,
+ post_id: inserted_post.id,
+ removed: None,
+ deleted: None,
+ read: None,
+ parent_id: Some(inserted_comment.id),
+ published: None,
+ updated: None,
+ ap_id: None,
+ local: true,
+ };
+
+ let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
+
+ let post_like = PostLikeForm {
+ post_id: inserted_post.id,
+ user_id: inserted_user.id,
+ score: 1,
+ };
+
+ PostLike::like(&conn, &post_like).unwrap();
+
+ let post_aggs_before_delete = PostAggregates::read(&conn, inserted_post.id).unwrap();
+
+ assert_eq!(2, post_aggs_before_delete.comments);
+ assert_eq!(1, post_aggs_before_delete.score);
+ assert_eq!(1, post_aggs_before_delete.upvotes);
+ assert_eq!(0, post_aggs_before_delete.downvotes);
+
+ // Add a post dislike from the other user
+ let post_dislike = PostLikeForm {
+ post_id: inserted_post.id,
+ user_id: another_inserted_user.id,
+ score: -1,
+ };
+
+ PostLike::like(&conn, &post_dislike).unwrap();
+
+ let post_aggs_after_dislike = PostAggregates::read(&conn, inserted_post.id).unwrap();
+
+ assert_eq!(2, post_aggs_after_dislike.comments);
+ assert_eq!(0, post_aggs_after_dislike.score);
+ assert_eq!(1, post_aggs_after_dislike.upvotes);
+ assert_eq!(1, post_aggs_after_dislike.downvotes);
+
+ // Remove the parent comment
+ Comment::delete(&conn, inserted_comment.id).unwrap();
+ let after_comment_delete = PostAggregates::read(&conn, inserted_post.id).unwrap();
+ assert_eq!(0, after_comment_delete.comments);
+ assert_eq!(0, after_comment_delete.score);
+ assert_eq!(1, after_comment_delete.upvotes);
+ assert_eq!(1, after_comment_delete.downvotes);
+
+ // Remove the first post like
+ PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
+ let after_like_remove = PostAggregates::read(&conn, inserted_post.id).unwrap();
+ assert_eq!(0, after_like_remove.comments);
+ assert_eq!(-1, after_like_remove.score);
+ assert_eq!(0, after_like_remove.upvotes);
+ assert_eq!(1, after_like_remove.downvotes);
+
+ // This should delete all the associated rows, and fire triggers
+ User_::delete(&conn, another_inserted_user.id).unwrap();
+ let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
+ assert_eq!(1, user_num_deleted);
+
+ // Should be none found, since the creator was deleted
+ let after_delete = PostAggregates::read(&conn, inserted_post.id);
+ assert!(after_delete.is_err());
+ }
+}
--- /dev/null
+use crate::schema::site_aggregates;
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize)]
+#[table_name = "site_aggregates"]
+pub struct SiteAggregates {
+ pub id: i32,
+ pub users: i64,
+ pub posts: i64,
+ pub comments: i64,
+ pub communities: i64,
+}
+
+impl SiteAggregates {
+ pub fn read(conn: &PgConnection) -> Result<Self, Error> {
+ site_aggregates::table.first::<Self>(conn)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{
+ aggregates::site_aggregates::SiteAggregates,
+ source::{
+ comment::{Comment, CommentForm},
+ community::{Community, CommunityForm},
+ post::{Post, PostForm},
+ user::{UserForm, User_},
+ },
+ tests::establish_unpooled_connection,
+ Crud,
+ ListingType,
+ SortType,
+ };
+
+ #[test]
+ fn test_crud() {
+ let conn = establish_unpooled_connection();
+
+ let new_user = UserForm {
+ name: "thommy_site_agg".into(),
+ preferred_username: None,
+ password_encrypted: "nope".into(),
+ email: None,
+ matrix_user_id: None,
+ avatar: None,
+ banner: None,
+ admin: false,
+ banned: Some(false),
+ published: None,
+ updated: None,
+ show_nsfw: false,
+ theme: "browser".into(),
+ default_sort_type: SortType::Hot as i16,
+ default_listing_type: ListingType::Subscribed as i16,
+ lang: "browser".into(),
+ show_avatars: true,
+ send_notifications_to_email: false,
+ actor_id: None,
+ bio: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ };
+
+ let inserted_user = User_::create(&conn, &new_user).unwrap();
+
+ let new_community = CommunityForm {
+ name: "TIL_site_agg".into(),
+ creator_id: inserted_user.id,
+ title: "nada".to_owned(),
+ description: None,
+ category_id: 1,
+ nsfw: false,
+ removed: None,
+ deleted: None,
+ updated: None,
+ actor_id: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ published: None,
+ icon: None,
+ banner: None,
+ };
+
+ let inserted_community = Community::create(&conn, &new_community).unwrap();
+
+ let new_post = PostForm {
+ name: "A test post".into(),
+ url: None,
+ body: None,
+ creator_id: inserted_user.id,
+ community_id: inserted_community.id,
+ removed: None,
+ deleted: None,
+ locked: None,
+ stickied: None,
+ nsfw: false,
+ updated: None,
+ embed_title: None,
+ embed_description: None,
+ embed_html: None,
+ thumbnail_url: None,
+ ap_id: None,
+ local: true,
+ published: None,
+ };
+
+ // Insert two of those posts
+ let inserted_post = Post::create(&conn, &new_post).unwrap();
+ let _inserted_post_again = Post::create(&conn, &new_post).unwrap();
+
+ let comment_form = CommentForm {
+ content: "A test comment".into(),
+ creator_id: inserted_user.id,
+ post_id: inserted_post.id,
+ removed: None,
+ deleted: None,
+ read: None,
+ parent_id: None,
+ published: None,
+ updated: None,
+ ap_id: None,
+ local: true,
+ };
+
+ // Insert two of those comments
+ let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
+
+ let child_comment_form = CommentForm {
+ content: "A test comment".into(),
+ creator_id: inserted_user.id,
+ post_id: inserted_post.id,
+ removed: None,
+ deleted: None,
+ read: None,
+ parent_id: Some(inserted_comment.id),
+ published: None,
+ updated: None,
+ ap_id: None,
+ local: true,
+ };
+
+ let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
+
+ let site_aggregates_before_delete = SiteAggregates::read(&conn).unwrap();
+
+ assert_eq!(1, site_aggregates_before_delete.users);
+ assert_eq!(1, site_aggregates_before_delete.communities);
+ assert_eq!(2, site_aggregates_before_delete.posts);
+ assert_eq!(2, site_aggregates_before_delete.comments);
+
+ // Try a post delete
+ Post::delete(&conn, inserted_post.id).unwrap();
+ let site_aggregates_after_post_delete = SiteAggregates::read(&conn).unwrap();
+ assert_eq!(1, site_aggregates_after_post_delete.posts);
+ assert_eq!(0, site_aggregates_after_post_delete.comments);
+
+ // This shouuld delete all the associated rows, and fire triggers
+ let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
+ assert_eq!(1, user_num_deleted);
+
+ let site_aggregates_after_delete = SiteAggregates::read(&conn).unwrap();
+ assert_eq!(0, site_aggregates_after_delete.users);
+ assert_eq!(0, site_aggregates_after_delete.communities);
+ assert_eq!(0, site_aggregates_after_delete.posts);
+ assert_eq!(0, site_aggregates_after_delete.comments);
+ }
+}
--- /dev/null
+use crate::schema::user_aggregates;
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Queryable, Associations, Identifiable, PartialEq, Debug, Serialize, Clone)]
+#[table_name = "user_aggregates"]
+pub struct UserAggregates {
+ pub id: i32,
+ pub user_id: i32,
+ pub post_count: i64,
+ pub post_score: i64,
+ pub comment_count: i64,
+ pub comment_score: i64,
+}
+
+impl UserAggregates {
+ pub fn read(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
+ user_aggregates::table
+ .filter(user_aggregates::user_id.eq(user_id))
+ .first::<Self>(conn)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{
+ aggregates::user_aggregates::UserAggregates,
+ source::{
+ comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
+ community::{Community, CommunityForm},
+ post::{Post, PostForm, PostLike, PostLikeForm},
+ user::{UserForm, User_},
+ },
+ tests::establish_unpooled_connection,
+ Crud,
+ Likeable,
+ ListingType,
+ SortType,
+ };
+
+ #[test]
+ fn test_crud() {
+ let conn = establish_unpooled_connection();
+
+ let new_user = UserForm {
+ name: "thommy_user_agg".into(),
+ preferred_username: None,
+ password_encrypted: "nope".into(),
+ email: None,
+ matrix_user_id: None,
+ avatar: None,
+ banner: None,
+ admin: false,
+ banned: Some(false),
+ published: None,
+ updated: None,
+ show_nsfw: false,
+ theme: "browser".into(),
+ default_sort_type: SortType::Hot as i16,
+ default_listing_type: ListingType::Subscribed as i16,
+ lang: "browser".into(),
+ show_avatars: true,
+ send_notifications_to_email: false,
+ actor_id: None,
+ bio: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ };
+
+ let inserted_user = User_::create(&conn, &new_user).unwrap();
+
+ let another_user = UserForm {
+ name: "jerry_user_agg".into(),
+ preferred_username: None,
+ password_encrypted: "nope".into(),
+ email: None,
+ matrix_user_id: None,
+ avatar: None,
+ banner: None,
+ admin: false,
+ banned: Some(false),
+ published: None,
+ updated: None,
+ show_nsfw: false,
+ theme: "browser".into(),
+ default_sort_type: SortType::Hot as i16,
+ default_listing_type: ListingType::Subscribed as i16,
+ lang: "browser".into(),
+ show_avatars: true,
+ send_notifications_to_email: false,
+ actor_id: None,
+ bio: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ };
+
+ let another_inserted_user = User_::create(&conn, &another_user).unwrap();
+
+ let new_community = CommunityForm {
+ name: "TIL_site_agg".into(),
+ creator_id: inserted_user.id,
+ title: "nada".to_owned(),
+ description: None,
+ category_id: 1,
+ nsfw: false,
+ removed: None,
+ deleted: None,
+ updated: None,
+ actor_id: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ published: None,
+ icon: None,
+ banner: None,
+ };
+
+ let inserted_community = Community::create(&conn, &new_community).unwrap();
+
+ let new_post = PostForm {
+ name: "A test post".into(),
+ url: None,
+ body: None,
+ creator_id: inserted_user.id,
+ community_id: inserted_community.id,
+ removed: None,
+ deleted: None,
+ locked: None,
+ stickied: None,
+ nsfw: false,
+ updated: None,
+ embed_title: None,
+ embed_description: None,
+ embed_html: None,
+ thumbnail_url: None,
+ ap_id: None,
+ local: true,
+ published: None,
+ };
+
+ let inserted_post = Post::create(&conn, &new_post).unwrap();
+
+ let post_like = PostLikeForm {
+ post_id: inserted_post.id,
+ user_id: inserted_user.id,
+ score: 1,
+ };
+
+ let _inserted_post_like = PostLike::like(&conn, &post_like).unwrap();
+
+ let comment_form = CommentForm {
+ content: "A test comment".into(),
+ creator_id: inserted_user.id,
+ post_id: inserted_post.id,
+ removed: None,
+ deleted: None,
+ read: None,
+ parent_id: None,
+ published: None,
+ updated: None,
+ ap_id: None,
+ local: true,
+ };
+
+ let inserted_comment = Comment::create(&conn, &comment_form).unwrap();
+
+ let mut comment_like = CommentLikeForm {
+ comment_id: inserted_comment.id,
+ user_id: inserted_user.id,
+ post_id: inserted_post.id,
+ score: 1,
+ };
+
+ let _inserted_comment_like = CommentLike::like(&conn, &comment_like).unwrap();
+
+ let mut child_comment_form = CommentForm {
+ content: "A test comment".into(),
+ creator_id: inserted_user.id,
+ post_id: inserted_post.id,
+ removed: None,
+ deleted: None,
+ read: None,
+ parent_id: Some(inserted_comment.id),
+ published: None,
+ updated: None,
+ ap_id: None,
+ local: true,
+ };
+
+ let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap();
+
+ let child_comment_like = CommentLikeForm {
+ comment_id: inserted_child_comment.id,
+ user_id: another_inserted_user.id,
+ post_id: inserted_post.id,
+ score: 1,
+ };
+
+ let _inserted_child_comment_like = CommentLike::like(&conn, &child_comment_like).unwrap();
+
+ let user_aggregates_before_delete = UserAggregates::read(&conn, inserted_user.id).unwrap();
+
+ assert_eq!(1, user_aggregates_before_delete.post_count);
+ assert_eq!(1, user_aggregates_before_delete.post_score);
+ assert_eq!(2, user_aggregates_before_delete.comment_count);
+ assert_eq!(2, user_aggregates_before_delete.comment_score);
+
+ // Remove a post like
+ PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
+ let after_post_like_remove = UserAggregates::read(&conn, inserted_user.id).unwrap();
+ assert_eq!(0, after_post_like_remove.post_score);
+
+ // Remove a parent comment (the scores should also be removed)
+ Comment::delete(&conn, inserted_comment.id).unwrap();
+ let after_parent_comment_delete = UserAggregates::read(&conn, inserted_user.id).unwrap();
+ assert_eq!(0, after_parent_comment_delete.comment_count);
+ assert_eq!(0, after_parent_comment_delete.comment_score);
+
+ // Add in the two comments again, then delete the post.
+ let new_parent_comment = Comment::create(&conn, &comment_form).unwrap();
+ child_comment_form.parent_id = Some(new_parent_comment.id);
+ Comment::create(&conn, &child_comment_form).unwrap();
+ comment_like.comment_id = new_parent_comment.id;
+ CommentLike::like(&conn, &comment_like).unwrap();
+ let after_comment_add = UserAggregates::read(&conn, inserted_user.id).unwrap();
+ assert_eq!(2, after_comment_add.comment_count);
+ assert_eq!(1, after_comment_add.comment_score);
+
+ Post::delete(&conn, inserted_post.id).unwrap();
+ let after_post_delete = UserAggregates::read(&conn, inserted_user.id).unwrap();
+ assert_eq!(0, after_post_delete.comment_score);
+ assert_eq!(0, after_post_delete.comment_count);
+ assert_eq!(0, after_post_delete.post_score);
+ assert_eq!(0, after_post_delete.post_count);
+
+ // This should delete all the associated rows, and fire triggers
+ let user_num_deleted = User_::delete(&conn, inserted_user.id).unwrap();
+ assert_eq!(1, user_num_deleted);
+ User_::delete(&conn, another_inserted_user.id).unwrap();
+
+ // Should be none found
+ let after_delete = UserAggregates::read(&conn, inserted_user.id);
+ assert!(after_delete.is_err());
+ }
+}
use serde::{Deserialize, Serialize};
use crate::{
- comment::Comment,
limit_and_offset,
naive_now,
schema::comment_report,
+ source::comment::Comment,
MaybeOptional,
Reportable,
};
#[cfg(test)]
mod tests {
use crate::{
- comment::*,
comment_view::*,
- community::*,
- post::*,
+ source::{comment::*, community::*, post::*, user::*},
tests::establish_unpooled_connection,
- user::*,
Crud,
Likeable,
*,
+++ /dev/null
-use super::community_view::community_fast_view::BoxedQuery;
-use crate::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
-use diesel::{pg::Pg, result::Error, *};
-use serde::{Deserialize, Serialize};
-
-table! {
- community_view (id) {
- id -> Int4,
- name -> Varchar,
- title -> Varchar,
- icon -> Nullable<Text>,
- banner -> Nullable<Text>,
- description -> Nullable<Text>,
- category_id -> Int4,
- creator_id -> Int4,
- removed -> Bool,
- published -> Timestamp,
- updated -> Nullable<Timestamp>,
- deleted -> Bool,
- nsfw -> Bool,
- actor_id -> Text,
- local -> Bool,
- last_refreshed_at -> Timestamp,
- creator_actor_id -> Text,
- creator_local -> Bool,
- creator_name -> Varchar,
- creator_preferred_username -> Nullable<Varchar>,
- creator_avatar -> Nullable<Text>,
- category_name -> Varchar,
- number_of_subscribers -> BigInt,
- number_of_posts -> BigInt,
- number_of_comments -> BigInt,
- hot_rank -> Int4,
- user_id -> Nullable<Int4>,
- subscribed -> Nullable<Bool>,
- }
-}
-
-table! {
- community_fast_view (id) {
- id -> Int4,
- name -> Varchar,
- title -> Varchar,
- icon -> Nullable<Text>,
- banner -> Nullable<Text>,
- description -> Nullable<Text>,
- category_id -> Int4,
- creator_id -> Int4,
- removed -> Bool,
- published -> Timestamp,
- updated -> Nullable<Timestamp>,
- deleted -> Bool,
- nsfw -> Bool,
- actor_id -> Text,
- local -> Bool,
- last_refreshed_at -> Timestamp,
- creator_actor_id -> Text,
- creator_local -> Bool,
- creator_name -> Varchar,
- creator_preferred_username -> Nullable<Varchar>,
- creator_avatar -> Nullable<Text>,
- category_name -> Varchar,
- number_of_subscribers -> BigInt,
- number_of_posts -> BigInt,
- number_of_comments -> BigInt,
- hot_rank -> Int4,
- user_id -> Nullable<Int4>,
- subscribed -> Nullable<Bool>,
- }
-}
-
-table! {
- community_moderator_view (id) {
- id -> Int4,
- community_id -> Int4,
- user_id -> Int4,
- published -> Timestamp,
- user_actor_id -> Text,
- user_local -> Bool,
- user_name -> Varchar,
- user_preferred_username -> Nullable<Varchar>,
- avatar -> Nullable<Text>,
- community_actor_id -> Text,
- community_local -> Bool,
- community_name -> Varchar,
- community_icon -> Nullable<Text>,
- }
-}
-
-table! {
- community_follower_view (id) {
- id -> Int4,
- community_id -> Int4,
- user_id -> Int4,
- published -> Timestamp,
- user_actor_id -> Text,
- user_local -> Bool,
- user_name -> Varchar,
- user_preferred_username -> Nullable<Varchar>,
- avatar -> Nullable<Text>,
- community_actor_id -> Text,
- community_local -> Bool,
- community_name -> Varchar,
- community_icon -> Nullable<Text>,
- }
-}
-
-table! {
- community_user_ban_view (id) {
- id -> Int4,
- community_id -> Int4,
- user_id -> Int4,
- published -> Timestamp,
- user_actor_id -> Text,
- user_local -> Bool,
- user_name -> Varchar,
- user_preferred_username -> Nullable<Varchar>,
- avatar -> Nullable<Text>,
- community_actor_id -> Text,
- community_local -> Bool,
- community_name -> Varchar,
- community_icon -> Nullable<Text>,
- }
-}
-
-#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, QueryableByName, Clone)]
-#[table_name = "community_fast_view"]
-pub struct CommunityView {
- pub id: i32,
- pub name: String,
- pub title: String,
- pub icon: Option<String>,
- pub banner: Option<String>,
- pub description: Option<String>,
- pub category_id: i32,
- pub creator_id: i32,
- pub removed: bool,
- pub published: chrono::NaiveDateTime,
- pub updated: Option<chrono::NaiveDateTime>,
- pub deleted: bool,
- pub nsfw: bool,
- pub actor_id: String,
- pub local: bool,
- pub last_refreshed_at: chrono::NaiveDateTime,
- pub creator_actor_id: String,
- pub creator_local: bool,
- pub creator_name: String,
- pub creator_preferred_username: Option<String>,
- pub creator_avatar: Option<String>,
- pub category_name: String,
- pub number_of_subscribers: i64,
- pub number_of_posts: i64,
- pub number_of_comments: i64,
- pub hot_rank: i32,
- pub user_id: Option<i32>,
- pub subscribed: Option<bool>,
-}
-
-pub struct CommunityQueryBuilder<'a> {
- conn: &'a PgConnection,
- query: BoxedQuery<'a, Pg>,
- sort: &'a SortType,
- from_user_id: Option<i32>,
- show_nsfw: bool,
- search_term: Option<String>,
- page: Option<i64>,
- limit: Option<i64>,
-}
-
-impl<'a> CommunityQueryBuilder<'a> {
- pub fn create(conn: &'a PgConnection) -> Self {
- use super::community_view::community_fast_view::dsl::*;
-
- let query = community_fast_view.into_boxed();
-
- CommunityQueryBuilder {
- conn,
- query,
- sort: &SortType::Hot,
- from_user_id: None,
- show_nsfw: true,
- search_term: None,
- page: None,
- limit: None,
- }
- }
-
- pub fn sort(mut self, sort: &'a SortType) -> Self {
- self.sort = sort;
- self
- }
-
- pub fn for_user<T: MaybeOptional<i32>>(mut self, from_user_id: T) -> Self {
- self.from_user_id = from_user_id.get_optional();
- self
- }
-
- pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
- self.show_nsfw = show_nsfw;
- self
- }
-
- pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
- self.search_term = search_term.get_optional();
- self
- }
-
- pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
- self.page = page.get_optional();
- self
- }
-
- pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
- self.limit = limit.get_optional();
- self
- }
-
- pub fn list(self) -> Result<Vec<CommunityView>, Error> {
- use super::community_view::community_fast_view::dsl::*;
-
- let mut query = self.query;
-
- if let Some(search_term) = self.search_term {
- let searcher = fuzzy_search(&search_term);
- query = query
- .filter(name.ilike(searcher.to_owned()))
- .or_filter(title.ilike(searcher.to_owned()))
- .or_filter(description.ilike(searcher));
- };
-
- // The view lets you pass a null user_id, if you're not logged in
- match self.sort {
- SortType::New => query = query.order_by(published.desc()).filter(user_id.is_null()),
- SortType::TopAll => match self.from_user_id {
- Some(from_user_id) => {
- query = query
- .filter(user_id.eq(from_user_id))
- .order_by((subscribed.asc(), number_of_subscribers.desc()))
- }
- None => {
- query = query
- .order_by(number_of_subscribers.desc())
- .filter(user_id.is_null())
- }
- },
- // Covers all other sorts, including hot
- _ => {
- query = query
- .order_by(hot_rank.desc())
- .then_order_by(number_of_subscribers.desc())
- .filter(user_id.is_null())
- }
- };
-
- if !self.show_nsfw {
- query = query.filter(nsfw.eq(false));
- };
-
- let (limit, offset) = limit_and_offset(self.page, self.limit);
- query
- .limit(limit)
- .offset(offset)
- .filter(removed.eq(false))
- .filter(deleted.eq(false))
- .load::<CommunityView>(self.conn)
- }
-}
-
-impl CommunityView {
- pub fn read(
- conn: &PgConnection,
- from_community_id: i32,
- from_user_id: Option<i32>,
- ) -> Result<Self, Error> {
- use super::community_view::community_fast_view::dsl::*;
-
- let mut query = community_fast_view.into_boxed();
-
- query = query.filter(id.eq(from_community_id));
-
- // The view lets you pass a null user_id, if you're not logged in
- if let Some(from_user_id) = from_user_id {
- query = query.filter(user_id.eq(from_user_id));
- } else {
- query = query.filter(user_id.is_null());
- };
-
- query.first::<Self>(conn)
- }
-}
-
-#[derive(
- Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
-)]
-#[table_name = "community_moderator_view"]
-pub struct CommunityModeratorView {
- pub id: i32,
- pub community_id: i32,
- pub user_id: i32,
- pub published: chrono::NaiveDateTime,
- pub user_actor_id: String,
- pub user_local: bool,
- pub user_name: String,
- pub user_preferred_username: Option<String>,
- pub avatar: Option<String>,
- pub community_actor_id: String,
- pub community_local: bool,
- pub community_name: String,
- pub community_icon: Option<String>,
-}
-
-impl CommunityModeratorView {
- pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result<Vec<Self>, Error> {
- use super::community_view::community_moderator_view::dsl::*;
- community_moderator_view
- .filter(community_id.eq(for_community_id))
- .order_by(published)
- .load::<Self>(conn)
- }
-
- pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result<Vec<Self>, Error> {
- use super::community_view::community_moderator_view::dsl::*;
- community_moderator_view
- .filter(user_id.eq(for_user_id))
- .order_by(published)
- .load::<Self>(conn)
- }
-}
-
-#[derive(
- Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
-)]
-#[table_name = "community_follower_view"]
-pub struct CommunityFollowerView {
- pub id: i32,
- pub community_id: i32,
- pub user_id: i32,
- pub published: chrono::NaiveDateTime,
- pub user_actor_id: String,
- pub user_local: bool,
- pub user_name: String,
- pub user_preferred_username: Option<String>,
- pub avatar: Option<String>,
- pub community_actor_id: String,
- pub community_local: bool,
- pub community_name: String,
- pub community_icon: Option<String>,
-}
-
-impl CommunityFollowerView {
- pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
- use super::community_view::community_follower_view::dsl::*;
- community_follower_view
- .filter(community_id.eq(from_community_id))
- .load::<Self>(conn)
- }
-
- pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
- use super::community_view::community_follower_view::dsl::*;
- community_follower_view
- .filter(user_id.eq(from_user_id))
- .load::<Self>(conn)
- }
-}
-
-#[derive(
- Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
-)]
-#[table_name = "community_user_ban_view"]
-pub struct CommunityUserBanView {
- pub id: i32,
- pub community_id: i32,
- pub user_id: i32,
- pub published: chrono::NaiveDateTime,
- pub user_actor_id: String,
- pub user_local: bool,
- pub user_name: String,
- pub user_preferred_username: Option<String>,
- pub avatar: Option<String>,
- pub community_actor_id: String,
- pub community_local: bool,
- pub community_name: String,
- pub community_icon: Option<String>,
-}
-
-impl CommunityUserBanView {
- pub fn get(
- conn: &PgConnection,
- from_user_id: i32,
- from_community_id: i32,
- ) -> Result<Self, Error> {
- use super::community_view::community_user_ban_view::dsl::*;
- community_user_ban_view
- .filter(user_id.eq(from_user_id))
- .filter(community_id.eq(from_community_id))
- .first::<Self>(conn)
- }
-}
use serde::{Deserialize, Serialize};
use std::{env, env::VarError};
-pub mod activity;
-pub mod category;
-pub mod comment;
pub mod comment_report;
pub mod comment_view;
-pub mod community;
-pub mod community_view;
-pub mod moderator;
pub mod moderator_views;
-pub mod password_reset_request;
-pub mod post;
pub mod post_report;
-pub mod post_view;
-pub mod private_message;
pub mod private_message_view;
-pub mod schema;
-pub mod site;
-pub mod site_view;
-pub mod user;
-pub mod user_mention;
pub mod user_mention_view;
-pub mod user_view;
+
+pub mod aggregates;
+pub mod schema;
+pub mod source;
+pub mod views;
pub type DbPool = diesel::r2d2::Pool<diesel::r2d2::ConnectionManager<diesel::PgConnection>>;
}
}
+pub(crate) trait ToSafe {
+ type SafeColumns;
+ fn safe_columns_tuple() -> Self::SafeColumns;
+}
+
pub fn get_database_url_from_env() -> Result<String, VarError> {
env::var("LEMMY_DATABASE_URL")
}
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
}
+pub(crate) mod functions {
+ use diesel::sql_types::*;
+
+ sql_function! {
+ fn hot_rank(score: BigInt, time: Timestamp) -> Integer;
+ }
+}
+
#[cfg(test)]
mod tests {
use super::fuzzy_search;
use crate::{
limit_and_offset,
naive_now,
- post::Post,
schema::post_report,
+ source::post::Post,
MaybeOptional,
Reportable,
};
+++ /dev/null
-use super::post_view::post_fast_view::BoxedQuery;
-use crate::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
-use diesel::{dsl::*, pg::Pg, result::Error, *};
-use serde::Serialize;
-
-// The faked schema since diesel doesn't do views
-table! {
- post_view (id) {
- id -> Int4,
- name -> Varchar,
- url -> Nullable<Text>,
- body -> Nullable<Text>,
- creator_id -> Int4,
- community_id -> Int4,
- removed -> Bool,
- locked -> Bool,
- published -> Timestamp,
- updated -> Nullable<Timestamp>,
- deleted -> Bool,
- nsfw -> Bool,
- stickied -> Bool,
- embed_title -> Nullable<Text>,
- embed_description -> Nullable<Text>,
- embed_html -> Nullable<Text>,
- thumbnail_url -> Nullable<Text>,
- ap_id -> Text,
- local -> Bool,
- creator_actor_id -> Text,
- creator_local -> Bool,
- creator_name -> Varchar,
- creator_preferred_username -> Nullable<Varchar>,
- creator_published -> Timestamp,
- creator_avatar -> Nullable<Text>,
- banned -> Bool,
- banned_from_community -> Bool,
- community_actor_id -> Text,
- community_local -> Bool,
- community_name -> Varchar,
- community_icon -> Nullable<Text>,
- community_removed -> Bool,
- community_deleted -> Bool,
- community_nsfw -> Bool,
- number_of_comments -> BigInt,
- score -> BigInt,
- upvotes -> BigInt,
- downvotes -> BigInt,
- hot_rank -> Int4,
- hot_rank_active -> Int4,
- newest_activity_time -> Timestamp,
- user_id -> Nullable<Int4>,
- my_vote -> Nullable<Int4>,
- subscribed -> Nullable<Bool>,
- read -> Nullable<Bool>,
- saved -> Nullable<Bool>,
- }
-}
-
-table! {
- post_fast_view (id) {
- id -> Int4,
- name -> Varchar,
- url -> Nullable<Text>,
- body -> Nullable<Text>,
- creator_id -> Int4,
- community_id -> Int4,
- removed -> Bool,
- locked -> Bool,
- published -> Timestamp,
- updated -> Nullable<Timestamp>,
- deleted -> Bool,
- nsfw -> Bool,
- stickied -> Bool,
- embed_title -> Nullable<Text>,
- embed_description -> Nullable<Text>,
- embed_html -> Nullable<Text>,
- thumbnail_url -> Nullable<Text>,
- ap_id -> Text,
- local -> Bool,
- creator_actor_id -> Text,
- creator_local -> Bool,
- creator_name -> Varchar,
- creator_preferred_username -> Nullable<Varchar>,
- creator_published -> Timestamp,
- creator_avatar -> Nullable<Text>,
- banned -> Bool,
- banned_from_community -> Bool,
- community_actor_id -> Text,
- community_local -> Bool,
- community_name -> Varchar,
- community_icon -> Nullable<Text>,
- community_removed -> Bool,
- community_deleted -> Bool,
- community_nsfw -> Bool,
- number_of_comments -> BigInt,
- score -> BigInt,
- upvotes -> BigInt,
- downvotes -> BigInt,
- hot_rank -> Int4,
- hot_rank_active -> Int4,
- newest_activity_time -> Timestamp,
- user_id -> Nullable<Int4>,
- my_vote -> Nullable<Int4>,
- subscribed -> Nullable<Bool>,
- read -> Nullable<Bool>,
- saved -> Nullable<Bool>,
- }
-}
-
-#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, QueryableByName, Clone)]
-#[table_name = "post_fast_view"]
-pub struct PostView {
- pub id: i32,
- pub name: String,
- pub url: Option<String>,
- pub body: Option<String>,
- pub creator_id: i32,
- pub community_id: i32,
- pub removed: bool,
- pub locked: bool,
- pub published: chrono::NaiveDateTime,
- pub updated: Option<chrono::NaiveDateTime>,
- pub deleted: bool,
- pub nsfw: bool,
- pub stickied: bool,
- pub embed_title: Option<String>,
- pub embed_description: Option<String>,
- pub embed_html: Option<String>,
- pub thumbnail_url: Option<String>,
- pub ap_id: String,
- pub local: bool,
- pub creator_actor_id: String,
- pub creator_local: bool,
- pub creator_name: String,
- pub creator_preferred_username: Option<String>,
- pub creator_published: chrono::NaiveDateTime,
- pub creator_avatar: Option<String>,
- pub banned: bool,
- pub banned_from_community: bool,
- pub community_actor_id: String,
- pub community_local: bool,
- pub community_name: String,
- pub community_icon: Option<String>,
- pub community_removed: bool,
- pub community_deleted: bool,
- pub community_nsfw: bool,
- pub number_of_comments: i64,
- pub score: i64,
- pub upvotes: i64,
- pub downvotes: i64,
- pub hot_rank: i32,
- pub hot_rank_active: i32,
- pub newest_activity_time: chrono::NaiveDateTime,
- pub user_id: Option<i32>,
- pub my_vote: Option<i32>,
- pub subscribed: Option<bool>,
- pub read: Option<bool>,
- pub saved: Option<bool>,
-}
-
-pub struct PostQueryBuilder<'a> {
- conn: &'a PgConnection,
- query: BoxedQuery<'a, Pg>,
- listing_type: &'a ListingType,
- sort: &'a SortType,
- my_user_id: Option<i32>,
- for_creator_id: Option<i32>,
- for_community_id: Option<i32>,
- for_community_name: Option<String>,
- search_term: Option<String>,
- url_search: Option<String>,
- show_nsfw: bool,
- saved_only: bool,
- unread_only: bool,
- page: Option<i64>,
- limit: Option<i64>,
-}
-
-impl<'a> PostQueryBuilder<'a> {
- pub fn create(conn: &'a PgConnection) -> Self {
- use super::post_view::post_fast_view::dsl::*;
-
- let query = post_fast_view.into_boxed();
-
- PostQueryBuilder {
- conn,
- query,
- listing_type: &ListingType::All,
- sort: &SortType::Hot,
- my_user_id: None,
- for_creator_id: None,
- for_community_id: None,
- for_community_name: None,
- search_term: None,
- url_search: None,
- show_nsfw: true,
- saved_only: false,
- unread_only: false,
- page: None,
- limit: None,
- }
- }
-
- pub fn listing_type(mut self, listing_type: &'a ListingType) -> Self {
- self.listing_type = listing_type;
- self
- }
-
- pub fn sort(mut self, sort: &'a SortType) -> Self {
- self.sort = sort;
- self
- }
-
- pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
- self.for_community_id = for_community_id.get_optional();
- self
- }
-
- pub fn for_community_name<T: MaybeOptional<String>>(mut self, for_community_name: T) -> Self {
- self.for_community_name = for_community_name.get_optional();
- self
- }
-
- pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
- self.for_creator_id = for_creator_id.get_optional();
- self
- }
-
- pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
- self.search_term = search_term.get_optional();
- self
- }
-
- pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
- self.url_search = url_search.get_optional();
- self
- }
-
- pub fn my_user_id<T: MaybeOptional<i32>>(mut self, my_user_id: T) -> Self {
- self.my_user_id = my_user_id.get_optional();
- self
- }
-
- pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
- self.show_nsfw = show_nsfw;
- self
- }
-
- pub fn saved_only(mut self, saved_only: bool) -> Self {
- self.saved_only = saved_only;
- self
- }
-
- pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
- self.page = page.get_optional();
- self
- }
-
- pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
- self.limit = limit.get_optional();
- self
- }
-
- pub fn list(self) -> Result<Vec<PostView>, Error> {
- use super::post_view::post_fast_view::dsl::*;
-
- let mut query = self.query;
-
- query = match self.listing_type {
- ListingType::Subscribed => query.filter(subscribed.eq(true)),
- ListingType::Local => query.filter(community_local.eq(true)),
- _ => query,
- };
-
- if let Some(for_community_id) = self.for_community_id {
- query = query
- .filter(community_id.eq(for_community_id))
- .then_order_by(stickied.desc());
- }
-
- if let Some(for_community_name) = self.for_community_name {
- query = query
- .filter(community_name.eq(for_community_name))
- .filter(community_local.eq(true))
- .then_order_by(stickied.desc());
- }
-
- if let Some(url_search) = self.url_search {
- query = query.filter(url.eq(url_search));
- }
-
- if let Some(search_term) = self.search_term {
- let searcher = fuzzy_search(&search_term);
- query = query.filter(name.ilike(searcher.to_owned()).or(body.ilike(searcher)));
- }
-
- query = match self.sort {
- SortType::Active => query
- .then_order_by(hot_rank_active.desc())
- .then_order_by(published.desc()),
- SortType::Hot => query
- .then_order_by(hot_rank.desc())
- .then_order_by(published.desc()),
- SortType::New => query.then_order_by(published.desc()),
- SortType::TopAll => query.then_order_by(score.desc()),
- SortType::TopYear => query
- .filter(published.gt(now - 1.years()))
- .then_order_by(score.desc()),
- SortType::TopMonth => query
- .filter(published.gt(now - 1.months()))
- .then_order_by(score.desc()),
- SortType::TopWeek => query
- .filter(published.gt(now - 1.weeks()))
- .then_order_by(score.desc()),
- SortType::TopDay => query
- .filter(published.gt(now - 1.days()))
- .then_order_by(score.desc()),
- };
-
- // The view lets you pass a null user_id, if you're not logged in
- query = if let Some(my_user_id) = self.my_user_id {
- query.filter(user_id.eq(my_user_id))
- } else {
- query.filter(user_id.is_null())
- };
-
- // If its for a specific user, show the removed / deleted
- if let Some(for_creator_id) = self.for_creator_id {
- query = query.filter(creator_id.eq(for_creator_id));
- } else {
- query = query
- .filter(removed.eq(false))
- .filter(deleted.eq(false))
- .filter(community_removed.eq(false))
- .filter(community_deleted.eq(false));
- }
-
- if !self.show_nsfw {
- query = query
- .filter(nsfw.eq(false))
- .filter(community_nsfw.eq(false));
- };
-
- // TODO these are wrong, bc they'll only show saved for your logged in user, not theirs
- if self.saved_only {
- query = query.filter(saved.eq(true));
- };
-
- if self.unread_only {
- query = query.filter(read.eq(false));
- };
-
- let (limit, offset) = limit_and_offset(self.page, self.limit);
- query = query
- .limit(limit)
- .offset(offset)
- .filter(removed.eq(false))
- .filter(deleted.eq(false))
- .filter(community_removed.eq(false))
- .filter(community_deleted.eq(false));
-
- query.load::<PostView>(self.conn)
- }
-}
-
-impl PostView {
- pub fn read(
- conn: &PgConnection,
- from_post_id: i32,
- my_user_id: Option<i32>,
- ) -> Result<Self, Error> {
- use super::post_view::post_fast_view::dsl::*;
- use diesel::prelude::*;
-
- let mut query = post_fast_view.into_boxed();
-
- query = query.filter(id.eq(from_post_id));
-
- if let Some(my_user_id) = my_user_id {
- query = query.filter(user_id.eq(my_user_id));
- } else {
- query = query.filter(user_id.is_null());
- };
-
- query.first::<Self>(conn)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use crate::{
- community::*,
- post::*,
- post_view::*,
- tests::establish_unpooled_connection,
- user::*,
- Crud,
- Likeable,
- *,
- };
-
- #[test]
- fn test_crud() {
- let conn = establish_unpooled_connection();
-
- let user_name = "tegan".to_string();
- let community_name = "test_community_3".to_string();
- let post_name = "test post 3".to_string();
-
- let new_user = UserForm {
- name: user_name.to_owned(),
- preferred_username: None,
- password_encrypted: "nope".into(),
- email: None,
- matrix_user_id: None,
- avatar: None,
- banner: None,
- published: None,
- updated: None,
- admin: false,
- banned: Some(false),
- show_nsfw: false,
- theme: "browser".into(),
- default_sort_type: SortType::Hot as i16,
- default_listing_type: ListingType::Subscribed as i16,
- lang: "browser".into(),
- show_avatars: true,
- send_notifications_to_email: false,
- actor_id: None,
- bio: None,
- local: true,
- private_key: None,
- public_key: None,
- last_refreshed_at: None,
- };
-
- let inserted_user = User_::create(&conn, &new_user).unwrap();
-
- let new_community = CommunityForm {
- name: community_name.to_owned(),
- title: "nada".to_owned(),
- description: None,
- creator_id: inserted_user.id,
- category_id: 1,
- removed: None,
- deleted: None,
- updated: None,
- nsfw: false,
- actor_id: None,
- local: true,
- private_key: None,
- public_key: None,
- last_refreshed_at: None,
- published: None,
- icon: None,
- banner: None,
- };
-
- let inserted_community = Community::create(&conn, &new_community).unwrap();
-
- let new_post = PostForm {
- name: post_name.to_owned(),
- url: None,
- body: None,
- creator_id: inserted_user.id,
- community_id: inserted_community.id,
- removed: None,
- deleted: None,
- locked: None,
- stickied: None,
- updated: None,
- nsfw: false,
- embed_title: None,
- embed_description: None,
- embed_html: None,
- thumbnail_url: None,
- ap_id: None,
- local: true,
- published: None,
- };
-
- let inserted_post = Post::create(&conn, &new_post).unwrap();
-
- let post_like_form = PostLikeForm {
- post_id: inserted_post.id,
- user_id: inserted_user.id,
- score: 1,
- };
-
- let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
-
- let expected_post_like = PostLike {
- id: inserted_post_like.id,
- post_id: inserted_post.id,
- user_id: inserted_user.id,
- published: inserted_post_like.published,
- score: 1,
- };
-
- let read_post_listings_with_user = PostQueryBuilder::create(&conn)
- .listing_type(&ListingType::Community)
- .sort(&SortType::New)
- .for_community_id(inserted_community.id)
- .my_user_id(inserted_user.id)
- .list()
- .unwrap();
-
- let read_post_listings_no_user = PostQueryBuilder::create(&conn)
- .listing_type(&ListingType::Community)
- .sort(&SortType::New)
- .for_community_id(inserted_community.id)
- .list()
- .unwrap();
-
- let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
- let read_post_listing_with_user =
- PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
-
- // the non user version
- let expected_post_listing_no_user = PostView {
- user_id: None,
- my_vote: None,
- id: inserted_post.id,
- name: post_name.to_owned(),
- url: None,
- body: None,
- creator_id: inserted_user.id,
- creator_name: user_name.to_owned(),
- creator_preferred_username: None,
- creator_published: inserted_user.published,
- creator_avatar: None,
- banned: false,
- banned_from_community: false,
- community_id: inserted_community.id,
- removed: false,
- deleted: false,
- locked: false,
- stickied: false,
- community_name: community_name.to_owned(),
- community_icon: None,
- community_removed: false,
- community_deleted: false,
- community_nsfw: false,
- number_of_comments: 0,
- score: 1,
- upvotes: 1,
- downvotes: 0,
- hot_rank: read_post_listing_no_user.hot_rank,
- hot_rank_active: read_post_listing_no_user.hot_rank_active,
- published: inserted_post.published,
- newest_activity_time: inserted_post.published,
- updated: None,
- subscribed: None,
- read: None,
- saved: None,
- nsfw: false,
- embed_title: None,
- embed_description: None,
- embed_html: None,
- thumbnail_url: None,
- ap_id: inserted_post.ap_id.to_owned(),
- local: true,
- creator_actor_id: inserted_user.actor_id.to_owned(),
- creator_local: true,
- community_actor_id: inserted_community.actor_id.to_owned(),
- community_local: true,
- };
-
- let expected_post_listing_with_user = PostView {
- user_id: Some(inserted_user.id),
- my_vote: Some(1),
- id: inserted_post.id,
- name: post_name,
- url: None,
- body: None,
- removed: false,
- deleted: false,
- locked: false,
- stickied: false,
- creator_id: inserted_user.id,
- creator_name: user_name,
- creator_preferred_username: None,
- creator_published: inserted_user.published,
- creator_avatar: None,
- banned: false,
- banned_from_community: false,
- community_id: inserted_community.id,
- community_name,
- community_icon: None,
- community_removed: false,
- community_deleted: false,
- community_nsfw: false,
- number_of_comments: 0,
- score: 1,
- upvotes: 1,
- downvotes: 0,
- hot_rank: read_post_listing_with_user.hot_rank,
- hot_rank_active: read_post_listing_with_user.hot_rank_active,
- published: inserted_post.published,
- newest_activity_time: inserted_post.published,
- updated: None,
- subscribed: Some(false),
- read: Some(false),
- saved: Some(false),
- nsfw: false,
- embed_title: None,
- embed_description: None,
- embed_html: None,
- thumbnail_url: None,
- ap_id: inserted_post.ap_id.to_owned(),
- local: true,
- creator_actor_id: inserted_user.actor_id.to_owned(),
- creator_local: true,
- community_actor_id: inserted_community.actor_id.to_owned(),
- community_local: true,
- };
-
- let like_removed = PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
- let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
- Community::delete(&conn, inserted_community.id).unwrap();
- User_::delete(&conn, inserted_user.id).unwrap();
-
- // The with user
- assert_eq!(
- expected_post_listing_with_user,
- read_post_listings_with_user[0]
- );
- assert_eq!(expected_post_listing_with_user, read_post_listing_with_user);
- assert_eq!(1, read_post_listings_with_user.len());
-
- // Without the user
- assert_eq!(expected_post_listing_no_user, read_post_listings_no_user[0]);
- assert_eq!(expected_post_listing_no_user, read_post_listing_no_user);
- assert_eq!(1, read_post_listings_no_user.len());
-
- // assert_eq!(expected_post, inserted_post);
- // assert_eq!(expected_post, updated_post);
- assert_eq!(expected_post_like, inserted_post_like);
- assert_eq!(1, like_removed);
- assert_eq!(1, num_deleted);
- }
-}
}
}
+table! {
+ community_aggregates (id) {
+ id -> Int4,
+ community_id -> Int4,
+ subscribers -> Int8,
+ posts -> Int8,
+ comments -> Int8,
+ }
+}
+
table! {
community_aggregates_fast (id) {
id -> Int4,
}
}
+table! {
+ post_aggregates (id) {
+ id -> Int4,
+ post_id -> Int4,
+ comments -> Int8,
+ score -> Int8,
+ upvotes -> Int8,
+ downvotes -> Int8,
+ newest_comment_time -> Timestamp,
+ }
+}
+
table! {
post_aggregates_fast (id) {
id -> Int4,
}
}
+table! {
+ site_aggregates (id) {
+ id -> Int4,
+ users -> Int8,
+ posts -> Int8,
+ comments -> Int8,
+ communities -> Int8,
+ }
+}
+
table! {
user_ (id) {
id -> Int4,
}
}
+table! {
+ user_aggregates (id) {
+ id -> Int4,
+ user_id -> Int4,
+ post_count -> Int8,
+ post_score -> Int8,
+ comment_count -> Int8,
+ comment_score -> Int8,
+ }
+}
+
table! {
user_ban (id) {
id -> Int4,
joinable!(comment_saved -> user_ (user_id));
joinable!(community -> category (category_id));
joinable!(community -> user_ (creator_id));
+joinable!(community_aggregates -> community (community_id));
joinable!(community_follower -> community (community_id));
joinable!(community_follower -> user_ (user_id));
joinable!(community_moderator -> community (community_id));
joinable!(password_reset_request -> user_ (user_id));
joinable!(post -> community (community_id));
joinable!(post -> user_ (creator_id));
+joinable!(post_aggregates -> post (post_id));
joinable!(post_like -> post (post_id));
joinable!(post_like -> user_ (user_id));
joinable!(post_read -> post (post_id));
joinable!(post_saved -> post (post_id));
joinable!(post_saved -> user_ (user_id));
joinable!(site -> user_ (creator_id));
+joinable!(user_aggregates -> user_ (user_id));
joinable!(user_ban -> user_ (user_id));
joinable!(user_mention -> comment (comment_id));
joinable!(user_mention -> user_ (recipient_id));
comment_report,
comment_saved,
community,
+ community_aggregates,
community_aggregates_fast,
community_follower,
community_moderator,
mod_sticky_post,
password_reset_request,
post,
+ post_aggregates,
post_aggregates_fast,
post_like,
post_read,
post_saved,
private_message,
site,
+ site_aggregates,
user_,
+ user_aggregates,
user_ban,
user_fast,
user_mention,
+++ /dev/null
-use diesel::{result::Error, *};
-use serde::Serialize;
-
-table! {
- site_view (id) {
- id -> Int4,
- name -> Varchar,
- description -> Nullable<Text>,
- creator_id -> Int4,
- published -> Timestamp,
- updated -> Nullable<Timestamp>,
- enable_downvotes -> Bool,
- open_registration -> Bool,
- enable_nsfw -> Bool,
- icon -> Nullable<Text>,
- banner -> Nullable<Text>,
- creator_name -> Varchar,
- creator_preferred_username -> Nullable<Varchar>,
- creator_avatar -> Nullable<Text>,
- number_of_users -> BigInt,
- number_of_posts -> BigInt,
- number_of_comments -> BigInt,
- number_of_communities -> BigInt,
- }
-}
-
-#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, QueryableByName, Clone)]
-#[table_name = "site_view"]
-pub struct SiteView {
- pub id: i32,
- pub name: String,
- pub description: Option<String>,
- pub creator_id: i32,
- pub published: chrono::NaiveDateTime,
- pub updated: Option<chrono::NaiveDateTime>,
- pub enable_downvotes: bool,
- pub open_registration: bool,
- pub enable_nsfw: bool,
- pub icon: Option<String>,
- pub banner: Option<String>,
- pub creator_name: String,
- pub creator_preferred_username: Option<String>,
- pub creator_avatar: Option<String>,
- pub number_of_users: i64,
- pub number_of_posts: i64,
- pub number_of_comments: i64,
- pub number_of_communities: i64,
-}
-
-impl SiteView {
- pub fn read(conn: &PgConnection) -> Result<Self, Error> {
- use super::site_view::site_view::dsl::*;
- site_view.first::<Self>(conn)
- }
-}
#[cfg(test)]
mod tests {
use crate::{
- activity::{Activity, ActivityForm},
+ source::{
+ activity::{Activity, ActivityForm},
+ user::{UserForm, User_},
+ },
tests::establish_unpooled_connection,
- user::{UserForm, User_},
Crud,
ListingType,
SortType,
use diesel::{dsl::*, result::Error, *};
use serde::Serialize;
-#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize)]
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "category"]
pub struct Category {
pub id: i32,
#[cfg(test)]
mod tests {
- use crate::{category::Category, tests::establish_unpooled_connection};
+ use crate::{source::category::Category, tests::establish_unpooled_connection};
#[test]
fn test_crud() {
pub id: i32,
pub user_id: i32,
pub comment_id: i32,
- pub post_id: i32,
+ pub post_id: i32, // TODO this is redundant
pub score: i16,
pub published: chrono::NaiveDateTime,
}
pub struct CommentLikeForm {
pub user_id: i32,
pub comment_id: i32,
- pub post_id: i32,
+ pub post_id: i32, // TODO this is redundant
pub score: i16,
}
#[cfg(test)]
mod tests {
use crate::{
- comment::*,
- community::*,
- post::*,
+ source::{comment::*, community::*, post::*, user::*},
tests::establish_unpooled_connection,
- user::*,
Crud,
ListingType,
SortType,
Joinable,
};
use diesel::{dsl::*, result::Error, *};
+use serde::Serialize;
-#[derive(Clone, Queryable, Identifiable, PartialEq, Debug)]
+#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "community"]
pub struct Community {
pub id: i32,
pub banner: Option<String>,
}
+/// A safe representation of community, without the sensitive info
+#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
+#[table_name = "community"]
+pub struct CommunitySafe {
+ pub id: i32,
+ pub name: String,
+ pub title: String,
+ pub description: Option<String>,
+ pub category_id: i32,
+ pub creator_id: i32,
+ pub removed: bool,
+ pub published: chrono::NaiveDateTime,
+ pub updated: Option<chrono::NaiveDateTime>,
+ pub deleted: bool,
+ pub nsfw: bool,
+ pub actor_id: String,
+ pub local: bool,
+ pub icon: Option<String>,
+ pub banner: Option<String>,
+}
+
+mod safe_type {
+ use crate::{schema::community::columns::*, source::community::Community, ToSafe};
+ type Columns = (
+ id,
+ name,
+ title,
+ description,
+ category_id,
+ creator_id,
+ removed,
+ published,
+ updated,
+ deleted,
+ nsfw,
+ actor_id,
+ local,
+ icon,
+ banner,
+ );
+
+ impl ToSafe for Community {
+ type SafeColumns = Columns;
+ fn safe_columns_tuple() -> Self::SafeColumns {
+ (
+ id,
+ name,
+ title,
+ description,
+ category_id,
+ creator_id,
+ removed,
+ published,
+ updated,
+ deleted,
+ nsfw,
+ actor_id,
+ local,
+ icon,
+ banner,
+ )
+ }
+ }
+}
+
#[derive(Insertable, AsChangeset, Debug)]
#[table_name = "community"]
pub struct CommunityForm {
}
fn community_mods_and_admins(conn: &PgConnection, community_id: i32) -> Result<Vec<i32>, Error> {
- use crate::{community_view::CommunityModeratorView, user_view::UserView};
+ use crate::views::{community_moderator_view::CommunityModeratorView, user_view::UserViewSafe};
let mut mods_and_admins: Vec<i32> = Vec::new();
mods_and_admins.append(
&mut CommunityModeratorView::for_community(conn, community_id)
- .map(|v| v.into_iter().map(|m| m.user_id).collect())?,
+ .map(|v| v.into_iter().map(|m| m.moderator.id).collect())?,
);
mods_and_admins
- .append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?);
+ .append(&mut UserViewSafe::admins(conn).map(|v| v.into_iter().map(|a| a.user.id).collect())?);
Ok(mods_and_admins)
}
#[cfg(test)]
mod tests {
- use crate::{community::*, tests::establish_unpooled_connection, user::*, ListingType, SortType};
+ use crate::{
+ source::{community::*, user::*},
+ tests::establish_unpooled_connection,
+ ListingType,
+ SortType,
+ };
#[test]
fn test_crud() {
--- /dev/null
+pub mod activity;
+pub mod category;
+pub mod comment;
+pub mod community;
+pub mod moderator;
+pub mod password_reset_request;
+pub mod post;
+pub mod private_message;
+pub mod site;
+pub mod user;
+pub mod user_mention;
#[cfg(test)]
mod tests {
use crate::{
- comment::*,
- community::*,
- moderator::*,
- post::*,
+ source::{comment::*, community::*, moderator::*, post::*, user::*},
tests::establish_unpooled_connection,
- user::*,
ListingType,
SortType,
};
mod tests {
use super::super::user::*;
use crate::{
- password_reset_request::PasswordResetRequest,
+ source::password_reset_request::PasswordResetRequest,
tests::establish_unpooled_connection,
Crud,
ListingType,
Saveable,
};
use diesel::{dsl::*, result::Error, *};
+use serde::Serialize;
use url::{ParseError, Url};
-#[derive(Queryable, Identifiable, PartialEq, Debug)]
+#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
#[table_name = "post"]
pub struct Post {
pub id: i32,
#[cfg(test)]
mod tests {
use crate::{
- community::*,
- post::*,
+ source::{community::*, post::*, user::*},
tests::establish_unpooled_connection,
- user::*,
ListingType,
SortType,
};
#[cfg(test)]
mod tests {
use crate::{
- private_message::*,
+ source::{private_message::*, user::*},
tests::establish_unpooled_connection,
- user::*,
ListingType,
SortType,
};
use crate::{naive_now, schema::site, Crud};
use diesel::{dsl::*, result::Error, *};
+use serde::Serialize;
-#[derive(Queryable, Identifiable, PartialEq, Debug)]
+#[derive(Queryable, Identifiable, PartialEq, Debug, Clone, Serialize)]
#[table_name = "site"]
pub struct Site {
pub id: i32,
pub deleted: bool,
}
+/// A safe representation of user, without the sensitive info
+#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize)]
+#[table_name = "user_"]
+pub struct UserSafe {
+ pub id: i32,
+ pub name: String,
+ pub preferred_username: Option<String>,
+ pub avatar: Option<String>,
+ pub admin: bool,
+ pub banned: bool,
+ pub published: chrono::NaiveDateTime,
+ pub updated: Option<chrono::NaiveDateTime>,
+ pub matrix_user_id: Option<String>,
+ pub actor_id: String,
+ pub bio: Option<String>,
+ pub local: bool,
+ pub banner: Option<String>,
+ pub deleted: bool,
+}
+
+mod safe_type {
+ use crate::{schema::user_::columns::*, source::user::User_, ToSafe};
+ type Columns = (
+ id,
+ name,
+ preferred_username,
+ avatar,
+ admin,
+ banned,
+ published,
+ updated,
+ matrix_user_id,
+ actor_id,
+ bio,
+ local,
+ banner,
+ deleted,
+ );
+
+ impl ToSafe for User_ {
+ type SafeColumns = Columns;
+ fn safe_columns_tuple() -> Self::SafeColumns {
+ (
+ id,
+ name,
+ preferred_username,
+ avatar,
+ admin,
+ banned,
+ published,
+ updated,
+ matrix_user_id,
+ actor_id,
+ bio,
+ local,
+ banner,
+ deleted,
+ )
+ }
+ }
+}
+
#[derive(Insertable, AsChangeset, Clone)]
#[table_name = "user_"]
pub struct UserForm {
#[cfg(test)]
mod tests {
- use crate::{tests::establish_unpooled_connection, user::*, ListingType, SortType};
+ use crate::{source::user::*, tests::establish_unpooled_connection, ListingType, SortType};
#[test]
fn test_crud() {
private_key: None,
public_key: None,
last_refreshed_at: inserted_user.published,
+ deleted: false,
};
let read_user = User_::read(&conn, inserted_user.id).unwrap();
#[cfg(test)]
mod tests {
use crate::{
- comment::*,
- community::*,
- post::*,
+ source::{comment::*, community::*, post::*, user::*, user_mention::*},
tests::establish_unpooled_connection,
- user::*,
- user_mention::*,
ListingType,
SortType,
};
+++ /dev/null
-use super::user_view::user_fast::BoxedQuery;
-use crate::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
-use diesel::{dsl::*, pg::Pg, result::Error, *};
-use serde::Serialize;
-
-table! {
- user_view (id) {
- id -> Int4,
- actor_id -> Text,
- name -> Varchar,
- preferred_username -> Nullable<Varchar>,
- avatar -> Nullable<Text>,
- banner -> Nullable<Text>,
- email -> Nullable<Text>,
- matrix_user_id -> Nullable<Text>,
- bio -> Nullable<Text>,
- local -> Bool,
- admin -> Bool,
- banned -> Bool,
- show_avatars -> Bool,
- send_notifications_to_email -> Bool,
- published -> Timestamp,
- number_of_posts -> BigInt,
- post_score -> BigInt,
- number_of_comments -> BigInt,
- comment_score -> BigInt,
- }
-}
-
-table! {
- user_fast (id) {
- id -> Int4,
- actor_id -> Text,
- name -> Varchar,
- preferred_username -> Nullable<Varchar>,
- avatar -> Nullable<Text>,
- banner -> Nullable<Text>,
- email -> Nullable<Text>,
- matrix_user_id -> Nullable<Text>,
- bio -> Nullable<Text>,
- local -> Bool,
- admin -> Bool,
- banned -> Bool,
- show_avatars -> Bool,
- send_notifications_to_email -> Bool,
- published -> Timestamp,
- number_of_posts -> BigInt,
- post_score -> BigInt,
- number_of_comments -> BigInt,
- comment_score -> BigInt,
- }
-}
-
-#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, QueryableByName, Clone)]
-#[table_name = "user_fast"]
-pub struct UserView {
- pub id: i32,
- pub actor_id: String,
- pub name: String,
- pub preferred_username: Option<String>,
- pub avatar: Option<String>,
- pub banner: Option<String>,
- pub email: Option<String>, // TODO this shouldn't be in this view
- pub matrix_user_id: Option<String>,
- pub bio: Option<String>,
- pub local: bool,
- pub admin: bool,
- pub banned: bool,
- pub show_avatars: bool, // TODO this is a setting, probably doesn't need to be here
- pub send_notifications_to_email: bool, // TODO also never used
- pub published: chrono::NaiveDateTime,
- pub number_of_posts: i64,
- pub post_score: i64,
- pub number_of_comments: i64,
- pub comment_score: i64,
-}
-
-pub struct UserQueryBuilder<'a> {
- conn: &'a PgConnection,
- query: BoxedQuery<'a, Pg>,
- sort: &'a SortType,
- page: Option<i64>,
- limit: Option<i64>,
-}
-
-impl<'a> UserQueryBuilder<'a> {
- pub fn create(conn: &'a PgConnection) -> Self {
- use super::user_view::user_fast::dsl::*;
-
- let query = user_fast.into_boxed();
-
- UserQueryBuilder {
- conn,
- query,
- sort: &SortType::Hot,
- page: None,
- limit: None,
- }
- }
-
- pub fn sort(mut self, sort: &'a SortType) -> Self {
- self.sort = sort;
- self
- }
-
- pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
- use super::user_view::user_fast::dsl::*;
- if let Some(search_term) = search_term.get_optional() {
- self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
- }
- self
- }
-
- pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
- self.page = page.get_optional();
- self
- }
-
- pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
- self.limit = limit.get_optional();
- self
- }
-
- pub fn list(self) -> Result<Vec<UserView>, Error> {
- use super::user_view::user_fast::dsl::*;
- use diesel::sql_types::{Nullable, Text};
-
- let mut query = self.query;
-
- query = match self.sort {
- SortType::Hot => query
- .order_by(comment_score.desc())
- .then_order_by(published.desc()),
- SortType::Active => query
- .order_by(comment_score.desc())
- .then_order_by(published.desc()),
- SortType::New => query.order_by(published.desc()),
- SortType::TopAll => query.order_by(comment_score.desc()),
- SortType::TopYear => query
- .filter(published.gt(now - 1.years()))
- .order_by(comment_score.desc()),
- SortType::TopMonth => query
- .filter(published.gt(now - 1.months()))
- .order_by(comment_score.desc()),
- SortType::TopWeek => query
- .filter(published.gt(now - 1.weeks()))
- .order_by(comment_score.desc()),
- SortType::TopDay => query
- .filter(published.gt(now - 1.days()))
- .order_by(comment_score.desc()),
- };
-
- let (limit, offset) = limit_and_offset(self.page, self.limit);
- query = query.limit(limit).offset(offset);
-
- // The select is necessary here to not get back emails
- query = query.select((
- id,
- actor_id,
- name,
- preferred_username,
- avatar,
- banner,
- "".into_sql::<Nullable<Text>>(),
- matrix_user_id,
- bio,
- local,
- admin,
- banned,
- show_avatars,
- send_notifications_to_email,
- published,
- number_of_posts,
- post_score,
- number_of_comments,
- comment_score,
- ));
- query.load::<UserView>(self.conn)
- }
-}
-
-impl UserView {
- pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
- use super::user_view::user_fast::dsl::*;
- use diesel::sql_types::{Nullable, Text};
- user_fast
- // The select is necessary here to not get back emails
- .select((
- id,
- actor_id,
- name,
- preferred_username,
- avatar,
- banner,
- "".into_sql::<Nullable<Text>>(),
- matrix_user_id,
- bio,
- local,
- admin,
- banned,
- show_avatars,
- send_notifications_to_email,
- published,
- number_of_posts,
- post_score,
- number_of_comments,
- comment_score,
- ))
- .filter(admin.eq(true))
- .order_by(published)
- .load::<Self>(conn)
- }
-
- pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
- use super::user_view::user_fast::dsl::*;
- use diesel::sql_types::{Nullable, Text};
- user_fast
- .select((
- id,
- actor_id,
- name,
- preferred_username,
- avatar,
- banner,
- "".into_sql::<Nullable<Text>>(),
- matrix_user_id,
- bio,
- local,
- admin,
- banned,
- show_avatars,
- send_notifications_to_email,
- published,
- number_of_posts,
- post_score,
- number_of_comments,
- comment_score,
- ))
- .filter(banned.eq(true))
- .load::<Self>(conn)
- }
-
- // WARNING!!! this method WILL return sensitive user information and should only be called
- // if the user requesting these details is also the authenticated user.
- // please use get_user_secure to obtain user rows in most cases.
- pub fn get_user_dangerous(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
- use super::user_view::user_fast::dsl::*;
- user_fast.find(user_id).first::<Self>(conn)
- }
-
- pub fn get_user_secure(conn: &PgConnection, user_id: i32) -> Result<Self, Error> {
- use super::user_view::user_fast::dsl::*;
- use diesel::sql_types::{Nullable, Text};
- user_fast
- .select((
- id,
- actor_id,
- name,
- preferred_username,
- avatar,
- banner,
- "".into_sql::<Nullable<Text>>(),
- matrix_user_id,
- bio,
- local,
- admin,
- banned,
- show_avatars,
- send_notifications_to_email,
- published,
- number_of_posts,
- post_score,
- number_of_comments,
- comment_score,
- ))
- .find(user_id)
- .first::<Self>(conn)
- }
-}
--- /dev/null
+use crate::{
+ schema::{community, community_follower, user_},
+ source::{
+ community::{Community, CommunitySafe},
+ user::{UserSafe, User_},
+ },
+ views::ViewToVec,
+ ToSafe,
+};
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct CommunityFollowerView {
+ pub community: CommunitySafe,
+ pub follower: UserSafe,
+}
+
+type CommunityFollowerViewTuple = (CommunitySafe, UserSafe);
+
+impl CommunityFollowerView {
+ pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result<Vec<Self>, Error> {
+ let res = community_follower::table
+ .inner_join(community::table)
+ .inner_join(user_::table)
+ .select((Community::safe_columns_tuple(), User_::safe_columns_tuple()))
+ .filter(community_follower::community_id.eq(for_community_id))
+ .order_by(community_follower::published)
+ .load::<CommunityFollowerViewTuple>(conn)?;
+
+ Ok(Self::to_vec(res))
+ }
+
+ pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result<Vec<Self>, Error> {
+ let res = community_follower::table
+ .inner_join(community::table)
+ .inner_join(user_::table)
+ .select((Community::safe_columns_tuple(), User_::safe_columns_tuple()))
+ .filter(community_follower::user_id.eq(for_user_id))
+ .order_by(community_follower::published)
+ .load::<CommunityFollowerViewTuple>(conn)?;
+
+ Ok(Self::to_vec(res))
+ }
+}
+
+impl ViewToVec for CommunityFollowerView {
+ type DbTuple = CommunityFollowerViewTuple;
+ fn to_vec(users: Vec<Self::DbTuple>) -> Vec<Self> {
+ users
+ .iter()
+ .map(|a| Self {
+ community: a.0.to_owned(),
+ follower: a.1.to_owned(),
+ })
+ .collect::<Vec<Self>>()
+ }
+}
--- /dev/null
+use crate::{
+ schema::{community, community_moderator, user_},
+ source::{
+ community::{Community, CommunitySafe},
+ user::{UserSafe, User_},
+ },
+ views::ViewToVec,
+ ToSafe,
+};
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct CommunityModeratorView {
+ pub community: CommunitySafe,
+ pub moderator: UserSafe,
+}
+
+type CommunityModeratorViewTuple = (CommunitySafe, UserSafe);
+
+impl CommunityModeratorView {
+ pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result<Vec<Self>, Error> {
+ let res = community_moderator::table
+ .inner_join(community::table)
+ .inner_join(user_::table)
+ .select((Community::safe_columns_tuple(), User_::safe_columns_tuple()))
+ .filter(community_moderator::community_id.eq(for_community_id))
+ .order_by(community_moderator::published)
+ .load::<CommunityModeratorViewTuple>(conn)?;
+
+ Ok(Self::to_vec(res))
+ }
+
+ pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result<Vec<Self>, Error> {
+ let res = community_moderator::table
+ .inner_join(community::table)
+ .inner_join(user_::table)
+ .select((Community::safe_columns_tuple(), User_::safe_columns_tuple()))
+ .filter(community_moderator::user_id.eq(for_user_id))
+ .order_by(community_moderator::published)
+ .load::<CommunityModeratorViewTuple>(conn)?;
+
+ Ok(Self::to_vec(res))
+ }
+}
+
+impl ViewToVec for CommunityModeratorView {
+ type DbTuple = CommunityModeratorViewTuple;
+ fn to_vec(community_moderators: Vec<Self::DbTuple>) -> Vec<Self> {
+ community_moderators
+ .iter()
+ .map(|a| Self {
+ community: a.0.to_owned(),
+ moderator: a.1.to_owned(),
+ })
+ .collect::<Vec<Self>>()
+ }
+}
--- /dev/null
+use crate::{
+ schema::{community, community_user_ban, user_},
+ source::{
+ community::{Community, CommunitySafe},
+ user::{UserSafe, User_},
+ },
+ ToSafe,
+};
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct CommunityUserBanView {
+ pub community: CommunitySafe,
+ pub user: UserSafe,
+}
+
+impl CommunityUserBanView {
+ pub fn get(
+ conn: &PgConnection,
+ from_user_id: i32,
+ from_community_id: i32,
+ ) -> Result<Self, Error> {
+ let (community, user) = community_user_ban::table
+ .inner_join(community::table)
+ .inner_join(user_::table)
+ .select((Community::safe_columns_tuple(), User_::safe_columns_tuple()))
+ .filter(community_user_ban::community_id.eq(from_community_id))
+ .filter(community_user_ban::user_id.eq(from_user_id))
+ .order_by(community_user_ban::published)
+ .first::<(CommunitySafe, UserSafe)>(conn)?;
+
+ Ok(CommunityUserBanView { community, user })
+ }
+}
--- /dev/null
+use crate::{
+ aggregates::community_aggregates::CommunityAggregates,
+ functions::hot_rank,
+ fuzzy_search,
+ limit_and_offset,
+ schema::{category, community, community_aggregates, community_follower, user_},
+ source::{
+ category::Category,
+ community::{Community, CommunityFollower, CommunitySafe},
+ user::{UserSafe, User_},
+ },
+ views::ViewToVec,
+ MaybeOptional,
+ SortType,
+ ToSafe,
+};
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct CommunityView {
+ pub community: CommunitySafe,
+ pub creator: UserSafe,
+ pub category: Category,
+ pub subscribed: bool,
+ pub counts: CommunityAggregates,
+}
+
+type CommunityViewTuple = (
+ CommunitySafe,
+ UserSafe,
+ Category,
+ CommunityAggregates,
+ Option<CommunityFollower>,
+);
+
+impl CommunityView {
+ pub fn read(
+ conn: &PgConnection,
+ community_id: i32,
+ my_user_id: Option<i32>,
+ ) -> Result<Self, Error> {
+ // The left join below will return None in this case
+ let user_id_join = my_user_id.unwrap_or(-1);
+
+ let (community, creator, category, counts, follower) = community::table
+ .find(community_id)
+ .inner_join(user_::table)
+ .inner_join(category::table)
+ .inner_join(community_aggregates::table)
+ .left_join(
+ community_follower::table.on(
+ community::id
+ .eq(community_follower::community_id)
+ .and(community_follower::user_id.eq(user_id_join)),
+ ),
+ )
+ .select((
+ Community::safe_columns_tuple(),
+ User_::safe_columns_tuple(),
+ category::all_columns,
+ community_aggregates::all_columns,
+ community_follower::all_columns.nullable(),
+ ))
+ .first::<CommunityViewTuple>(conn)?;
+
+ Ok(CommunityView {
+ community,
+ creator,
+ category,
+ subscribed: follower.is_some(),
+ counts,
+ })
+ }
+}
+
+mod join_types {
+ use crate::schema::{category, community, community_aggregates, community_follower, user_};
+ use diesel::{
+ pg::Pg,
+ query_builder::BoxedSelectStatement,
+ query_source::joins::{Inner, Join, JoinOn, LeftOuter},
+ sql_types::*,
+ };
+
+ /// TODO awful, but necessary because of the boxed join
+ pub(super) type BoxedCommunityJoin<'a> = BoxedSelectStatement<
+ 'a,
+ (
+ (
+ Integer,
+ Text,
+ Text,
+ Nullable<Text>,
+ Integer,
+ Integer,
+ Bool,
+ Timestamp,
+ Nullable<Timestamp>,
+ Bool,
+ Bool,
+ Text,
+ Bool,
+ Nullable<Text>,
+ Nullable<Text>,
+ ),
+ (
+ Integer,
+ Text,
+ Nullable<Text>,
+ Nullable<Text>,
+ Bool,
+ Bool,
+ Timestamp,
+ Nullable<Timestamp>,
+ Nullable<Text>,
+ Text,
+ Nullable<Text>,
+ Bool,
+ Nullable<Text>,
+ Bool,
+ ),
+ (Integer, Text),
+ (Integer, Integer, BigInt, BigInt, BigInt),
+ Nullable<(Integer, Integer, Integer, Timestamp, Nullable<Bool>)>,
+ ),
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<community::table, user_::table, Inner>,
+ diesel::expression::operators::Eq<
+ diesel::expression::nullable::Nullable<community::columns::creator_id>,
+ diesel::expression::nullable::Nullable<user_::columns::id>,
+ >,
+ >,
+ category::table,
+ Inner,
+ >,
+ diesel::expression::operators::Eq<
+ diesel::expression::nullable::Nullable<community::columns::category_id>,
+ diesel::expression::nullable::Nullable<category::columns::id>,
+ >,
+ >,
+ community_aggregates::table,
+ Inner,
+ >,
+ diesel::expression::operators::Eq<
+ diesel::expression::nullable::Nullable<community_aggregates::columns::community_id>,
+ diesel::expression::nullable::Nullable<community::columns::id>,
+ >,
+ >,
+ community_follower::table,
+ LeftOuter,
+ >,
+ diesel::expression::operators::And<
+ diesel::expression::operators::Eq<
+ community::columns::id,
+ community_follower::columns::community_id,
+ >,
+ diesel::expression::operators::Eq<
+ community_follower::columns::user_id,
+ diesel::expression::bound::Bound<diesel::sql_types::Integer, i32>,
+ >,
+ >,
+ >,
+ Pg,
+ >;
+}
+
+pub struct CommunityQueryBuilder<'a> {
+ conn: &'a PgConnection,
+ query: join_types::BoxedCommunityJoin<'a>,
+ sort: &'a SortType,
+ show_nsfw: bool,
+ search_term: Option<String>,
+ page: Option<i64>,
+ limit: Option<i64>,
+}
+
+impl<'a> CommunityQueryBuilder<'a> {
+ pub fn create(conn: &'a PgConnection, my_user_id: Option<i32>) -> Self {
+ // The left join below will return None in this case
+ let user_id_join = my_user_id.unwrap_or(-1);
+
+ let query = community::table
+ .inner_join(user_::table)
+ .inner_join(category::table)
+ .inner_join(community_aggregates::table)
+ .left_join(
+ community_follower::table.on(
+ community::id
+ .eq(community_follower::community_id)
+ .and(community_follower::user_id.eq(user_id_join)),
+ ),
+ )
+ .select((
+ Community::safe_columns_tuple(),
+ User_::safe_columns_tuple(),
+ category::all_columns,
+ community_aggregates::all_columns,
+ community_follower::all_columns.nullable(),
+ ))
+ .into_boxed();
+
+ CommunityQueryBuilder {
+ conn,
+ query,
+ sort: &SortType::Hot,
+ show_nsfw: true,
+ search_term: None,
+ page: None,
+ limit: None,
+ }
+ }
+
+ pub fn sort(mut self, sort: &'a SortType) -> Self {
+ self.sort = sort;
+ self
+ }
+
+ pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
+ self.show_nsfw = show_nsfw;
+ self
+ }
+
+ pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
+ self.search_term = search_term.get_optional();
+ self
+ }
+
+ pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
+ self.page = page.get_optional();
+ self
+ }
+
+ pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
+ self.limit = limit.get_optional();
+ self
+ }
+
+ pub fn list(self) -> Result<Vec<CommunityView>, Error> {
+ let mut query = self.query;
+
+ if let Some(search_term) = self.search_term {
+ let searcher = fuzzy_search(&search_term);
+ query = query
+ .filter(community::name.ilike(searcher.to_owned()))
+ .or_filter(community::title.ilike(searcher.to_owned()))
+ .or_filter(community::description.ilike(searcher));
+ };
+
+ match self.sort {
+ SortType::New => query = query.order_by(community::published.desc()),
+ SortType::TopAll => query = query.order_by(community_aggregates::subscribers.desc()),
+ // Covers all other sorts, including hot
+ _ => {
+ query = query
+ // TODO do custom sql function for hot_rank, make sure this works
+ .order_by(hot_rank(community_aggregates::subscribers, community::published).desc())
+ .then_order_by(community_aggregates::subscribers.desc())
+ }
+ };
+
+ if !self.show_nsfw {
+ query = query.filter(community::nsfw.eq(false));
+ };
+
+ let (limit, offset) = limit_and_offset(self.page, self.limit);
+ let res = query
+ .limit(limit)
+ .offset(offset)
+ .filter(community::removed.eq(false))
+ .filter(community::deleted.eq(false))
+ .load::<CommunityViewTuple>(self.conn)?;
+
+ Ok(CommunityView::to_vec(res))
+ }
+}
+
+impl ViewToVec for CommunityView {
+ type DbTuple = CommunityViewTuple;
+ fn to_vec(communities: Vec<Self::DbTuple>) -> Vec<Self> {
+ communities
+ .iter()
+ .map(|a| Self {
+ community: a.0.to_owned(),
+ creator: a.1.to_owned(),
+ category: a.2.to_owned(),
+ counts: a.3.to_owned(),
+ subscribed: a.4.is_some(),
+ })
+ .collect::<Vec<Self>>()
+ }
+}
--- /dev/null
+pub mod community_follower_view;
+pub mod community_moderator_view;
+pub mod community_user_ban_view;
+pub mod community_view;
+pub mod post_view;
+pub mod site_view;
+pub mod user_view;
+
+pub(crate) trait ViewToVec {
+ type DbTuple;
+ fn to_vec(tuple: Vec<Self::DbTuple>) -> Vec<Self>
+ where
+ Self: Sized;
+}
--- /dev/null
+use crate::{
+ aggregates::post_aggregates::PostAggregates,
+ functions::hot_rank,
+ fuzzy_search,
+ limit_and_offset,
+ schema::{
+ community,
+ community_follower,
+ community_user_ban,
+ post,
+ post_aggregates,
+ post_like,
+ post_read,
+ post_saved,
+ user_,
+ },
+ source::{
+ community::{Community, CommunityFollower, CommunitySafe, CommunityUserBan},
+ post::{Post, PostRead, PostSaved},
+ user::{UserSafe, User_},
+ },
+ views::ViewToVec,
+ ListingType,
+ MaybeOptional,
+ SortType,
+ ToSafe,
+};
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, PartialEq, Serialize, Clone)]
+pub struct PostView {
+ pub post: Post,
+ pub creator: UserSafe,
+ pub community: CommunitySafe,
+ pub counts: PostAggregates,
+ pub subscribed: bool, // Left join to CommunityFollower
+ pub creator_banned_from_community: bool, // Left Join to CommunityUserBan
+ pub saved: bool, // Left join to PostSaved
+ pub read: bool, // Left join to PostRead
+ pub my_vote: Option<i16>, // Left join to PostLike
+}
+
+type PostViewTuple = (
+ Post,
+ UserSafe,
+ CommunitySafe,
+ Option<CommunityUserBan>,
+ PostAggregates,
+ Option<CommunityFollower>,
+ Option<PostSaved>,
+ Option<PostRead>,
+ Option<i16>,
+);
+
+impl PostView {
+ pub fn read(conn: &PgConnection, post_id: i32, my_user_id: Option<i32>) -> Result<Self, Error> {
+ // The left join below will return None in this case
+ let user_id_join = my_user_id.unwrap_or(-1);
+
+ let (
+ post,
+ creator,
+ community,
+ creator_banned_from_community,
+ counts,
+ follower,
+ saved,
+ read,
+ my_vote,
+ ) = post::table
+ .find(post_id)
+ .inner_join(user_::table)
+ .inner_join(community::table)
+ .left_join(
+ community_user_ban::table.on(
+ post::community_id
+ .eq(community_user_ban::community_id)
+ .and(community_user_ban::user_id.eq(community::creator_id)),
+ ),
+ )
+ .inner_join(post_aggregates::table)
+ .left_join(
+ community_follower::table.on(
+ post::community_id
+ .eq(community_follower::community_id)
+ .and(community_follower::user_id.eq(user_id_join)),
+ ),
+ )
+ .left_join(
+ post_saved::table.on(
+ post::id
+ .eq(post_saved::post_id)
+ .and(post_saved::user_id.eq(user_id_join)),
+ ),
+ )
+ .left_join(
+ post_read::table.on(
+ post::id
+ .eq(post_read::post_id)
+ .and(post_read::user_id.eq(user_id_join)),
+ ),
+ )
+ .left_join(
+ post_like::table.on(
+ post::id
+ .eq(post_like::post_id)
+ .and(post_like::user_id.eq(user_id_join)),
+ ),
+ )
+ .select((
+ post::all_columns,
+ User_::safe_columns_tuple(),
+ Community::safe_columns_tuple(),
+ community_user_ban::all_columns.nullable(),
+ post_aggregates::all_columns,
+ community_follower::all_columns.nullable(),
+ post_saved::all_columns.nullable(),
+ post_read::all_columns.nullable(),
+ post_like::score.nullable(),
+ ))
+ .first::<PostViewTuple>(conn)?;
+
+ Ok(PostView {
+ post,
+ creator,
+ community,
+ creator_banned_from_community: creator_banned_from_community.is_some(),
+ counts,
+ subscribed: follower.is_some(),
+ saved: saved.is_some(),
+ read: read.is_some(),
+ my_vote,
+ })
+ }
+}
+
+mod join_types {
+ use crate::schema::{
+ community,
+ community_follower,
+ community_user_ban,
+ post,
+ post_aggregates,
+ post_like,
+ post_read,
+ post_saved,
+ user_,
+ };
+ use diesel::{
+ pg::Pg,
+ query_builder::BoxedSelectStatement,
+ query_source::joins::{Inner, Join, JoinOn, LeftOuter},
+ sql_types::*,
+ };
+
+ // /// TODO awful, but necessary because of the boxed join
+ pub(super) type BoxedPostJoin<'a> = BoxedSelectStatement<
+ 'a,
+ (
+ (
+ Integer,
+ Text,
+ Nullable<Text>,
+ Nullable<Text>,
+ Integer,
+ Integer,
+ Bool,
+ Bool,
+ Timestamp,
+ Nullable<Timestamp>,
+ Bool,
+ Bool,
+ Bool,
+ Nullable<Text>,
+ Nullable<Text>,
+ Nullable<Text>,
+ Nullable<Text>,
+ Text,
+ Bool,
+ ),
+ (
+ Integer,
+ Text,
+ Nullable<Text>,
+ Nullable<Text>,
+ Bool,
+ Bool,
+ Timestamp,
+ Nullable<Timestamp>,
+ Nullable<Text>,
+ Text,
+ Nullable<Text>,
+ Bool,
+ Nullable<Text>,
+ Bool,
+ ),
+ (
+ Integer,
+ Text,
+ Text,
+ Nullable<Text>,
+ Integer,
+ Integer,
+ Bool,
+ Timestamp,
+ Nullable<Timestamp>,
+ Bool,
+ Bool,
+ Text,
+ Bool,
+ Nullable<Text>,
+ Nullable<Text>,
+ ),
+ Nullable<(Integer, Integer, Integer, Timestamp)>,
+ (Integer, Integer, BigInt, BigInt, BigInt, BigInt, Timestamp),
+ Nullable<(Integer, Integer, Integer, Timestamp, Nullable<Bool>)>,
+ Nullable<(Integer, Integer, Integer, Timestamp)>,
+ Nullable<(Integer, Integer, Integer, Timestamp)>,
+ Nullable<SmallInt>,
+ ),
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<
+ JoinOn<
+ Join<post::table, user_::table, Inner>,
+ diesel::expression::operators::Eq<
+ diesel::expression::nullable::Nullable<
+ post::columns::creator_id,
+ >,
+ diesel::expression::nullable::Nullable<user_::columns::id>,
+ >,
+ >,
+ community::table,
+ Inner,
+ >,
+ diesel::expression::operators::Eq<
+ diesel::expression::nullable::Nullable<post::columns::community_id>,
+ diesel::expression::nullable::Nullable<community::columns::id>,
+ >,
+ >,
+ community_user_ban::table,
+ LeftOuter,
+ >,
+ diesel::expression::operators::And<
+ diesel::expression::operators::Eq<
+ post::columns::community_id,
+ community_user_ban::columns::community_id,
+ >,
+ diesel::expression::operators::Eq<
+ community_user_ban::columns::user_id,
+ community::columns::creator_id,
+ >,
+ >,
+ >,
+ post_aggregates::table,
+ Inner,
+ >,
+ diesel::expression::operators::Eq<
+ diesel::expression::nullable::Nullable<post_aggregates::columns::post_id>,
+ diesel::expression::nullable::Nullable<post::columns::id>,
+ >,
+ >,
+ community_follower::table,
+ LeftOuter,
+ >,
+ diesel::expression::operators::And<
+ diesel::expression::operators::Eq<
+ post::columns::community_id,
+ community_follower::columns::community_id,
+ >,
+ diesel::expression::operators::Eq<
+ community_follower::columns::user_id,
+ diesel::expression::bound::Bound<Integer, i32>,
+ >,
+ >,
+ >,
+ post_saved::table,
+ LeftOuter,
+ >,
+ diesel::expression::operators::And<
+ diesel::expression::operators::Eq<post::columns::id, post_saved::columns::post_id>,
+ diesel::expression::operators::Eq<
+ post_saved::columns::user_id,
+ diesel::expression::bound::Bound<Integer, i32>,
+ >,
+ >,
+ >,
+ post_read::table,
+ LeftOuter,
+ >,
+ diesel::expression::operators::And<
+ diesel::expression::operators::Eq<post::columns::id, post_read::columns::post_id>,
+ diesel::expression::operators::Eq<
+ post_read::columns::user_id,
+ diesel::expression::bound::Bound<Integer, i32>,
+ >,
+ >,
+ >,
+ post_like::table,
+ LeftOuter,
+ >,
+ diesel::expression::operators::And<
+ diesel::expression::operators::Eq<post::columns::id, post_like::columns::post_id>,
+ diesel::expression::operators::Eq<
+ post_like::columns::user_id,
+ diesel::expression::bound::Bound<Integer, i32>,
+ >,
+ >,
+ >,
+ Pg,
+ >;
+}
+
+pub struct PostQueryBuilder<'a> {
+ conn: &'a PgConnection,
+ query: join_types::BoxedPostJoin<'a>,
+ listing_type: &'a ListingType,
+ sort: &'a SortType,
+ for_creator_id: Option<i32>,
+ for_community_id: Option<i32>,
+ for_community_name: Option<String>,
+ search_term: Option<String>,
+ url_search: Option<String>,
+ show_nsfw: bool,
+ saved_only: bool,
+ unread_only: bool,
+ page: Option<i64>,
+ limit: Option<i64>,
+}
+
+impl<'a> PostQueryBuilder<'a> {
+ pub fn create(conn: &'a PgConnection, my_user_id: Option<i32>) -> Self {
+ // The left join below will return None in this case
+ let user_id_join = my_user_id.unwrap_or(-1);
+
+ let query = post::table
+ .inner_join(user_::table)
+ .inner_join(community::table)
+ .left_join(
+ community_user_ban::table.on(
+ post::community_id
+ .eq(community_user_ban::community_id)
+ .and(community_user_ban::user_id.eq(community::creator_id)),
+ ),
+ )
+ .inner_join(post_aggregates::table)
+ .left_join(
+ community_follower::table.on(
+ post::community_id
+ .eq(community_follower::community_id)
+ .and(community_follower::user_id.eq(user_id_join)),
+ ),
+ )
+ .left_join(
+ post_saved::table.on(
+ post::id
+ .eq(post_saved::post_id)
+ .and(post_saved::user_id.eq(user_id_join)),
+ ),
+ )
+ .left_join(
+ post_read::table.on(
+ post::id
+ .eq(post_read::post_id)
+ .and(post_read::user_id.eq(user_id_join)),
+ ),
+ )
+ .left_join(
+ post_like::table.on(
+ post::id
+ .eq(post_like::post_id)
+ .and(post_like::user_id.eq(user_id_join)),
+ ),
+ )
+ .select((
+ post::all_columns,
+ User_::safe_columns_tuple(),
+ Community::safe_columns_tuple(),
+ community_user_ban::all_columns.nullable(),
+ post_aggregates::all_columns,
+ community_follower::all_columns.nullable(),
+ post_saved::all_columns.nullable(),
+ post_read::all_columns.nullable(),
+ post_like::score.nullable(),
+ ))
+ .into_boxed();
+
+ PostQueryBuilder {
+ conn,
+ query,
+ listing_type: &ListingType::All,
+ sort: &SortType::Hot,
+ for_creator_id: None,
+ for_community_id: None,
+ for_community_name: None,
+ search_term: None,
+ url_search: None,
+ show_nsfw: true,
+ saved_only: false,
+ unread_only: false,
+ page: None,
+ limit: None,
+ }
+ }
+
+ pub fn listing_type(mut self, listing_type: &'a ListingType) -> Self {
+ self.listing_type = listing_type;
+ self
+ }
+
+ pub fn sort(mut self, sort: &'a SortType) -> Self {
+ self.sort = sort;
+ self
+ }
+
+ pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
+ self.for_community_id = for_community_id.get_optional();
+ self
+ }
+
+ pub fn for_community_name<T: MaybeOptional<String>>(mut self, for_community_name: T) -> Self {
+ self.for_community_name = for_community_name.get_optional();
+ self
+ }
+
+ pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
+ self.for_creator_id = for_creator_id.get_optional();
+ self
+ }
+
+ pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
+ self.search_term = search_term.get_optional();
+ self
+ }
+
+ pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
+ self.url_search = url_search.get_optional();
+ self
+ }
+
+ pub fn show_nsfw(mut self, show_nsfw: bool) -> Self {
+ self.show_nsfw = show_nsfw;
+ self
+ }
+
+ pub fn saved_only(mut self, saved_only: bool) -> Self {
+ self.saved_only = saved_only;
+ self
+ }
+
+ pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
+ self.page = page.get_optional();
+ self
+ }
+
+ pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
+ self.limit = limit.get_optional();
+ self
+ }
+
+ pub fn list(self) -> Result<Vec<PostView>, Error> {
+ use diesel::dsl::*;
+
+ let mut query = self.query;
+
+ query = match self.listing_type {
+ ListingType::Subscribed => query.filter(community_follower::user_id.is_not_null()), // TODO could be this: and(community_follower::user_id.eq(user_id_join)),
+ ListingType::Local => query.filter(community::local.eq(true)),
+ _ => query,
+ };
+
+ if let Some(for_community_id) = self.for_community_id {
+ query = query
+ .filter(post::community_id.eq(for_community_id))
+ .then_order_by(post::stickied.desc());
+ }
+
+ if let Some(for_community_name) = self.for_community_name {
+ query = query
+ .filter(community::name.eq(for_community_name))
+ .filter(community::local.eq(true))
+ .then_order_by(post::stickied.desc());
+ }
+
+ if let Some(url_search) = self.url_search {
+ query = query.filter(post::url.eq(url_search));
+ }
+
+ if let Some(search_term) = self.search_term {
+ let searcher = fuzzy_search(&search_term);
+ query = query.filter(
+ post::name
+ .ilike(searcher.to_owned())
+ .or(post::body.ilike(searcher)),
+ );
+ }
+
+ query = match self.sort {
+ SortType::Active => query
+ .then_order_by(
+ hot_rank(post_aggregates::score, post_aggregates::newest_comment_time).desc(),
+ )
+ .then_order_by(post::published.desc()),
+ SortType::Hot => query
+ .then_order_by(hot_rank(post_aggregates::score, post::published).desc())
+ .then_order_by(post::published.desc()),
+ SortType::New => query.then_order_by(post::published.desc()),
+ SortType::TopAll => query.then_order_by(post_aggregates::score.desc()),
+ SortType::TopYear => query
+ .filter(post::published.gt(now - 1.years()))
+ .then_order_by(post_aggregates::score.desc()),
+ SortType::TopMonth => query
+ .filter(post::published.gt(now - 1.months()))
+ .then_order_by(post_aggregates::score.desc()),
+ SortType::TopWeek => query
+ .filter(post::published.gt(now - 1.weeks()))
+ .then_order_by(post_aggregates::score.desc()),
+ SortType::TopDay => query
+ .filter(post::published.gt(now - 1.days()))
+ .then_order_by(post_aggregates::score.desc()),
+ };
+
+ // If its for a specific user, show the removed / deleted
+ if let Some(for_creator_id) = self.for_creator_id {
+ query = query.filter(post::creator_id.eq(for_creator_id));
+ }
+
+ if !self.show_nsfw {
+ query = query
+ .filter(post::nsfw.eq(false))
+ .filter(community::nsfw.eq(false));
+ };
+
+ // TODO These two might be wrong
+ if self.saved_only {
+ query = query.filter(post_saved::id.is_not_null());
+ };
+
+ if self.unread_only {
+ query = query.filter(post_read::id.is_not_null());
+ };
+
+ let (limit, offset) = limit_and_offset(self.page, self.limit);
+
+ let res = query
+ .limit(limit)
+ .offset(offset)
+ .filter(post::removed.eq(false))
+ .filter(post::deleted.eq(false))
+ .filter(community::removed.eq(false))
+ .filter(community::deleted.eq(false))
+ .load::<PostViewTuple>(self.conn)?;
+
+ Ok(PostView::to_vec(res))
+ }
+}
+
+impl ViewToVec for PostView {
+ type DbTuple = PostViewTuple;
+ fn to_vec(posts: Vec<Self::DbTuple>) -> Vec<Self> {
+ posts
+ .iter()
+ .map(|a| Self {
+ post: a.0.to_owned(),
+ creator: a.1.to_owned(),
+ community: a.2.to_owned(),
+ creator_banned_from_community: a.3.is_some(),
+ counts: a.4.to_owned(),
+ subscribed: a.5.is_some(),
+ saved: a.6.is_some(),
+ read: a.7.is_some(),
+ my_vote: a.8,
+ })
+ .collect::<Vec<Self>>()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{
+ aggregates::post_aggregates::PostAggregates,
+ source::{community::*, post::*, user::*},
+ tests::establish_unpooled_connection,
+ views::post_view::{PostQueryBuilder, PostView},
+ Crud,
+ Likeable,
+ *,
+ };
+
+ #[test]
+ fn test_crud() {
+ let conn = establish_unpooled_connection();
+
+ let user_name = "tegan".to_string();
+ let community_name = "test_community_3".to_string();
+ let post_name = "test post 3".to_string();
+
+ let new_user = UserForm {
+ name: user_name.to_owned(),
+ preferred_username: None,
+ password_encrypted: "nope".into(),
+ email: None,
+ matrix_user_id: None,
+ avatar: None,
+ banner: None,
+ published: None,
+ updated: None,
+ admin: false,
+ banned: Some(false),
+ show_nsfw: false,
+ theme: "browser".into(),
+ default_sort_type: SortType::Hot as i16,
+ default_listing_type: ListingType::Subscribed as i16,
+ lang: "browser".into(),
+ show_avatars: true,
+ send_notifications_to_email: false,
+ actor_id: None,
+ bio: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ };
+
+ let inserted_user = User_::create(&conn, &new_user).unwrap();
+
+ let new_community = CommunityForm {
+ name: community_name.to_owned(),
+ title: "nada".to_owned(),
+ description: None,
+ creator_id: inserted_user.id,
+ category_id: 1,
+ removed: None,
+ deleted: None,
+ updated: None,
+ nsfw: false,
+ actor_id: None,
+ local: true,
+ private_key: None,
+ public_key: None,
+ last_refreshed_at: None,
+ published: None,
+ icon: None,
+ banner: None,
+ };
+
+ let inserted_community = Community::create(&conn, &new_community).unwrap();
+
+ let new_post = PostForm {
+ name: post_name.to_owned(),
+ url: None,
+ body: None,
+ creator_id: inserted_user.id,
+ community_id: inserted_community.id,
+ removed: None,
+ deleted: None,
+ locked: None,
+ stickied: None,
+ updated: None,
+ nsfw: false,
+ embed_title: None,
+ embed_description: None,
+ embed_html: None,
+ thumbnail_url: None,
+ ap_id: None,
+ local: true,
+ published: None,
+ };
+
+ let inserted_post = Post::create(&conn, &new_post).unwrap();
+
+ let post_like_form = PostLikeForm {
+ post_id: inserted_post.id,
+ user_id: inserted_user.id,
+ score: 1,
+ };
+
+ let inserted_post_like = PostLike::like(&conn, &post_like_form).unwrap();
+
+ let expected_post_like = PostLike {
+ id: inserted_post_like.id,
+ post_id: inserted_post.id,
+ user_id: inserted_user.id,
+ published: inserted_post_like.published,
+ score: 1,
+ };
+
+ let read_post_listings_with_user = PostQueryBuilder::create(&conn, Some(inserted_user.id))
+ .listing_type(&ListingType::Community)
+ .sort(&SortType::New)
+ .for_community_id(inserted_community.id)
+ .list()
+ .unwrap();
+
+ let read_post_listings_no_user = PostQueryBuilder::create(&conn, None)
+ .listing_type(&ListingType::Community)
+ .sort(&SortType::New)
+ .for_community_id(inserted_community.id)
+ .list()
+ .unwrap();
+
+ let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
+ let read_post_listing_with_user =
+ PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
+
+ // the non user version
+ let expected_post_listing_no_user = PostView {
+ post: Post {
+ id: inserted_post.id,
+ name: post_name.to_owned(),
+ creator_id: inserted_user.id,
+ url: None,
+ body: None,
+ published: inserted_post.published,
+ updated: None,
+ community_id: inserted_community.id,
+ removed: false,
+ deleted: false,
+ locked: false,
+ stickied: false,
+ nsfw: false,
+ embed_title: None,
+ embed_description: None,
+ embed_html: None,
+ thumbnail_url: None,
+ ap_id: inserted_post.ap_id.to_owned(),
+ local: true,
+ },
+ my_vote: None,
+ creator: UserSafe {
+ id: inserted_user.id,
+ name: user_name.to_owned(),
+ preferred_username: None,
+ published: inserted_user.published,
+ avatar: None,
+ actor_id: inserted_user.actor_id.to_owned(),
+ local: true,
+ banned: false,
+ deleted: false,
+ bio: None,
+ banner: None,
+ admin: false,
+ updated: None,
+ matrix_user_id: None,
+ },
+ creator_banned_from_community: false,
+ community: CommunitySafe {
+ id: inserted_community.id,
+ name: community_name.to_owned(),
+ icon: None,
+ removed: false,
+ deleted: false,
+ nsfw: false,
+ actor_id: inserted_community.actor_id.to_owned(),
+ local: true,
+ title: "nada".to_owned(),
+ description: None,
+ creator_id: inserted_user.id,
+ category_id: 1,
+ updated: None,
+ banner: None,
+ published: inserted_community.published,
+ },
+ counts: PostAggregates {
+ id: inserted_post.id, // TODO this might fail
+ post_id: inserted_post.id,
+ comments: 0,
+ score: 1,
+ upvotes: 1,
+ downvotes: 0,
+ newest_comment_time: inserted_post.published,
+ },
+ subscribed: false,
+ read: false,
+ saved: false,
+ };
+
+ // TODO More needs to be added here
+ let mut expected_post_listing_with_user = expected_post_listing_no_user.to_owned();
+ expected_post_listing_with_user.my_vote = Some(1);
+
+ let like_removed = PostLike::remove(&conn, inserted_user.id, inserted_post.id).unwrap();
+ let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
+ Community::delete(&conn, inserted_community.id).unwrap();
+ User_::delete(&conn, inserted_user.id).unwrap();
+
+ // The with user
+ assert_eq!(
+ expected_post_listing_with_user,
+ read_post_listings_with_user[0]
+ );
+ assert_eq!(expected_post_listing_with_user, read_post_listing_with_user);
+ assert_eq!(1, read_post_listings_with_user.len());
+
+ // Without the user
+ assert_eq!(expected_post_listing_no_user, read_post_listings_no_user[0]);
+ assert_eq!(expected_post_listing_no_user, read_post_listing_no_user);
+ assert_eq!(1, read_post_listings_no_user.len());
+
+ // assert_eq!(expected_post, inserted_post);
+ // assert_eq!(expected_post, updated_post);
+ assert_eq!(expected_post_like, inserted_post_like);
+ assert_eq!(1, like_removed);
+ assert_eq!(1, num_deleted);
+ }
+}
--- /dev/null
+use crate::{
+ schema::{site, user_},
+ source::{
+ site::Site,
+ user::{UserSafe, User_},
+ },
+ ToSafe,
+};
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct SiteView {
+ pub site: Site,
+ pub creator: UserSafe,
+}
+
+impl SiteView {
+ pub fn read(conn: &PgConnection) -> Result<Self, Error> {
+ let (site, creator) = site::table
+ .inner_join(user_::table)
+ .select((site::all_columns, User_::safe_columns_tuple()))
+ .first::<(Site, UserSafe)>(conn)?;
+
+ Ok(SiteView { site, creator })
+ }
+}
--- /dev/null
+use crate::{
+ aggregates::user_aggregates::UserAggregates,
+ fuzzy_search,
+ limit_and_offset,
+ schema::{user_, user_aggregates},
+ source::user::{UserSafe, User_},
+ views::ViewToVec,
+ MaybeOptional,
+ SortType,
+ ToSafe,
+};
+use diesel::{dsl::*, result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct UserViewSafe {
+ pub user: UserSafe,
+ pub counts: UserAggregates,
+}
+
+type UserViewSafeTuple = (UserSafe, UserAggregates);
+
+#[derive(Debug, Serialize, Clone)]
+pub struct UserViewDangerous {
+ pub user: User_,
+ pub counts: UserAggregates,
+}
+
+type UserViewDangerousTuple = (User_, UserAggregates);
+
+impl UserViewDangerous {
+ pub fn read(conn: &PgConnection, id: i32) -> Result<Self, Error> {
+ let (user, counts) = user_::table
+ .find(id)
+ .inner_join(user_aggregates::table)
+ .first::<UserViewDangerousTuple>(conn)?;
+ Ok(Self { user, counts })
+ }
+}
+
+impl UserViewSafe {
+ pub fn read(conn: &PgConnection, id: i32) -> Result<Self, Error> {
+ let (user, counts) = user_::table
+ .find(id)
+ .inner_join(user_aggregates::table)
+ .select((User_::safe_columns_tuple(), user_aggregates::all_columns))
+ .first::<UserViewSafeTuple>(conn)?;
+ Ok(Self { user, counts })
+ }
+
+ pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
+ let admins = user_::table
+ .inner_join(user_aggregates::table)
+ .select((User_::safe_columns_tuple(), user_aggregates::all_columns))
+ .filter(user_::admin.eq(true))
+ .order_by(user_::published)
+ .load::<UserViewSafeTuple>(conn)?;
+
+ Ok(Self::to_vec(admins))
+ }
+
+ pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
+ let banned = user_::table
+ .inner_join(user_aggregates::table)
+ .select((User_::safe_columns_tuple(), user_aggregates::all_columns))
+ .filter(user_::banned.eq(true))
+ .load::<UserViewSafeTuple>(conn)?;
+
+ Ok(Self::to_vec(banned))
+ }
+}
+
+mod join_types {
+ use crate::schema::{user_, user_aggregates};
+ use diesel::{
+ pg::Pg,
+ query_builder::BoxedSelectStatement,
+ query_source::joins::{Inner, Join, JoinOn},
+ sql_types::*,
+ };
+
+ /// TODO awful, but necessary because of the boxed join
+ pub(super) type BoxedUserJoin<'a> = BoxedSelectStatement<
+ 'a,
+ (
+ // UserSafe column types
+ (
+ Integer,
+ Text,
+ Nullable<Text>,
+ Nullable<Text>,
+ Bool,
+ Bool,
+ Timestamp,
+ Nullable<Timestamp>,
+ Nullable<Text>,
+ Text,
+ Nullable<Text>,
+ Bool,
+ Nullable<Text>,
+ Bool,
+ ),
+ // UserAggregates column types
+ (Integer, Integer, BigInt, BigInt, BigInt, BigInt),
+ ),
+ JoinOn<
+ Join<user_::table, user_aggregates::table, Inner>,
+ diesel::expression::operators::Eq<
+ diesel::expression::nullable::Nullable<user_aggregates::columns::user_id>,
+ diesel::expression::nullable::Nullable<user_::columns::id>,
+ >,
+ >,
+ Pg,
+ >;
+}
+
+pub struct UserQueryBuilder<'a> {
+ conn: &'a PgConnection,
+ query: join_types::BoxedUserJoin<'a>,
+ sort: &'a SortType,
+ page: Option<i64>,
+ limit: Option<i64>,
+}
+
+impl<'a> UserQueryBuilder<'a> {
+ pub fn create(conn: &'a PgConnection) -> Self {
+ let query = user_::table
+ .inner_join(user_aggregates::table)
+ .select((User_::safe_columns_tuple(), user_aggregates::all_columns))
+ .into_boxed();
+
+ UserQueryBuilder {
+ conn,
+ query,
+ sort: &SortType::Hot,
+ page: None,
+ limit: None,
+ }
+ }
+
+ pub fn sort(mut self, sort: &'a SortType) -> Self {
+ self.sort = sort;
+ self
+ }
+
+ pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
+ if let Some(search_term) = search_term.get_optional() {
+ self.query = self
+ .query
+ .filter(user_::name.ilike(fuzzy_search(&search_term)));
+ }
+ self
+ }
+
+ pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
+ self.page = page.get_optional();
+ self
+ }
+
+ pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
+ self.limit = limit.get_optional();
+ self
+ }
+
+ pub fn list(self) -> Result<Vec<UserViewSafe>, Error> {
+ let mut query = self.query;
+
+ query = match self.sort {
+ SortType::Hot => query
+ .order_by(user_aggregates::comment_score.desc())
+ .then_order_by(user_::published.desc()),
+ SortType::Active => query
+ .order_by(user_aggregates::comment_score.desc())
+ .then_order_by(user_::published.desc()),
+ SortType::New => query.order_by(user_::published.desc()),
+ SortType::TopAll => query.order_by(user_aggregates::comment_score.desc()),
+ SortType::TopYear => query
+ .filter(user_::published.gt(now - 1.years()))
+ .order_by(user_aggregates::comment_score.desc()),
+ SortType::TopMonth => query
+ .filter(user_::published.gt(now - 1.months()))
+ .order_by(user_aggregates::comment_score.desc()),
+ SortType::TopWeek => query
+ .filter(user_::published.gt(now - 1.weeks()))
+ .order_by(user_aggregates::comment_score.desc()),
+ SortType::TopDay => query
+ .filter(user_::published.gt(now - 1.days()))
+ .order_by(user_aggregates::comment_score.desc()),
+ };
+
+ let (limit, offset) = limit_and_offset(self.page, self.limit);
+ query = query.limit(limit).offset(offset);
+
+ let res = query.load::<UserViewSafeTuple>(self.conn)?;
+
+ Ok(UserViewSafe::to_vec(res))
+ }
+}
+
+impl ViewToVec for UserViewSafe {
+ type DbTuple = UserViewSafeTuple;
+ fn to_vec(users: Vec<Self::DbTuple>) -> Vec<Self> {
+ users
+ .iter()
+ .map(|a| Self {
+ user: a.0.to_owned(),
+ counts: a.1.to_owned(),
+ })
+ .collect::<Vec<Self>>()
+ }
+}
-use lemmy_db::{
- community_view::{CommunityFollowerView, CommunityModeratorView, CommunityView},
- user_view::UserView,
+use lemmy_db::views::{
+ community_follower_view::CommunityFollowerView,
+ community_moderator_view::CommunityModeratorView,
+ community_view::CommunityView,
+ user_view::UserViewSafe,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct GetCommunityResponse {
- pub community: CommunityView,
+ pub community_view: CommunityView,
pub moderators: Vec<CommunityModeratorView>,
pub online: usize,
}
#[derive(Serialize, Clone)]
pub struct CommunityResponse {
- pub community: CommunityView,
+ pub community_view: CommunityView,
}
#[derive(Deserialize, Debug)]
#[derive(Serialize, Clone)]
pub struct BanFromCommunityResponse {
- pub user: UserView,
+ pub user_view: UserViewSafe,
pub banned: bool,
}
use diesel::PgConnection;
use lemmy_db::{
- comment::Comment,
- post::Post,
- user::User_,
- user_mention::{UserMention, UserMentionForm},
+ source::{
+ comment::Comment,
+ post::Post,
+ user::User_,
+ user_mention::{UserMention, UserMentionForm},
+ },
Crud,
DbPool,
};
use lemmy_db::{
comment_view::CommentView,
- community_view::{CommunityModeratorView, CommunityView},
post_report::PostReportView,
- post_view::PostView,
+ views::{
+ community_moderator_view::CommunityModeratorView,
+ community_view::CommunityView,
+ post_view::PostView,
+ },
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Clone)]
pub struct PostResponse {
- pub post: PostView,
+ pub post_view: PostView,
}
#[derive(Deserialize)]
#[derive(Serialize)]
pub struct GetPostResponse {
- pub post: PostView,
+ pub post_view: PostView,
pub comments: Vec<CommentView>,
pub community: CommunityView,
pub moderators: Vec<CommunityModeratorView>,
use lemmy_db::{
- category::*,
+ aggregates::site_aggregates::SiteAggregates,
comment_view::*,
- community_view::*,
moderator_views::*,
- post_view::*,
- site_view::*,
- user::*,
- user_view::*,
+ source::{category::*, user::*},
+ views::{
+ community_view::CommunityView,
+ post_view::PostView,
+ site_view::SiteView,
+ user_view::UserViewSafe,
+ },
};
use serde::{Deserialize, Serialize};
pub comments: Vec<CommentView>,
pub posts: Vec<PostView>,
pub communities: Vec<CommunityView>,
- pub users: Vec<UserView>,
+ pub users: Vec<UserViewSafe>,
}
#[derive(Deserialize)]
pub auth: Option<String>,
}
+// TODO combine siteresponse and getsiteresponse
#[derive(Serialize, Clone)]
pub struct SiteResponse {
pub site: SiteView,
#[derive(Serialize)]
pub struct GetSiteResponse {
- pub site: Option<SiteView>,
- pub admins: Vec<UserView>,
- pub banned: Vec<UserView>,
+ pub site: Option<SiteView>, // Because the site might not be set up yet
+ pub counts: SiteAggregates,
+ pub admins: Vec<UserViewSafe>,
+ pub banned: Vec<UserViewSafe>,
pub online: usize,
pub version: String,
pub my_user: Option<User_>,
use lemmy_db::{
comment_view::{CommentView, ReplyView},
- community_view::{CommunityFollowerView, CommunityModeratorView},
- post_view::PostView,
private_message_view::PrivateMessageView,
user_mention_view::UserMentionView,
- user_view::UserView,
+ views::{
+ community_follower_view::CommunityFollowerView,
+ community_moderator_view::CommunityModeratorView,
+ post_view::PostView,
+ user_view::{UserViewDangerous, UserViewSafe},
+ },
};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct GetUserDetailsResponse {
- pub user: UserView,
+ pub user: Option<UserViewSafe>,
+ pub user_dangerous: Option<UserViewDangerous>,
pub follows: Vec<CommunityFollowerView>,
pub moderates: Vec<CommunityModeratorView>,
pub comments: Vec<CommentView>,
#[derive(Serialize, Clone)]
pub struct AddAdminResponse {
- pub admins: Vec<UserView>,
+ pub admins: Vec<UserViewSafe>,
}
#[derive(Deserialize)]
#[derive(Serialize, Clone)]
pub struct BanUserResponse {
- pub user: UserView,
+ pub user: UserViewSafe,
pub banned: bool,
}
pub fn send_post(
&self,
user_operation: &UserOperation,
- post: &PostResponse,
+ post_res: &PostResponse,
websocket_id: Option<ConnectionId>,
) -> Result<(), LemmyError> {
- let community_id = post.post.community_id;
+ let community_id = post_res.post_view.community.id;
// Don't send my data with it
- let mut post_sent = post.clone();
- post_sent.post.my_vote = None;
- post_sent.post.user_id = None;
+ // TODO no idea what to do here
+ // let mut post_sent = post_res.clone();
+ // post_sent.post.my_vote = None;
+ // post_sent.post.user_id = None;
// Send it to /c/all and that community
- self.send_community_room_message(user_operation, &post_sent, 0, websocket_id)?;
- self.send_community_room_message(user_operation, &post_sent, community_id, websocket_id)?;
+ self.send_community_room_message(user_operation, &post_res, 0, websocket_id)?;
+ self.send_community_room_message(user_operation, &post_res, community_id, websocket_id)?;
// Send it to the post room
- self.send_post_room_message(user_operation, &post_sent, post.post.id, websocket_id)?;
+ self.send_post_room_message(
+ user_operation,
+ &post_res,
+ post_res.post_view.post.id,
+ websocket_id,
+ )?;
Ok(())
}
--- /dev/null
+-- Site aggregates
+drop table site_aggregates;
+drop trigger site_aggregates_user on user_;
+drop trigger site_aggregates_post on post;
+drop trigger site_aggregates_comment on comment;
+drop trigger site_aggregates_community on community;
+drop function
+ site_aggregates_user,
+ site_aggregates_post,
+ site_aggregates_comment,
+ site_aggregates_community;
--- /dev/null
+-- Add site aggregates
+create table site_aggregates (
+ id serial primary key,
+ users bigint not null,
+ posts bigint not null,
+ comments bigint not null,
+ communities bigint not null
+);
+
+insert into site_aggregates (users, posts, comments, communities)
+ select ( select coalesce(count(*), 0) from user_) as users,
+ ( select coalesce(count(*), 0) from post) as posts,
+ ( select coalesce(count(*), 0) from comment) as comments,
+ ( select coalesce(count(*), 0) from community) as communities;
+
+-- Add site aggregate triggers
+-- user
+create function site_aggregates_user()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update site_aggregates
+ set users = users + 1;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update site_aggregates
+ set users = users - 1;
+ END IF;
+ return null;
+end $$;
+
+create trigger site_aggregates_user
+after insert or delete on user_
+for each row
+execute procedure site_aggregates_user();
+
+-- post
+create function site_aggregates_post()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update site_aggregates
+ set posts = posts + 1;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update site_aggregates
+ set posts = posts - 1;
+ END IF;
+ return null;
+end $$;
+
+create trigger site_aggregates_post
+after insert or delete on post
+for each row
+execute procedure site_aggregates_post();
+
+-- comment
+create function site_aggregates_comment()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update site_aggregates
+ set comments = comments + 1;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update site_aggregates
+ set comments = comments - 1;
+ END IF;
+ return null;
+end $$;
+
+create trigger site_aggregates_comment
+after insert or delete on comment
+for each row
+execute procedure site_aggregates_comment();
+
+-- community
+create function site_aggregates_community()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update site_aggregates
+ set communities = communities + 1;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update site_aggregates
+ set communities = communities - 1;
+ END IF;
+ return null;
+end $$;
+
+create trigger site_aggregates_community
+after insert or delete on community
+for each row
+execute procedure site_aggregates_community();
+
--- /dev/null
+-- User aggregates
+drop table user_aggregates;
+drop trigger user_aggregates_user on user_;
+drop trigger user_aggregates_post_count on post;
+drop trigger user_aggregates_post_score on post_like;
+drop trigger user_aggregates_comment_count on comment;
+drop trigger user_aggregates_comment_score on comment_like;
+drop function
+ user_aggregates_user,
+ user_aggregates_post_count,
+ user_aggregates_post_score,
+ user_aggregates_comment_count,
+ user_aggregates_comment_score;
--- /dev/null
+-- Add user aggregates
+create table user_aggregates (
+ id serial primary key,
+ user_id int references user_ on update cascade on delete cascade not null,
+ post_count bigint not null default 0,
+ post_score bigint not null default 0,
+ comment_count bigint not null default 0,
+ comment_score bigint not null default 0,
+ unique (user_id)
+);
+
+insert into user_aggregates (user_id, post_count, post_score, comment_count, comment_score)
+ select u.id,
+ coalesce(pd.posts, 0),
+ coalesce(pd.score, 0),
+ coalesce(cd.comments, 0),
+ coalesce(cd.score, 0)
+ from user_ u
+ left join (
+ select p.creator_id,
+ count(distinct p.id) as posts,
+ sum(pl.score) as score
+ from post p
+ left join post_like pl on p.id = pl.post_id
+ group by p.creator_id
+ ) pd on u.id = pd.creator_id
+ left join (
+ select c.creator_id,
+ count(distinct c.id) as comments,
+ sum(cl.score) as score
+ from comment c
+ left join comment_like cl on c.id = cl.comment_id
+ group by c.creator_id
+ ) cd on u.id = cd.creator_id;
+
+
+-- Add user aggregate triggers
+
+-- initial user add
+create function user_aggregates_user()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ insert into user_aggregates (user_id) values (NEW.id);
+ ELSIF (TG_OP = 'DELETE') THEN
+ delete from user_aggregates where user_id = OLD.id;
+ END IF;
+ return null;
+end $$;
+
+create trigger user_aggregates_user
+after insert or delete on user_
+for each row
+execute procedure user_aggregates_user();
+
+-- post count
+create function user_aggregates_post_count()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update user_aggregates
+ set post_count = post_count + 1 where user_id = NEW.creator_id;
+
+ ELSIF (TG_OP = 'DELETE') THEN
+ update user_aggregates
+ set post_count = post_count - 1 where user_id = OLD.creator_id;
+
+ -- If the post gets deleted, the score calculation trigger won't fire,
+ -- so you need to re-calculate
+ update user_aggregates ua
+ set post_score = pd.score
+ from (
+ select u.id,
+ coalesce(0, sum(pl.score)) as score
+ -- User join because posts could be empty
+ from user_ u
+ left join post p on u.id = p.creator_id
+ left join post_like pl on p.id = pl.post_id
+ group by u.id
+ ) pd
+ where ua.user_id = OLD.creator_id;
+
+ END IF;
+ return null;
+end $$;
+
+create trigger user_aggregates_post_count
+after insert or delete on post
+for each row
+execute procedure user_aggregates_post_count();
+
+-- post score
+create function user_aggregates_post_score()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ -- Need to get the post creator, not the voter
+ update user_aggregates ua
+ set post_score = post_score + NEW.score
+ from post p
+ where ua.user_id = p.creator_id and p.id = NEW.post_id;
+
+ ELSIF (TG_OP = 'DELETE') THEN
+ update user_aggregates ua
+ set post_score = post_score - OLD.score
+ from post p
+ where ua.user_id = p.creator_id and p.id = OLD.post_id;
+ END IF;
+ return null;
+end $$;
+
+create trigger user_aggregates_post_score
+after insert or delete on post_like
+for each row
+execute procedure user_aggregates_post_score();
+
+-- comment count
+create function user_aggregates_comment_count()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update user_aggregates
+ set comment_count = comment_count + 1 where user_id = NEW.creator_id;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update user_aggregates
+ set comment_count = comment_count - 1 where user_id = OLD.creator_id;
+
+ -- If the comment gets deleted, the score calculation trigger won't fire,
+ -- so you need to re-calculate
+ update user_aggregates ua
+ set comment_score = cd.score
+ from (
+ select u.id,
+ coalesce(0, sum(cl.score)) as score
+ -- User join because comments could be empty
+ from user_ u
+ left join comment c on u.id = c.creator_id
+ left join comment_like cl on c.id = cl.comment_id
+ group by u.id
+ ) cd
+ where ua.user_id = OLD.creator_id;
+ END IF;
+ return null;
+end $$;
+
+create trigger user_aggregates_comment_count
+after insert or delete on comment
+for each row
+execute procedure user_aggregates_comment_count();
+
+-- comment score
+create function user_aggregates_comment_score()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ -- Need to get the post creator, not the voter
+ update user_aggregates ua
+ set comment_score = comment_score + NEW.score
+ from comment c
+ where ua.user_id = c.creator_id and c.id = NEW.comment_id;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update user_aggregates ua
+ set comment_score = comment_score - OLD.score
+ from comment c
+ where ua.user_id = c.creator_id and c.id = OLD.comment_id;
+ END IF;
+ return null;
+end $$;
+
+create trigger user_aggregates_comment_score
+after insert or delete on comment_like
+for each row
+execute procedure user_aggregates_comment_score();
--- /dev/null
+-- community aggregates
+drop table community_aggregates;
+drop trigger community_aggregates_community on community;
+drop trigger community_aggregates_post_count on post;
+drop trigger community_aggregates_comment_count on comment;
+drop trigger community_aggregates_subscriber_count on community_follower;
+drop function
+ community_aggregates_community,
+ community_aggregates_post_count,
+ community_aggregates_comment_count,
+ community_aggregates_subscriber_count;
--- /dev/null
+-- Add community aggregates
+create table community_aggregates (
+ id serial primary key,
+ community_id int references community on update cascade on delete cascade not null,
+ subscribers bigint not null default 0,
+ posts bigint not null default 0,
+ comments bigint not null default 0,
+ unique (community_id)
+);
+
+insert into community_aggregates (community_id, subscribers, posts, comments)
+ select
+ c.id,
+ coalesce(cf.subs, 0) as subscribers,
+ coalesce(cd.posts, 0) as posts,
+ coalesce(cd.comments, 0) as comments
+ from community c
+ left join (
+ select
+ p.community_id,
+ count(distinct p.id) as posts,
+ count(distinct ct.id) as comments
+ from post p
+ left join comment ct on p.id = ct.post_id
+ group by p.community_id
+ ) cd on cd.community_id = c.id
+ left join (
+ select
+ community_follower.community_id,
+ count(*) as subs
+ from community_follower
+ group by community_follower.community_id
+ ) cf on cf.community_id = c.id;
+
+-- Add community aggregate triggers
+
+-- initial community add
+create function community_aggregates_community()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ insert into community_aggregates (community_id) values (NEW.id);
+ ELSIF (TG_OP = 'DELETE') THEN
+ delete from community_aggregates where community_id = OLD.id;
+ END IF;
+ return null;
+end $$;
+
+create trigger community_aggregates_community
+after insert or delete on community
+for each row
+execute procedure community_aggregates_community();
+-- post count
+create function community_aggregates_post_count()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update community_aggregates
+ set posts = posts + 1 where community_id = NEW.community_id;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update community_aggregates
+ set posts = posts - 1 where community_id = OLD.community_id;
+
+ -- Update the counts if the post got deleted
+ update community_aggregates ca
+ set posts = coalesce(cd.posts, 0),
+ comments = coalesce(cd.comments, 0)
+ from (
+ select
+ c.id,
+ count(distinct p.id) as posts,
+ count(distinct ct.id) as comments
+ from community c
+ left join post p on c.id = p.community_id
+ left join comment ct on p.id = ct.post_id
+ group by c.id
+ ) cd
+ where ca.community_id = OLD.community_id;
+ END IF;
+ return null;
+end $$;
+
+create trigger community_aggregates_post_count
+after insert or delete on post
+for each row
+execute procedure community_aggregates_post_count();
+
+-- comment count
+create function community_aggregates_comment_count()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update community_aggregates ca
+ set comments = comments + 1 from comment c, post p
+ where p.id = c.post_id
+ and p.id = NEW.post_id
+ and ca.community_id = p.community_id;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update community_aggregates ca
+ set comments = comments - 1 from comment c, post p
+ where p.id = c.post_id
+ and p.id = OLD.post_id
+ and ca.community_id = p.community_id;
+
+ END IF;
+ return null;
+end $$;
+
+create trigger community_aggregates_comment_count
+after insert or delete on comment
+for each row
+execute procedure community_aggregates_comment_count();
+
+-- subscriber count
+create function community_aggregates_subscriber_count()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update community_aggregates
+ set subscribers = subscribers + 1 where community_id = NEW.community_id;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update community_aggregates
+ set subscribers = subscribers - 1 where community_id = OLD.community_id;
+ END IF;
+ return null;
+end $$;
+
+create trigger community_aggregates_subscriber_count
+after insert or delete on community_follower
+for each row
+execute procedure community_aggregates_subscriber_count();
+
--- /dev/null
+-- post aggregates
+drop table post_aggregates;
+drop trigger post_aggregates_post on post;
+drop trigger post_aggregates_comment_count on comment;
+drop trigger post_aggregates_score on post_like;
+drop function
+ post_aggregates_post,
+ post_aggregates_comment_count,
+ post_aggregates_score;
--- /dev/null
+-- Add post aggregates
+create table post_aggregates (
+ id serial primary key,
+ post_id int references post on update cascade on delete cascade not null,
+ comments bigint not null default 0,
+ score bigint not null default 0,
+ upvotes bigint not null default 0,
+ downvotes bigint not null default 0,
+ newest_comment_time timestamp not null default now(),
+ unique (post_id)
+);
+
+insert into post_aggregates (post_id, comments, score, upvotes, downvotes, newest_comment_time)
+ select
+ p.id,
+ coalesce(ct.comments, 0::bigint) as comments,
+ coalesce(pl.score, 0::bigint) as score,
+ coalesce(pl.upvotes, 0::bigint) as upvotes,
+ coalesce(pl.downvotes, 0::bigint) as downvotes,
+ greatest(ct.recent_comment_time, p.published) as newest_activity_time
+ from post p
+ left join (
+ select comment.post_id,
+ count(*) as comments,
+ max(comment.published) as recent_comment_time
+ from comment
+ group by comment.post_id
+ ) ct on ct.post_id = p.id
+ left join (
+ select post_like.post_id,
+ sum(post_like.score) as score,
+ sum(post_like.score) filter (where post_like.score = 1) as upvotes,
+ -sum(post_like.score) filter (where post_like.score = '-1'::integer) as downvotes
+ from post_like
+ group by post_like.post_id
+ ) pl on pl.post_id = p.id;
+
+-- Add community aggregate triggers
+
+-- initial post add
+create function post_aggregates_post()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ insert into post_aggregates (post_id) values (NEW.id);
+ ELSIF (TG_OP = 'DELETE') THEN
+ delete from post_aggregates where post_id = OLD.id;
+ END IF;
+ return null;
+end $$;
+
+create trigger post_aggregates_post
+after insert or delete on post
+for each row
+execute procedure post_aggregates_post();
+
+-- comment count
+create function post_aggregates_comment_count()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update post_aggregates pa
+ set comments = comments + 1,
+ newest_comment_time = NEW.published
+ where pa.post_id = NEW.post_id;
+ ELSIF (TG_OP = 'DELETE') THEN
+ -- Join to post because that post may not exist anymore
+ update post_aggregates pa
+ set comments = comments - 1
+ from post p
+ where pa.post_id = p.id
+ and pa.post_id = OLD.post_id;
+ END IF;
+ return null;
+end $$;
+
+create trigger post_aggregates_comment_count
+after insert or delete on comment
+for each row
+execute procedure post_aggregates_comment_count();
+
+-- post score
+create function post_aggregates_score()
+returns trigger language plpgsql
+as $$
+begin
+ IF (TG_OP = 'INSERT') THEN
+ update post_aggregates pa
+ set score = score + NEW.score,
+ upvotes = case when NEW.score = 1 then upvotes + 1 else upvotes end,
+ downvotes = case when NEW.score = -1 then downvotes + 1 else downvotes end
+ where pa.post_id = NEW.post_id;
+
+ ELSIF (TG_OP = 'DELETE') THEN
+ -- Join to post because that post may not exist anymore
+ update post_aggregates pa
+ set score = score - OLD.score,
+ upvotes = case when OLD.score = 1 then upvotes - 1 else upvotes end,
+ downvotes = case when OLD.score = -1 then downvotes - 1 else downvotes end
+ from post p
+ where pa.post_id = p.id
+ and pa.post_id = OLD.post_id;
+
+ END IF;
+ return null;
+end $$;
+
+create trigger post_aggregates_score
+after insert or delete on post_like
+for each row
+execute procedure post_aggregates_score();
*,
};
use lemmy_db::{
- comment::Comment,
- community::{Community, CommunityForm},
naive_now,
- post::Post,
- private_message::PrivateMessage,
- user::{UserForm, User_},
+ source::{
+ comment::Comment,
+ community::{Community, CommunityForm},
+ post::Post,
+ private_message::PrivateMessage,
+ user::{UserForm, User_},
+ },
Crud,
};
use lemmy_utils::{
use lemmy_api::claims::Claims;
use lemmy_db::{
comment_view::{ReplyQueryBuilder, ReplyView},
- community::Community,
- post_view::{PostQueryBuilder, PostView},
- site_view::SiteView,
- user::User_,
+ source::{community::Community, user::User_},
user_mention_view::{UserMentionQueryBuilder, UserMentionView},
+ views::{
+ post_view::{PostQueryBuilder, PostView},
+ site_view::SiteView,
+ },
ListingType,
SortType,
};
let listing_type_ = listing_type.clone();
let posts = blocking(context.pool(), move |conn| {
- PostQueryBuilder::create(&conn)
+ PostQueryBuilder::create(&conn, None)
.listing_type(&listing_type_)
.sort(&sort_type)
.list()
.namespaces(RSS_NAMESPACE.to_owned())
.title(&format!(
"{} - {}",
- site_view.name,
+ site_view.site.name,
listing_type.to_string()
))
.link(Settings::get().get_protocol_and_hostname())
.items(items);
- if let Some(site_desc) = site_view.description {
+ if let Some(site_desc) = site_view.site.description {
channel_builder.description(&site_desc);
}
let user = User_::find_by_username(&conn, &user_name)?;
let user_url = user.get_profile_url(&Settings::get().hostname);
- let posts = PostQueryBuilder::create(&conn)
+ let posts = PostQueryBuilder::create(&conn, None)
.listing_type(&ListingType::All)
.sort(sort_type)
.for_creator_id(user.id)
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.to_owned())
- .title(&format!("{} - {}", site_view.name, user.name))
+ .title(&format!("{} - {}", site_view.site.name, user.name))
.link(user_url)
.items(items);
let site_view = SiteView::read(&conn)?;
let community = Community::read_from_name(&conn, &community_name)?;
- let posts = PostQueryBuilder::create(&conn)
+ let posts = PostQueryBuilder::create(&conn, None)
.listing_type(&ListingType::All)
.sort(sort_type)
.for_community_id(community.id)
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.to_owned())
- .title(&format!("{} - {}", site_view.name, community.name))
+ .title(&format!("{} - {}", site_view.site.name, community.name))
.link(community.actor_id)
.items(items);
let site_view = SiteView::read(&conn)?;
let user_id = Claims::decode(&jwt)?.claims.id;
- let posts = PostQueryBuilder::create(&conn)
+ let posts = PostQueryBuilder::create(&conn, Some(user_id))
.listing_type(&ListingType::Subscribed)
.sort(sort_type)
- .my_user_id(user_id)
.list()?;
let items = create_post_items(posts)?;
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.to_owned())
- .title(&format!("{} - Subscribed", site_view.name))
+ .title(&format!("{} - Subscribed", site_view.site.name))
.link(Settings::get().get_protocol_and_hostname())
.items(items);
- if let Some(site_desc) = site_view.description {
+ if let Some(site_desc) = site_view.site.description {
channel_builder.description(&site_desc);
}
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.to_owned())
- .title(&format!("{} - Inbox", site_view.name))
+ .title(&format!("{} - Inbox", site_view.site.name))
.link(format!(
"{}/inbox",
Settings::get().get_protocol_and_hostname()
))
.items(items);
- if let Some(site_desc) = site_view.description {
+ if let Some(site_desc) = site_view.site.description {
channel_builder.description(&site_desc);
}
let mut i = ItemBuilder::default();
let mut dc_extension = DublinCoreExtensionBuilder::default();
- i.title(p.name);
+ i.title(p.post.name);
- dc_extension.creators(vec![p.creator_actor_id.to_owned()]);
+ dc_extension.creators(vec![p.creator.actor_id.to_owned()]);
- let dt = DateTime::<Utc>::from_utc(p.published, Utc);
+ let dt = DateTime::<Utc>::from_utc(p.post.published, Utc);
i.pub_date(dt.to_rfc2822());
let post_url = format!(
"{}/post/{}",
Settings::get().get_protocol_and_hostname(),
- p.id
+ p.post.id
);
i.comments(post_url.to_owned());
let guid = GuidBuilder::default()
let community_url = format!(
"{}/c/{}",
Settings::get().get_protocol_and_hostname(),
- p.community_name
+ p.community.name
);
// TODO: for category we should just put the name of the category, but then we would have
// to read each community from the db
- if let Some(url) = p.url {
+ if let Some(url) = p.post.url {
i.link(url);
}
// TODO add images
let mut description = format!("submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
- p.creator_actor_id,
- p.creator_name,
+ p.creator.actor_id,
+ p.creator.name,
community_url,
- p.community_name,
- p.score,
+ p.community.name,
+ p.counts.score,
post_url,
- p.number_of_comments);
+ p.counts.comments);
- if let Some(body) = p.body {
+ if let Some(body) = p.post.body {
let html = markdown_to_html(&body);
description.push_str(&html);
}
use actix_web::{body::Body, error::ErrorBadRequest, *};
use anyhow::anyhow;
use lemmy_api::version;
-use lemmy_db::site_view::SiteView;
+use lemmy_db::views::site_view::SiteView;
use lemmy_structs::blocking;
use lemmy_utils::{settings::Settings, LemmyError};
use lemmy_websocket::LemmyContext;
},
protocols,
usage: NodeInfoUsage {
- users: NodeInfoUsers {
- total: site_view.number_of_users,
- },
- local_posts: site_view.number_of_posts,
- local_comments: site_view.number_of_comments,
- open_registrations: site_view.open_registration,
+ // TODO get these again
+ users: NodeInfoUsers { total: 0 },
+ local_posts: 0,
+ local_comments: 0,
+ open_registrations: site_view.site.open_registration,
},
};
use actix_web::{error::ErrorBadRequest, web::Query, *};
use anyhow::anyhow;
-use lemmy_db::{community::Community, user::User_};
+use lemmy_db::source::{community::Community, user::User_};
use lemmy_structs::{blocking, WebFingerLink, WebFingerResponse};
use lemmy_utils::{
settings::Settings,
},
};
use lemmy_db::{
- community::{Community, CommunityForm},
- user::{User_, *},
+ source::{
+ community::{Community, CommunityForm},
+ user::{User_, *},
+ },
Crud,
ListingType,
SortType,