- [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)
comment_view::*,
moderator::*,
post::*,
- site_view::*,
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());
}
}
comment::Comment,
comment_view::CommentQueryBuilder,
community::*,
- community_view::*,
diesel_option_overwrite,
moderator::*,
naive_now,
post::Post,
site::*,
- user_view::*,
+ 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 actix_web::{web, web::Data};
use lemmy_db::{
community::{Community, CommunityModerator},
- community_view::CommunityUserBanView,
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::*,
+ views::{
+ community_moderator_view::CommunityModeratorView,
+ community_view::CommunityView,
+ 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::fetcher::search_by_apub_id;
use lemmy_db::{
+ aggregates::site_aggregates::SiteAggregates,
category::*,
comment_view::*,
- community_view::*,
diesel_option_overwrite,
moderator::*,
moderator_views::*,
naive_now,
post_view::*,
site::*,
- site_view::*,
- user_view::*,
+ views::{
+ community_view::CommunityQueryBuilder,
+ 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,
})
}
}
}
SearchType::Communities => {
communities = blocking(context.pool(), move |conn| {
- CommunityQueryBuilder::create(conn)
+ CommunityQueryBuilder::create(conn, None)
.sort(&sort)
.search_term(q)
.page(page)
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)
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());
comment_report::CommentReportView,
comment_view::*,
community::*,
- community_view::*,
diesel_option_overwrite,
moderator::*,
naive_now,
private_message::*,
private_message_view::*,
site::*,
- site_view::*,
user::*,
user_mention::*,
user_mention_view::*,
- user_view::*,
+ views::{
+ community_follower_view::CommunityFollowerView,
+ community_moderator_view::CommunityModeratorView,
+ 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;
// 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??;
base::{AnyBase, ExtendsExt},
};
use anyhow::Context;
-use lemmy_db::{community::Community, community_view::CommunityView, ApubObject};
+use lemmy_db::{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,
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::Context;
use itertools::Itertools;
-use lemmy_db::{community::Community, community_view::CommunityFollowerView, DbPool};
+use lemmy_db::{
+ 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)
comment::Comment,
comment_view::CommentView,
community::{Community, CommunityModerator, CommunityModeratorForm},
- community_view::CommunityView,
naive_now,
post::Post,
post_view::PostView,
user::User_,
- user_view::UserView,
+ views::{community_view::CommunityView, 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??,
];
collection::{CollectionExt, OrderedCollection, UnorderedCollection},
};
use actix_web::{body::Body, web, HttpResponse};
-use lemmy_db::{community::Community, community_view::CommunityFollowerView, post::Post};
+use lemmy_db::{
+ community::Community,
+ post::Post,
+ views::community_follower_view::CommunityFollowerView,
+};
use lemmy_structs::blocking;
use lemmy_utils::LemmyError;
use lemmy_websocket::LemmyContext;
use anyhow::{anyhow, Context};
use lemmy_db::{
community::{Community, CommunityFollower, CommunityFollowerForm},
- community_view::CommunityUserBanView,
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());
}
}
}
-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 {
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.
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 anyhow::Context;
use lemmy_db::{
community::{Community, CommunityForm},
- community_view::CommunityModeratorView,
naive_now,
+ 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
--- /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 counts: i64,
+}
+
+impl CommunityAggregates {
+ pub fn read(conn: &PgConnection, id: i32) -> Result<Self, Error> {
+ community_aggregates::table.find(id).first::<Self>(conn)
+ }
+}
+
+// TODO add unit tests, to make sure triggers are working
--- /dev/null
+pub mod community_aggregates;
+pub mod site_aggregates;
+pub mod user_aggregates;
--- /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,
+ comment::{Comment, CommentForm},
+ community::{Community, CommunityForm},
+ post::{Post, PostForm},
+ tests::establish_unpooled_connection,
+ user::{UserForm, User_},
+ 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,
+ };
+
+ 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 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!(1, site_aggregates_before_delete.posts);
+ assert_eq!(2, site_aggregates_before_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,
+ comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
+ community::{Community, CommunityForm},
+ post::{Post, PostForm, PostLike, PostLikeForm},
+ tests::establish_unpooled_connection,
+ user::{UserForm, User_},
+ 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 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 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);
+
+ // 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 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,
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,
}
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::{community::Community, schema::community::columns::*, 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)
}
+++ /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 std::{env, env::VarError};
pub mod activity;
+pub mod aggregates;
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 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 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;
}
}
+table! {
+ community_aggregates (id) {
+ id -> Int4,
+ community_id -> Int4,
+ subscribers -> Int8,
+ posts -> Int8,
+ comments -> Int8,
+ }
+}
+
table! {
community_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!(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,
post_saved,
private_message,
site,
+ site_aggregates,
user_,
+ user_aggregates,
user_ban,
user_fast,
user_mention,
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,
+++ /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)
- }
-}
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::*, 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 {
private_key: None,
public_key: None,
last_refreshed_at: inserted_user.published,
+ deleted: false,
};
let read_user = User_::read(&conn, inserted_user.id).unwrap();
+++ /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::{
+ community::{Community, CommunitySafe},
+ schema::{community, community_follower, user_},
+ user::{UserSafe, User_},
+ ToSafe,
+};
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct CommunityFollowerView {
+ pub community: CommunitySafe,
+ pub follower: 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::<(CommunitySafe, UserSafe)>(conn)?;
+
+ Ok(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::<(CommunitySafe, UserSafe)>(conn)?;
+
+ Ok(to_vec(res))
+ }
+}
+
+fn to_vec(users: Vec<(CommunitySafe, UserSafe)>) -> Vec<CommunityFollowerView> {
+ users
+ .iter()
+ .map(|a| CommunityFollowerView {
+ community: a.0.to_owned(),
+ follower: a.1.to_owned(),
+ })
+ .collect::<Vec<CommunityFollowerView>>()
+}
--- /dev/null
+use crate::{
+ community::{Community, CommunitySafe},
+ schema::{community, community_moderator, user_},
+ user::{UserSafe, User_},
+ ToSafe,
+};
+use diesel::{result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct CommunityModeratorView {
+ pub community: CommunitySafe,
+ pub moderator: 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::<(CommunitySafe, UserSafe)>(conn)?;
+
+ Ok(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::<(CommunitySafe, UserSafe)>(conn)?;
+
+ Ok(to_vec(res))
+ }
+}
+
+fn to_vec(users: Vec<(CommunitySafe, UserSafe)>) -> Vec<CommunityModeratorView> {
+ users
+ .iter()
+ .map(|a| CommunityModeratorView {
+ community: a.0.to_owned(),
+ moderator: a.1.to_owned(),
+ })
+ .collect::<Vec<CommunityModeratorView>>()
+}
--- /dev/null
+use crate::{
+ community::{Community, CommunitySafe},
+ schema::{community, community_user_ban, user_},
+ 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,
+ category::Category,
+ community::{Community, CommunityFollower, CommunitySafe},
+ functions::hot_rank,
+ fuzzy_search,
+ limit_and_offset,
+ schema::{category, community, community_aggregates, community_follower, user_},
+ user::{UserSafe, User_},
+ 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,
+}
+
+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, subscribed) = 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::<(
+ CommunitySafe,
+ UserSafe,
+ Category,
+ CommunityAggregates,
+ Option<CommunityFollower>,
+ )>(conn)?;
+
+ Ok(CommunityView {
+ community,
+ creator,
+ category,
+ subscribed: subscribed.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::<(
+ CommunitySafe,
+ UserSafe,
+ Category,
+ CommunityAggregates,
+ Option<CommunityFollower>,
+ )>(self.conn)?;
+
+ Ok(to_vec(res))
+ }
+}
+
+fn to_vec(
+ users: Vec<(
+ CommunitySafe,
+ UserSafe,
+ Category,
+ CommunityAggregates,
+ Option<CommunityFollower>,
+ )>,
+) -> Vec<CommunityView> {
+ users
+ .iter()
+ .map(|a| CommunityView {
+ 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<CommunityView>>()
+}
--- /dev/null
+pub mod community_follower_view;
+pub mod community_moderator_view;
+pub mod community_user_ban_view;
+pub mod community_view;
+pub mod site_view;
+pub mod user_view;
+
+// TODO Every single aggregate trigger is likely broken, you need to test every one of these out
--- /dev/null
+use crate::{
+ schema::{site, user_},
+ 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},
+ user::{UserSafe, User_},
+ MaybeOptional,
+ SortType,
+ ToSafe,
+};
+use diesel::{dsl::*, result::Error, *};
+use serde::Serialize;
+
+#[derive(Debug, Serialize, Clone)]
+pub struct UserViewSafe {
+ pub user: UserSafe,
+ pub counts: UserAggregates,
+}
+
+#[derive(Debug, Serialize, Clone)]
+pub struct UserViewDangerous {
+ pub user: User_,
+ pub counts: 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::<(User_, UserAggregates)>(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::<(UserSafe, UserAggregates)>(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::<(UserSafe, UserAggregates)>(conn)?;
+
+ Ok(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::<(UserSafe, UserAggregates)>(conn)?;
+
+ Ok(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::<(UserSafe, UserAggregates)>(self.conn)?;
+
+ Ok(to_vec(res))
+ }
+}
+
+fn to_vec(users: Vec<(UserSafe, UserAggregates)>) -> Vec<UserViewSafe> {
+ users
+ .iter()
+ .map(|a| UserViewSafe {
+ user: a.0.to_owned(),
+ counts: a.1.to_owned(),
+ })
+ .collect::<Vec<UserViewSafe>>()
+}
-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 lemmy_db::{
comment_view::CommentView,
- community_view::{CommunityModeratorView, CommunityView},
post_report::PostReportView,
post_view::PostView,
+ views::{community_moderator_view::CommunityModeratorView, community_view::CommunityView},
};
use serde::{Deserialize, Serialize};
use lemmy_db::{
+ aggregates::site_aggregates::SiteAggregates,
category::*,
comment_view::*,
- community_view::*,
moderator_views::*,
post_view::*,
- site_view::*,
user::*,
- user_view::*,
+ views::{community_view::CommunityView, 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,
+ 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,
}
--- /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_
+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 = pd.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
+ -- TODO not sure if this is working right
+ -- 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 = cd.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_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_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,
+ posts bigint not null,
+ comments bigint not null,
+ unique (community_id)
+);
+
+insert into community_aggregates (community_id, subscribers, posts, comments)
+ select
+ c.id,
+ coalesce(cf.subs, 0::bigint) as subscribers,
+ coalesce(cd.posts, 0::bigint) as posts,
+ coalesce(cd.comments, 0::bigint) 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
+-- 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;
+ 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
+ set comments = comments + 1 from comment c join post p on p.id = c.post_id and p.id = NEW.post_id;
+ ELSIF (TG_OP = 'DELETE') THEN
+ update community_aggregates
+ set comments = comments - 1 from comment c join post p on p.id = c.post_id and p.id = OLD.post_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();
+
comment_view::{ReplyQueryBuilder, ReplyView},
community::Community,
post_view::{PostQueryBuilder, PostView},
- site_view::SiteView,
user::User_,
user_mention_view::{UserMentionQueryBuilder, UserMentionView},
+ views::site_view::SiteView,
ListingType,
SortType,
};
.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 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 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 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);
}
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,
},
};