Merge branch 'main' into split_user_table

This commit is contained in:
Dessalines 2021-02-25 12:34:00 -05:00
commit aba32917bd
68 changed files with 559 additions and 701 deletions

View file

@ -1,5 +1,5 @@
tab_spaces = 2
edition="2018"
imports_layout="HorizontalVertical"
merge_imports=true
imports_granularity="Crate"
reorder_imports=true

View file

@ -1,3 +1,23 @@
# Lemmy v0.9.9 Release (2021-02-19)
## Changes
### Lemmy backend
- Added an federated activity query sorting order.
- Explicitly marking posts and comments as public.
- Added a `NewComment` / forum sort for posts.
- Fixed an issue with not setting correct published time for fetched posts.
- Fixed an issue with an open docker port on lemmy-ui.
- Using lemmy post link for RSS link.
- Fixed reason and display name lengths to use char counts instead.
### Lemmy-ui
- Updated translations.
- Made websocket host configurable.
- Added some accessibility features.
- Always showing password reset link.
# Lemmy v0.9.7 Release (2021-02-08)
## Changes

View file

@ -1 +1 @@
0.9.7
0.9.9

View file

@ -64,6 +64,14 @@
- src: '../docker/iframely.config.local.js'
dest: '{{lemmy_base_dir}}/iframely.config.local.js'
mode: '0600'
vars:
lemmy_docker_image: "dessalines/lemmy:dev"
lemmy_docker_ui_image: "dessalines/lemmy-ui:{{ lookup('file', 'VERSION') }}"
lemmy_port: "8536"
lemmy_ui_port: "1235"
pictshare_port: "8537"
iframely_port: "8538"
postgres_password: "{{ lookup('password', 'passwords/{{ inventory_hostname }}/postgres chars=ascii_letters,digits') }}"
- name: add config file (only during initial setup)
template:

View file

@ -27,7 +27,7 @@ use lemmy_db_views::{
use lemmy_structs::{blocking, comment::*, send_local_notifs};
use lemmy_utils::{
utils::{remove_slurs, scrape_text_for_mentions},
APIError,
ApiError,
ConnectionId,
LemmyError,
};
@ -60,7 +60,7 @@ impl Perform for CreateComment {
// Check if post is locked, no new comments
if post.locked {
return Err(APIError::err("locked").into());
return Err(ApiError::err("locked").into());
}
// If there's a parent_id, check to make sure that comment is in that post
@ -69,10 +69,10 @@ impl Perform for CreateComment {
let parent =
match blocking(context.pool(), move |conn| Comment::read(&conn, parent_id)).await? {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
if parent.post_id != post_id {
return Err(APIError::err("couldnt_create_comment").into());
return Err(ApiError::err("couldnt_create_comment").into());
}
}
@ -98,7 +98,7 @@ impl Perform for CreateComment {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
// Necessary to update the ap_id
@ -112,7 +112,7 @@ impl Perform for CreateComment {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_comment").into()),
};
updated_comment.send_create(&user, context).await?;
@ -140,7 +140,7 @@ impl Perform for CreateComment {
let like = move |conn: &'_ _| CommentLike::like(&conn, &like_form);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_comment").into());
return Err(ApiError::err("couldnt_like_comment").into());
}
updated_comment.send_like(&user, context).await?;
@ -160,7 +160,7 @@ impl Perform for CreateComment {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
comment_view.comment.read = true;
}
@ -205,7 +205,7 @@ impl Perform for EditComment {
// Verify that only the creator can edit
if user.id != orig_comment.creator.id {
return Err(APIError::err("no_comment_edit_allowed").into());
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the update
@ -217,7 +217,7 @@ impl Perform for EditComment {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Send the apub update
@ -281,7 +281,7 @@ impl Perform for DeleteComment {
// Verify that only the creator can delete
if user.id != orig_comment.creator.id {
return Err(APIError::err("no_comment_edit_allowed").into());
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the delete
@ -292,7 +292,7 @@ impl Perform for DeleteComment {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Send the apub message
@ -370,7 +370,7 @@ impl Perform for RemoveComment {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Mod tables
@ -452,7 +452,7 @@ impl Perform for MarkCommentAsRead {
// Verify that only the recipient can mark as read
if user.id != orig_comment.get_recipient_id() {
return Err(APIError::err("no_comment_edit_allowed").into());
return Err(ApiError::err("no_comment_edit_allowed").into());
}
// Do the mark as read
@ -463,7 +463,7 @@ impl Perform for MarkCommentAsRead {
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_comment").into()),
};
// Refetch it
@ -504,12 +504,12 @@ impl Perform for SaveComment {
if data.save {
let save_comment = move |conn: &'_ _| CommentSaved::save(conn, &comment_saved_form);
if blocking(context.pool(), save_comment).await?.is_err() {
return Err(APIError::err("couldnt_save_comment").into());
return Err(ApiError::err("couldnt_save_comment").into());
}
} else {
let unsave_comment = move |conn: &'_ _| CommentSaved::unsave(conn, &comment_saved_form);
if blocking(context.pool(), unsave_comment).await?.is_err() {
return Err(APIError::err("couldnt_save_comment").into());
return Err(ApiError::err("couldnt_save_comment").into());
}
}
@ -577,7 +577,7 @@ impl Perform for CreateCommentLike {
let like_form2 = like_form.clone();
let like = move |conn: &'_ _| CommentLike::like(conn, &like_form2);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_comment").into());
return Err(ApiError::err("couldnt_like_comment").into());
}
if like_form.score == 1 {
@ -647,7 +647,7 @@ impl Perform for GetComments {
.await?;
let comments = match comments {
Ok(comments) => comments,
Err(_) => return Err(APIError::err("couldnt_get_comments").into()),
Err(_) => return Err(ApiError::err("couldnt_get_comments").into()),
};
Ok(GetCommentsResponse { comments })
@ -670,10 +670,10 @@ impl Perform for CreateCommentReport {
// check size of report and check for whitespace
let reason = data.reason.trim();
if reason.is_empty() {
return Err(APIError::err("report_reason_required").into());
return Err(ApiError::err("report_reason_required").into());
}
if reason.len() > 1000 {
return Err(APIError::err("report_too_long").into());
if reason.chars().count() > 1000 {
return Err(ApiError::err("report_too_long").into());
}
let user_id = user.id;
@ -698,7 +698,7 @@ impl Perform for CreateCommentReport {
.await?
{
Ok(report) => report,
Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
};
let res = CreateCommentReportResponse { success: true };
@ -753,7 +753,7 @@ impl Perform for ResolveCommentReport {
};
if blocking(context.pool(), resolve_fun).await?.is_err() {
return Err(APIError::err("couldnt_resolve_report").into());
return Err(ApiError::err("couldnt_resolve_report").into());
};
let report_id = data.report_id;

View file

@ -48,7 +48,7 @@ use lemmy_utils::{
apub::generate_actor_keypair,
location_info,
utils::{check_slurs, check_slurs_opt, is_valid_community_name, naive_from_unix},
APIError,
ApiError,
ConnectionId,
LemmyError,
};
@ -82,7 +82,7 @@ impl Perform for GetCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
}
.id
}
@ -94,7 +94,7 @@ impl Perform for GetCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let moderators: Vec<CommunityModeratorView> = match blocking(context.pool(), move |conn| {
@ -103,7 +103,7 @@ impl Perform for GetCommunity {
.await?
{
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let online = context
@ -140,7 +140,7 @@ impl Perform for CreateCommunity {
check_slurs_opt(&data.description)?;
if !is_valid_community_name(&data.name) {
return Err(APIError::err("invalid_community_name").into());
return Err(ApiError::err("invalid_community_name").into());
}
// Double check for duplicate community actor_ids
@ -151,7 +151,7 @@ impl Perform for CreateCommunity {
})
.await?;
if community_dupe.is_ok() {
return Err(APIError::err("community_already_exists").into());
return Err(ApiError::err("community_already_exists").into());
}
// Check to make sure the icon and banners are urls
@ -170,7 +170,6 @@ impl Perform for CreateCommunity {
description: data.description.to_owned(),
icon,
banner,
category_id: data.category_id,
creator_id: user.id,
removed: None,
deleted: None,
@ -193,7 +192,7 @@ impl Perform for CreateCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("community_already_exists").into()),
Err(_e) => return Err(ApiError::err("community_already_exists").into()),
};
// The community creator becomes a moderator
@ -204,7 +203,7 @@ impl Perform for CreateCommunity {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
// Follow your own community
@ -216,7 +215,7 @@ impl Perform for CreateCommunity {
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
if blocking(context.pool(), follow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
}
let user_id = user.id;
@ -252,7 +251,7 @@ impl Perform for EditCommunity {
})
.await??;
if !mods.contains(&user.id) {
return Err(APIError::err("not_a_moderator").into());
return Err(ApiError::err("not_a_moderator").into());
}
let community_id = data.community_id;
@ -273,7 +272,6 @@ impl Perform for EditCommunity {
description: data.description.to_owned(),
icon,
banner,
category_id: data.category_id.to_owned(),
creator_id: read_community.creator_id,
removed: Some(read_community.removed),
deleted: Some(read_community.deleted),
@ -297,7 +295,7 @@ impl Perform for EditCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// TODO there needs to be some kind of an apub update
@ -337,7 +335,7 @@ impl Perform for DeleteCommunity {
})
.await??;
if read_community.creator_id != user.id {
return Err(APIError::err("no_community_edit_allowed").into());
return Err(ApiError::err("no_community_edit_allowed").into());
}
// Do the delete
@ -349,7 +347,7 @@ impl Perform for DeleteCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// Send apub messages
@ -398,7 +396,7 @@ impl Perform for RemoveCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_community").into()),
};
// Mod tables
@ -513,13 +511,13 @@ impl Perform for FollowCommunity {
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
if blocking(context.pool(), follow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
}
} else {
let unfollow =
move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(context.pool(), unfollow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
}
}
} else if data.follow {
@ -530,7 +528,7 @@ impl Perform for FollowCommunity {
user.send_unfollow(&community.actor_id(), context).await?;
let unfollow = move |conn: &'_ _| CommunityFollower::unfollow(conn, &community_follower_form);
if blocking(context.pool(), unfollow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
}
}
@ -571,7 +569,7 @@ impl Perform for GetFollowedCommunities {
.await?
{
Ok(communities) => communities,
_ => return Err(APIError::err("system_err_login").into()),
_ => return Err(ApiError::err("system_err_login").into()),
};
// Return the jwt
@ -605,7 +603,7 @@ impl Perform for BanFromCommunity {
if data.ban {
let ban = move |conn: &'_ _| CommunityUserBan::ban(conn, &community_user_ban_form);
if blocking(context.pool(), ban).await?.is_err() {
return Err(APIError::err("community_user_already_banned").into());
return Err(ApiError::err("community_user_already_banned").into());
}
// Also unsubscribe them from the community, if they are subscribed
@ -622,7 +620,7 @@ impl Perform for BanFromCommunity {
} else {
let unban = move |conn: &'_ _| CommunityUserBan::unban(conn, &community_user_ban_form);
if blocking(context.pool(), unban).await?.is_err() {
return Err(APIError::err("community_user_already_banned").into());
return Err(ApiError::err("community_user_already_banned").into());
}
}
@ -721,12 +719,12 @@ impl Perform for AddModToCommunity {
if data.added {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
} else {
let leave = move |conn: &'_ _| CommunityModerator::leave(conn, &community_moderator_form);
if blocking(context.pool(), leave).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
}
@ -798,14 +796,14 @@ impl Perform for TransferCommunity {
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());
return Err(ApiError::err("not_an_admin").into());
}
let community_id = data.community_id;
let new_creator = data.user_id;
let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
if blocking(context.pool(), update).await?.is_err() {
return Err(APIError::err("couldnt_update_community").into());
return Err(ApiError::err("couldnt_update_community").into());
};
// You also have to re-do the community_moderator table, reordering it.
@ -836,7 +834,7 @@ impl Perform for TransferCommunity {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
}
@ -860,7 +858,7 @@ impl Perform for TransferCommunity {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let community_id = data.community_id;
@ -870,7 +868,7 @@ impl Perform for TransferCommunity {
.await?
{
Ok(moderators) => moderators,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
// Return the jwt

View file

@ -19,7 +19,7 @@ use lemmy_db_views_actor::{
community_view::CommunityView,
};
use lemmy_structs::{blocking, comment::*, community::*, post::*, site::*, user::*, websocket::*};
use lemmy_utils::{claims::Claims, settings::Settings, APIError, ConnectionId, LemmyError};
use lemmy_utils::{claims::Claims, settings::Settings, ApiError, ConnectionId, LemmyError};
use lemmy_websocket::{serialize_websocket_message, LemmyContext, UserOperation};
use serde::Deserialize;
use std::process::Command;
@ -54,14 +54,14 @@ pub(crate) async fn is_mod_or_admin(
})
.await?;
if !is_mod_or_admin {
return Err(APIError::err("not_a_mod_or_admin").into());
return Err(ApiError::err("not_a_mod_or_admin").into());
}
Ok(())
}
pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
return Err(ApiError::err("not_an_admin").into());
}
Ok(())
}
@ -69,20 +69,20 @@ pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
pub(crate) async fn get_post(post_id: i32, pool: &DbPool) -> Result<Post, LemmyError> {
match blocking(pool, move |conn| Post::read(conn, post_id)).await? {
Ok(post) => Ok(post),
Err(_e) => Err(APIError::err("couldnt_find_post").into()),
Err(_e) => Err(ApiError::err("couldnt_find_post").into()),
}
}
pub(crate) async fn get_user_from_jwt(jwt: &str, pool: &DbPool) -> Result<User_, LemmyError> {
let claims = match Claims::decode(&jwt) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
Err(_e) => return Err(ApiError::err("not_logged_in").into()),
};
let user_id = claims.id;
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
// Check for a site ban
if user.banned {
return Err(APIError::err("site_ban").into());
return Err(ApiError::err("site_ban").into());
}
Ok(user)
}
@ -103,13 +103,13 @@ pub(crate) async fn get_user_safe_settings_from_jwt(
) -> Result<UserSafeSettings, LemmyError> {
let claims = match Claims::decode(&jwt) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
Err(_e) => return Err(ApiError::err("not_logged_in").into()),
};
let user_id = claims.id;
let user = blocking(pool, move |conn| UserSafeSettings::read(conn, user_id)).await??;
// Check for a site ban
if user.banned {
return Err(APIError::err("site_ban").into());
return Err(ApiError::err("site_ban").into());
}
Ok(user)
}
@ -131,7 +131,7 @@ pub(crate) async fn check_community_ban(
) -> Result<(), LemmyError> {
let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
Err(APIError::err("community_ban").into())
Err(ApiError::err("community_ban").into())
} else {
Ok(())
}
@ -141,7 +141,7 @@ pub(crate) async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result
if score == -1 {
let site = blocking(pool, move |conn| Site::read_simple(conn)).await??;
if !site.enable_downvotes {
return Err(APIError::err("downvotes_disabled").into());
return Err(ApiError::err("downvotes_disabled").into());
}
}
Ok(())
@ -175,7 +175,7 @@ pub(crate) async fn collect_moderated_communities(
pub(crate) fn check_optional_url(item: &Option<Option<String>>) -> Result<(), LemmyError> {
if let Some(Some(item)) = &item {
if Url::parse(item).is_err() {
return Err(APIError::err("invalid_url").into());
return Err(ApiError::err("invalid_url").into());
}
}
Ok(())
@ -298,9 +298,6 @@ pub async fn match_websocket_operation(
UserOperation::TransferSite => {
do_websocket_operation::<TransferSite>(context, id, op, data).await
}
UserOperation::ListCategories => {
do_websocket_operation::<ListCategories>(context, id, op, data).await
}
// Community ops
UserOperation::GetCommunity => {

View file

@ -40,7 +40,7 @@ use lemmy_structs::{blocking, post::*};
use lemmy_utils::{
request::fetch_iframely_and_pictrs_data,
utils::{check_slurs, check_slurs_opt, is_valid_post_title},
APIError,
ApiError,
ConnectionId,
LemmyError,
};
@ -67,7 +67,7 @@ impl Perform for CreatePost {
check_slurs_opt(&data.body)?;
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
return Err(ApiError::err("invalid_post_title").into());
}
check_community_ban(user.id, data.community_id, context.pool()).await?;
@ -109,7 +109,7 @@ impl Perform for CreatePost {
"couldnt_create_post"
};
return Err(APIError::err(err_type).into());
return Err(ApiError::err(err_type).into());
}
};
@ -121,7 +121,7 @@ impl Perform for CreatePost {
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_post").into()),
};
updated_post.send_create(&user, context).await?;
@ -135,7 +135,7 @@ impl Perform for CreatePost {
let like = move |conn: &'_ _| PostLike::like(conn, &like_form);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_post").into());
return Err(ApiError::err("couldnt_like_post").into());
}
updated_post.send_like(&user, context).await?;
@ -148,7 +148,7 @@ impl Perform for CreatePost {
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let res = PostResponse { post_view };
@ -183,7 +183,7 @@ impl Perform for GetPost {
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let id = data.id;
@ -209,7 +209,7 @@ impl Perform for GetPost {
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_community").into()),
};
let online = context
@ -273,7 +273,7 @@ impl Perform for GetPosts {
.await?
{
Ok(posts) => posts,
Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
Err(_e) => return Err(ApiError::err("couldnt_get_posts").into()),
};
Ok(GetPostsResponse { posts })
@ -320,7 +320,7 @@ impl Perform for CreatePostLike {
let like_form2 = like_form.clone();
let like = move |conn: &'_ _| PostLike::like(conn, &like_form2);
if blocking(context.pool(), like).await?.is_err() {
return Err(APIError::err("couldnt_like_post").into());
return Err(ApiError::err("couldnt_like_post").into());
}
if like_form.score == 1 {
@ -340,7 +340,7 @@ impl Perform for CreatePostLike {
.await?
{
Ok(post) => post,
Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_post").into()),
};
let res = PostResponse { post_view };
@ -371,7 +371,7 @@ impl Perform for EditPost {
check_slurs_opt(&data.body)?;
if !is_valid_post_title(&data.name) {
return Err(APIError::err("invalid_post_title").into());
return Err(ApiError::err("invalid_post_title").into());
}
let post_id = data.post_id;
@ -381,7 +381,7 @@ impl Perform for EditPost {
// Verify that only the creator can edit
if !Post::is_post_creator(user.id, orig_post.creator_id) {
return Err(APIError::err("no_post_edit_allowed").into());
return Err(ApiError::err("no_post_edit_allowed").into());
}
// Fetch Iframely and Pictrs cached image
@ -423,7 +423,7 @@ impl Perform for EditPost {
"couldnt_update_post"
};
return Err(APIError::err(err_type).into());
return Err(ApiError::err(err_type).into());
}
};
@ -467,7 +467,7 @@ impl Perform for DeletePost {
// Verify that only the creator can delete
if !Post::is_post_creator(user.id, orig_post.creator_id) {
return Err(APIError::err("no_post_edit_allowed").into());
return Err(ApiError::err("no_post_edit_allowed").into());
}
// Update the post
@ -711,12 +711,12 @@ impl Perform for SavePost {
if data.save {
let save = move |conn: &'_ _| PostSaved::save(conn, &post_saved_form);
if blocking(context.pool(), save).await?.is_err() {
return Err(APIError::err("couldnt_save_post").into());
return Err(ApiError::err("couldnt_save_post").into());
}
} else {
let unsave = move |conn: &'_ _| PostSaved::unsave(conn, &post_saved_form);
if blocking(context.pool(), unsave).await?.is_err() {
return Err(APIError::err("couldnt_save_post").into());
return Err(ApiError::err("couldnt_save_post").into());
}
}
@ -747,10 +747,10 @@ impl Perform for CreatePostReport {
// check size of report and check for whitespace
let reason = data.reason.trim();
if reason.is_empty() {
return Err(APIError::err("report_reason_required").into());
return Err(ApiError::err("report_reason_required").into());
}
if reason.len() > 1000 {
return Err(APIError::err("report_too_long").into());
if reason.chars().count() > 1000 {
return Err(ApiError::err("report_too_long").into());
}
let user_id = user.id;
@ -777,7 +777,7 @@ impl Perform for CreatePostReport {
.await?
{
Ok(report) => report,
Err(_e) => return Err(APIError::err("couldnt_create_report").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_report").into()),
};
let res = CreatePostReportResponse { success: true };
@ -837,7 +837,7 @@ impl Perform for ResolvePostReport {
};
if blocking(context.pool(), resolve_fun).await?.is_err() {
return Err(APIError::err("couldnt_resolve_report").into());
return Err(ApiError::err("couldnt_resolve_report").into());
};
context.chat_server().do_send(SendModRoomMessage {

View file

@ -22,11 +22,6 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("/config", web::get().to(route_get::<GetSiteConfig>))
.route("/config", web::put().to(route_post::<SaveSiteConfig>)),
)
.service(
web::resource("/categories")
.wrap(rate_limit.message())
.route(web::get().to(route_get::<ListCategories>)),
)
.service(
web::resource("/modlog")
.wrap(rate_limit.message())

View file

@ -10,17 +10,10 @@ use crate::{
use actix_web::web::Data;
use anyhow::Context;
use lemmy_apub::fetcher::search::search_by_apub_id;
use lemmy_db_queries::{
diesel_option_overwrite,
source::{category::Category_, site::Site_},
Crud,
SearchType,
SortType,
};
use lemmy_db_queries::{diesel_option_overwrite, source::site::Site_, Crud, SearchType, SortType};
use lemmy_db_schema::{
naive_now,
source::{
category::Category,
moderator::*,
site::{Site, *},
},
@ -51,7 +44,7 @@ use lemmy_utils::{
settings::Settings,
utils::{check_slurs, check_slurs_opt},
version,
APIError,
ApiError,
ConnectionId,
LemmyError,
};
@ -63,24 +56,6 @@ use lemmy_websocket::{
use log::{debug, info};
use std::str::FromStr;
#[async_trait::async_trait(?Send)]
impl Perform for ListCategories {
type Response = ListCategoriesResponse;
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<ListCategoriesResponse, LemmyError> {
let _data: &ListCategories = &self;
let categories = blocking(context.pool(), move |conn| Category::list_all(conn)).await??;
// Return the jwt
Ok(ListCategoriesResponse { categories })
}
}
#[async_trait::async_trait(?Send)]
impl Perform for GetModlog {
type Response = GetModlogResponse;
@ -168,7 +143,7 @@ impl Perform for CreateSite {
let read_site = move |conn: &'_ _| Site::read_simple(conn);
if blocking(context.pool(), read_site).await?.is_ok() {
return Err(APIError::err("site_already_exists").into());
return Err(ApiError::err("site_already_exists").into());
};
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
@ -193,7 +168,7 @@ impl Perform for CreateSite {
let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
if blocking(context.pool(), create_site).await?.is_err() {
return Err(APIError::err("site_already_exists").into());
return Err(ApiError::err("site_already_exists").into());
}
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
@ -238,7 +213,7 @@ impl Perform for EditSite {
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
if blocking(context.pool(), update_site).await?.is_err() {
return Err(APIError::err("couldnt_update_site").into());
return Err(ApiError::err("couldnt_update_site").into());
}
let site_view = blocking(context.pool(), move |conn| SiteView::read(conn)).await??;
@ -525,13 +500,13 @@ impl Perform for TransferSite {
// Make sure user is the creator
if read_site.creator_id != user.id {
return Err(APIError::err("not_an_admin").into());
return Err(ApiError::err("not_an_admin").into());
}
let new_creator_id = data.user_id;
let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
if blocking(context.pool(), transfer_site).await?.is_err() {
return Err(APIError::err("couldnt_update_site").into());
return Err(ApiError::err("couldnt_update_site").into());
};
// Mod tables
@ -608,7 +583,7 @@ impl Perform for SaveSiteConfig {
// Make sure docker doesn't have :ro at the end of the volume, so its not a read-only filesystem
let config_hjson = match Settings::save_config_file(&data.config_hjson) {
Ok(config_hjson) => config_hjson,
Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_site").into()),
};
Ok(GetSiteConfigResponse { config_hjson })

View file

@ -80,7 +80,7 @@ use lemmy_utils::{
naive_from_unix,
remove_slurs,
},
APIError,
ApiError,
ConnectionId,
LemmyError,
};
@ -110,13 +110,13 @@ impl Perform for Login {
.await?
{
Ok(user) => user,
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_that_username_or_email").into()),
};
// Verify the password
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err("password_incorrect").into());
return Err(ApiError::err("password_incorrect").into());
}
// Return the jwt
@ -140,18 +140,18 @@ impl Perform for Register {
// Make sure site has open registration
if let Ok(site) = blocking(context.pool(), move |conn| Site::read_simple(conn)).await? {
if !site.open_registration {
return Err(APIError::err("registration_closed").into());
return Err(ApiError::err("registration_closed").into());
}
}
// Password length check
if data.password.len() > 60 {
return Err(APIError::err("invalid_password").into());
return Err(ApiError::err("invalid_password").into());
}
// Make sure passwords match
if data.password != data.password_verify {
return Err(APIError::err("passwords_dont_match").into());
return Err(ApiError::err("passwords_dont_match").into());
}
// Check if there are admins. False if admins exist
@ -176,7 +176,7 @@ impl Perform for Register {
})
.await?;
if !check {
return Err(APIError::err("captcha_incorrect").into());
return Err(ApiError::err("captcha_incorrect").into());
}
}
@ -184,7 +184,7 @@ impl Perform for Register {
let user_keypair = generate_actor_keypair()?;
if !is_valid_username(&data.username) {
return Err(APIError::err("invalid_username").into());
return Err(ApiError::err("invalid_username").into());
}
let user_actor_id = generate_apub_endpoint(EndpointType::User, &data.username)?;
@ -234,7 +234,7 @@ impl Perform for Register {
"user_already_exists"
};
return Err(APIError::err(err_type).into());
return Err(ApiError::err(err_type).into());
}
};
@ -251,7 +251,6 @@ impl Perform for Register {
name: default_community_name.to_string(),
title: "The Default Community".to_string(),
description: Some("The Default Community".to_string()),
category_id: 1,
nsfw: false,
creator_id: inserted_user.id,
removed: None,
@ -285,7 +284,7 @@ impl Perform for Register {
let follow = move |conn: &'_ _| CommunityFollower::follow(conn, &community_follower_form);
if blocking(context.pool(), follow).await?.is_err() {
return Err(APIError::err("community_follower_already_exists").into());
return Err(ApiError::err("community_follower_already_exists").into());
};
// If its an admin, add them as a mod and follower to main
@ -297,7 +296,7 @@ impl Perform for Register {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
if blocking(context.pool(), join).await?.is_err() {
return Err(APIError::err("community_moderator_already_exists").into());
return Err(ApiError::err("community_moderator_already_exists").into());
}
}
@ -380,13 +379,13 @@ impl Perform for SaveUserSettings {
if let Some(Some(bio)) = &bio {
if bio.chars().count() > 300 {
return Err(APIError::err("bio_length_overflow").into());
return Err(ApiError::err("bio_length_overflow").into());
}
}
if let Some(Some(preferred_username)) = &preferred_username {
if !is_valid_preferred_username(preferred_username.trim()) {
return Err(APIError::err("invalid_username").into());
return Err(ApiError::err("invalid_username").into());
}
}
@ -397,7 +396,7 @@ impl Perform for SaveUserSettings {
Some(new_password_verify) => {
// Make sure passwords match
if new_password != new_password_verify {
return Err(APIError::err("passwords_dont_match").into());
return Err(ApiError::err("passwords_dont_match").into());
}
// Check the old password
@ -405,7 +404,7 @@ impl Perform for SaveUserSettings {
Some(old_password) => {
let valid: bool = verify(old_password, &user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err("password_incorrect").into());
return Err(ApiError::err("password_incorrect").into());
}
let new_password = new_password.to_owned();
let user = blocking(context.pool(), move |conn| {
@ -414,10 +413,10 @@ impl Perform for SaveUserSettings {
.await??;
user.password_encrypted
}
None => return Err(APIError::err("password_incorrect").into()),
None => return Err(ApiError::err("password_incorrect").into()),
}
}
None => return Err(APIError::err("passwords_dont_match").into()),
None => return Err(ApiError::err("passwords_dont_match").into()),
}
}
None => user.password_encrypted,
@ -470,7 +469,7 @@ impl Perform for SaveUserSettings {
"user_already_exists"
};
return Err(APIError::err(err_type).into());
return Err(ApiError::err(err_type).into());
}
};
@ -513,7 +512,7 @@ impl Perform for GetUserDetails {
.await?;
match user {
Ok(user) => user.id,
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_that_username_or_email").into()),
}
}
};
@ -563,10 +562,15 @@ impl Perform for GetUserDetails {
})
.await??;
let follows = blocking(context.pool(), move |conn| {
CommunityFollowerView::for_user(conn, user_details_id)
})
.await??;
let mut follows = vec![];
if let Some(uid) = user_id {
if uid == user_details_id {
follows = blocking(context.pool(), move |conn| {
CommunityFollowerView::for_user(conn, user_details_id)
})
.await??;
}
};
let moderates = blocking(context.pool(), move |conn| {
CommunityModeratorView::for_user(conn, user_details_id)
})
@ -602,7 +606,7 @@ impl Perform for AddAdmin {
let added_user_id = data.user_id;
let add_admin = move |conn: &'_ _| User_::add_admin(conn, added_user_id, added);
if blocking(context.pool(), add_admin).await?.is_err() {
return Err(APIError::err("couldnt_update_user").into());
return Err(ApiError::err("couldnt_update_user").into());
}
// Mod tables
@ -658,7 +662,7 @@ impl Perform for BanUser {
let banned_user_id = data.user_id;
let ban_user = move |conn: &'_ _| User_::ban_user(conn, banned_user_id, ban);
if blocking(context.pool(), ban_user).await?.is_err() {
return Err(APIError::err("couldnt_update_user").into());
return Err(ApiError::err("couldnt_update_user").into());
}
// Remove their data if that's desired
@ -806,14 +810,14 @@ impl Perform for MarkUserMentionAsRead {
.await??;
if user.id != read_user_mention.recipient_id {
return Err(APIError::err("couldnt_update_comment").into());
return Err(ApiError::err("couldnt_update_comment").into());
}
let user_mention_id = read_user_mention.id;
let read = data.read;
let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read);
if blocking(context.pool(), update_mention).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
return Err(ApiError::err("couldnt_update_comment").into());
};
let user_mention_id = read_user_mention.id;
@ -858,7 +862,7 @@ impl Perform for MarkAllAsRead {
let reply_id = comment_view.comment.id;
let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
if blocking(context.pool(), mark_as_read).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
return Err(ApiError::err("couldnt_update_comment").into());
}
}
@ -868,13 +872,13 @@ impl Perform for MarkAllAsRead {
.await?
.is_err()
{
return Err(APIError::err("couldnt_update_comment").into());
return Err(ApiError::err("couldnt_update_comment").into());
}
// Mark all private_messages as read
let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id);
if blocking(context.pool(), update_pm).await?.is_err() {
return Err(APIError::err("couldnt_update_private_message").into());
return Err(ApiError::err("couldnt_update_private_message").into());
}
Ok(GetRepliesResponse { replies: vec![] })
@ -896,20 +900,20 @@ impl Perform for DeleteAccount {
// Verify the password
let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
if !valid {
return Err(APIError::err("password_incorrect").into());
return Err(ApiError::err("password_incorrect").into());
}
// Comments
let user_id = user.id;
let permadelete = move |conn: &'_ _| Comment::permadelete_for_creator(conn, user_id);
if blocking(context.pool(), permadelete).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
return Err(ApiError::err("couldnt_update_comment").into());
}
// Posts
let permadelete = move |conn: &'_ _| Post::permadelete_for_creator(conn, user_id);
if blocking(context.pool(), permadelete).await?.is_err() {
return Err(APIError::err("couldnt_update_post").into());
return Err(ApiError::err("couldnt_update_post").into());
}
blocking(context.pool(), move |conn| {
@ -942,7 +946,7 @@ impl Perform for PasswordReset {
.await?
{
Ok(user) => user,
Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
Err(_e) => return Err(ApiError::err("couldnt_find_that_username_or_email").into()),
};
// Generate a random token
@ -964,7 +968,7 @@ impl Perform for PasswordReset {
let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
match send_email(subject, user_email, &user.name, html) {
Ok(_o) => _o,
Err(_e) => return Err(APIError::err(&_e).into()),
Err(_e) => return Err(ApiError::err(&_e).into()),
};
Ok(PasswordResetResponse {})
@ -991,7 +995,7 @@ impl Perform for PasswordChange {
// Make sure passwords match
if data.password != data.password_verify {
return Err(APIError::err("passwords_dont_match").into());
return Err(ApiError::err("passwords_dont_match").into());
}
// Update the user with the new password
@ -1002,7 +1006,7 @@ impl Perform for PasswordChange {
.await?
{
Ok(user) => user,
Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_user").into()),
};
// Return the jwt
@ -1045,7 +1049,7 @@ impl Perform for CreatePrivateMessage {
{
Ok(private_message) => private_message,
Err(_e) => {
return Err(APIError::err("couldnt_create_private_message").into());
return Err(ApiError::err("couldnt_create_private_message").into());
}
};
@ -1067,7 +1071,7 @@ impl Perform for CreatePrivateMessage {
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_create_private_message").into()),
Err(_e) => return Err(ApiError::err("couldnt_create_private_message").into()),
};
updated_private_message.send_create(&user, context).await?;
@ -1124,7 +1128,7 @@ impl Perform for EditPrivateMessage {
})
.await??;
if user.id != orig_private_message.creator_id {
return Err(APIError::err("no_private_message_edit_allowed").into());
return Err(ApiError::err("no_private_message_edit_allowed").into());
}
// Doing the update
@ -1136,7 +1140,7 @@ impl Perform for EditPrivateMessage {
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
};
// Send the apub update
@ -1183,7 +1187,7 @@ impl Perform for DeletePrivateMessage {
})
.await??;
if user.id != orig_private_message.creator_id {
return Err(APIError::err("no_private_message_edit_allowed").into());
return Err(ApiError::err("no_private_message_edit_allowed").into());
}
// Doing the update
@ -1195,7 +1199,7 @@ impl Perform for DeletePrivateMessage {
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
};
// Send the apub update
@ -1248,7 +1252,7 @@ impl Perform for MarkPrivateMessageAsRead {
})
.await??;
if user.id != orig_private_message.recipient_id {
return Err(APIError::err("couldnt_update_private_message").into());
return Err(ApiError::err("couldnt_update_private_message").into());
}
// Doing the update
@ -1260,7 +1264,7 @@ impl Perform for MarkPrivateMessageAsRead {
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
Err(_e) => return Err(ApiError::err("couldnt_update_private_message").into()),
};
// No need to send an apub update

View file

@ -1,41 +1,19 @@
use activitystreams::unparsed::UnparsedMutExt;
use activitystreams_ext::UnparsedExtension;
use diesel::PgConnection;
use lemmy_db_queries::Crud;
use lemmy_db_schema::source::category::Category;
use lemmy_utils::LemmyError;
use serde::{Deserialize, Serialize};
/// Activitystreams extension to allow (de)serializing additional Community fields `category` and
/// Activitystreams extension to allow (de)serializing additional Community field
/// `sensitive` (called 'nsfw' in Lemmy).
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupExtension {
pub category: Option<GroupCategory>,
pub sensitive: Option<bool>,
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GroupCategory {
// Using a string because that's how Peertube does it.
pub identifier: String,
pub name: String,
}
impl GroupExtension {
pub fn new(
conn: &PgConnection,
category_id: i32,
sensitive: bool,
) -> Result<GroupExtension, LemmyError> {
let category = Category::read(conn, category_id)?;
let group_category = GroupCategory {
identifier: category_id.to_string(),
name: category.name,
};
pub fn new(sensitive: bool) -> Result<GroupExtension, LemmyError> {
Ok(GroupExtension {
category: Some(group_category),
sensitive: Some(sensitive),
})
}
@ -49,13 +27,11 @@ where
fn try_from_unparsed(unparsed_mut: &mut U) -> Result<Self, Self::Error> {
Ok(GroupExtension {
category: unparsed_mut.remove("category")?,
sensitive: unparsed_mut.remove("sensitive")?,
})
}
fn try_into_unparsed(self, unparsed_mut: &mut U) -> Result<(), Self::Error> {
unparsed_mut.insert("category", self.category)?;
unparsed_mut.insert("sensitive", self.sensitive)?;
Ok(())
}

View file

@ -41,8 +41,8 @@ use log::debug;
use url::Url;
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[serde(untagged)]
#[derive(serde::Deserialize, Debug)]
#[serde(untagged)]
enum SearchAcceptedObjects {
Person(Box<PersonExt>),
Group(Box<GroupExt>),

View file

@ -120,7 +120,7 @@ pub(crate) async fn community_receive_message(
User_::read_from_apub_id(&conn, &actor_id.into())
})
.await??;
check_community_or_site_ban(&user, &to_community, context.pool()).await?;
check_community_or_site_ban(&user, to_community.id, context.pool()).await?;
let any_base = activity.clone().into_any_base()?;
let actor_url = actor.actor_id();
@ -261,14 +261,13 @@ async fn handle_undo_follow(
pub(crate) async fn check_community_or_site_ban(
user: &User_,
community: &Community,
community_id: i32,
pool: &DbPool,
) -> Result<(), LemmyError> {
if user.banned {
return Err(anyhow!("User is banned from site").into());
}
let user_id = user.id;
let community_id = community.id;
let is_banned = move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(anyhow!("User is banned from community").into());

View file

@ -48,8 +48,15 @@ use lemmy_db_schema::source::site::Site;
use lemmy_structs::blocking;
use lemmy_utils::{location_info, LemmyError};
use lemmy_websocket::LemmyContext;
use strum_macros::EnumString;
use url::Url;
#[derive(EnumString)]
enum PageOrNote {
Page,
Note,
}
/// This file is for post/comment activities received by the community, and for post/comment
/// activities announced by the community and received by the user.
@ -64,9 +71,13 @@ pub(in crate::inbox) async fn receive_create_for_community(
verify_activity_domains_valid(&create, &expected_domain, true)?;
is_addressed_to_public(&create)?;
match create.object().as_single_kind_str() {
Some("Page") => receive_create_post(create, context, request_counter).await,
Some("Note") => receive_create_comment(create, context, request_counter).await,
let kind = create
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
match kind {
Some(PageOrNote::Page) => receive_create_post(create, context, request_counter).await,
Some(PageOrNote::Note) => receive_create_comment(create, context, request_counter).await,
_ => receive_unhandled_activity(create),
}
}
@ -82,9 +93,13 @@ pub(in crate::inbox) async fn receive_update_for_community(
verify_activity_domains_valid(&update, &expected_domain, true)?;
is_addressed_to_public(&update)?;
match update.object().as_single_kind_str() {
Some("Page") => receive_update_post(update, context, request_counter).await,
Some("Note") => receive_update_comment(update, context, request_counter).await,
let kind = update
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
match kind {
Some(PageOrNote::Page) => receive_update_post(update, context, request_counter).await,
Some(PageOrNote::Note) => receive_update_comment(update, context, request_counter).await,
_ => receive_unhandled_activity(update),
}
}
@ -201,6 +216,14 @@ pub(in crate::inbox) async fn receive_remove_for_community(
}
}
#[derive(EnumString)]
enum UndoableActivities {
Delete,
Remove,
Like,
Dislike,
}
/// A post/comment action being reverted (either a delete, remove, upvote or downvote)
pub(in crate::inbox) async fn receive_undo_for_community(
context: &LemmyContext,
@ -212,13 +235,18 @@ pub(in crate::inbox) async fn receive_undo_for_community(
verify_activity_domains_valid(&undo, &expected_domain.to_owned(), true)?;
is_addressed_to_public(&undo)?;
match undo.object().as_single_kind_str() {
Some("Delete") => receive_undo_delete_for_community(context, undo, expected_domain).await,
Some("Remove") => receive_undo_remove_for_community(context, undo, expected_domain).await,
Some("Like") => {
use UndoableActivities::*;
match undo
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok())
{
Some(Delete) => receive_undo_delete_for_community(context, undo, expected_domain).await,
Some(Remove) => receive_undo_remove_for_community(context, undo, expected_domain).await,
Some(Like) => {
receive_undo_like_for_community(context, undo, expected_domain, request_counter).await
}
Some("Dislike") => {
Some(Dislike) => {
receive_undo_dislike_for_community(context, undo, expected_domain, request_counter).await
}
_ => receive_unhandled_activity(undo),

View file

@ -60,6 +60,7 @@ use lemmy_websocket::LemmyContext;
use log::debug;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use strum_macros::EnumString;
use url::Url;
/// Allowed activities for user inbox.
@ -235,6 +236,17 @@ async fn receive_accept(
Ok(())
}
#[derive(EnumString)]
enum AnnouncableActivities {
Create,
Update,
Like,
Dislike,
Delete,
Remove,
Undo,
}
/// Takes an announce and passes the inner activity to the appropriate handler.
pub async fn receive_announce(
context: &LemmyContext,
@ -246,7 +258,10 @@ pub async fn receive_announce(
verify_activity_domains_valid(&announce, &actor.actor_id(), false)?;
is_addressed_to_public(&announce)?;
let kind = announce.object().as_single_kind_str();
let kind = announce
.object()
.as_single_kind_str()
.and_then(|s| s.parse().ok());
let inner_activity = announce
.object()
.to_owned()
@ -259,22 +274,23 @@ pub async fn receive_announce(
return Ok(());
}
use AnnouncableActivities::*;
match kind {
Some("Create") => {
Some(Create) => {
receive_create_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some("Update") => {
Some(Update) => {
receive_update_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some("Like") => {
Some(Like) => {
receive_like_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some("Dislike") => {
Some(Dislike) => {
receive_dislike_for_community(context, inner_activity, &inner_id, request_counter).await
}
Some("Delete") => receive_delete_for_community(context, inner_activity, &inner_id).await,
Some("Remove") => receive_remove_for_community(context, inner_activity, &inner_id).await,
Some("Undo") => {
Some(Delete) => receive_delete_for_community(context, inner_activity, &inner_id).await,
Some(Remove) => receive_remove_for_community(context, inner_activity, &inner_id).await,
Some(Undo) => {
receive_undo_for_community(context, inner_activity, &inner_id, request_counter).await
}
_ => receive_unhandled_activity(inner_activity),

View file

@ -18,12 +18,12 @@ use crate::{
use activitystreams::{
object::{kind::NoteType, ApObject, Note, Tombstone},
prelude::*,
public,
};
use anyhow::{anyhow, Context};
use lemmy_db_queries::{Crud, DbPool};
use lemmy_db_schema::source::{
comment::{Comment, CommentForm},
community::Community,
post::Post,
user::User_,
};
@ -49,9 +49,6 @@ impl ToApub for Comment {
let post_id = self.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let community = blocking(pool, move |conn| Community::read(conn, community_id)).await??;
// Add a vector containing some important info to the "in_reply_to" field
// [post_ap_id, Option(parent_comment_ap_id)]
let mut in_reply_to_vec = vec![post.ap_id.into_inner()];
@ -67,7 +64,7 @@ impl ToApub for Comment {
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
.set_published(convert_datetime(self.published))
.set_to(community.actor_id.into_inner())
.set_to(public())
.set_many_in_reply_tos(in_reply_to_vec)
.set_attributed_to(creator.actor_id.into_inner());
@ -103,13 +100,13 @@ impl FromApub for Comment {
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Comment, LemmyError> {
check_object_for_community_or_site_ban(note, context, request_counter).await?;
let comment: Comment =
get_object_from_apub(note, context, expected_domain, request_counter).await?;
let post_id = comment.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
check_object_for_community_or_site_ban(note, post.community_id, context, request_counter)
.await?;
if post.locked {
// This is not very efficient because a comment gets inserted just to be deleted right
// afterwards, but it seems to be the easiest way to implement it.

View file

@ -93,16 +93,9 @@ impl ToApub for Community {
..Default::default()
});
let nsfw = self.nsfw;
let category_id = self.category_id;
let group_extension = blocking(pool, move |conn| {
GroupExtension::new(conn, category_id, nsfw)
})
.await??;
Ok(Ext2::new(
ap_actor,
group_extension,
GroupExtension::new(self.nsfw)?,
self.get_public_key_ext()?,
))
}
@ -207,13 +200,6 @@ impl FromApubToForm<GroupExt> for CommunityForm {
name,
title,
description,
category_id: group
.ext_one
.category
.clone()
.map(|c| c.identifier.parse::<i32>().ok())
.flatten()
.unwrap_or(1),
creator_id: creator.id,
removed: None,
published: group.inner.published().map(|u| u.to_owned().naive_local()),

View file

@ -11,7 +11,9 @@ use activitystreams::{
};
use anyhow::{anyhow, Context};
use chrono::NaiveDateTime;
use diesel::result::Error::NotFound;
use lemmy_db_queries::{ApubObject, Crud, DbPool};
use lemmy_db_schema::source::community::Community;
use lemmy_structs::blocking;
use lemmy_utils::{location_info, settings::Settings, utils::convert_datetime, LemmyError};
use lemmy_websocket::LemmyContext;
@ -205,6 +207,7 @@ where
pub(in crate::objects) async fn check_object_for_community_or_site_ban<T, Kind>(
object: &T,
community_id: i32,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<(), LemmyError>
@ -217,11 +220,30 @@ where
.as_single_xsd_any_uri()
.context(location_info!())?;
let user = get_or_fetch_and_upsert_user(user_id, context, request_counter).await?;
let community_id = object
check_community_or_site_ban(&user, community_id, context.pool()).await
}
pub(in crate::objects) async fn get_to_community<T, Kind>(
object: &T,
context: &LemmyContext,
request_counter: &mut i32,
) -> Result<Community, LemmyError>
where
T: ObjectExt<Kind>,
{
let community_ids = object
.to()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let community = get_or_fetch_and_upsert_community(community_id, context, request_counter).await?;
check_community_or_site_ban(&user, &community, context.pool()).await
.as_many()
.context(location_info!())?
.iter()
.map(|a| a.as_xsd_any_uri().context(location_info!()))
.collect::<Result<Vec<&Url>, anyhow::Error>>()?;
for cid in community_ids {
let community = get_or_fetch_and_upsert_community(&cid, context, request_counter).await;
if community.is_ok() {
return community;
}
}
Err(NotFound.into())
}

View file

@ -1,12 +1,13 @@
use crate::{
extensions::{context::lemmy_context, page_extension::PageExtension},
fetcher::{community::get_or_fetch_and_upsert_community, user::get_or_fetch_and_upsert_user},
fetcher::user::get_or_fetch_and_upsert_user,
objects::{
check_object_domain,
check_object_for_community_or_site_ban,
create_tombstone,
get_object_from_apub,
get_source_markdown_value,
get_to_community,
set_content_and_source,
FromApub,
FromApubToForm,
@ -17,6 +18,7 @@ use crate::{
use activitystreams::{
object::{kind::PageType, ApObject, Image, Page, Tombstone},
prelude::*,
public,
};
use activitystreams_ext::Ext1;
use anyhow::Context;
@ -56,11 +58,12 @@ impl ToApub for Post {
// https://git.asonix.dog/Aardwolf/activitystreams/issues/5
.set_many_contexts(lemmy_context()?)
.set_id(self.ap_id.to_owned().into_inner())
// Use summary field to be consistent with mastodon content warning.
// https://mastodon.xyz/@Louisa/103987265222901387.json
.set_name(self.name.to_owned())
// `summary` field for compatibility with lemmy v0.9.9 and older,
// TODO: remove this after some time
.set_summary(self.name.to_owned())
.set_published(convert_datetime(self.published))
.set_to(community.actor_id.into_inner())
.set_many_tos(vec![community.actor_id.into_inner(), public()])
.set_attributed_to(creator.actor_id.into_inner());
if let Some(body) = &self.body {
@ -115,8 +118,10 @@ impl FromApub for Post {
expected_domain: Url,
request_counter: &mut i32,
) -> Result<Post, LemmyError> {
check_object_for_community_or_site_ban(page, context, request_counter).await?;
get_object_from_apub(page, context, expected_domain, request_counter).await
let post: Post = get_object_from_apub(page, context, expected_domain, request_counter).await?;
check_object_for_community_or_site_ban(page, post.community_id, context, request_counter)
.await?;
Ok(post)
}
}
@ -139,16 +144,7 @@ impl FromApubToForm<PageExt> for PostForm {
let creator = get_or_fetch_and_upsert_user(creator_actor_id, context, request_counter).await?;
let community_actor_id = page
.inner
.to()
.as_ref()
.context(location_info!())?
.as_single_xsd_any_uri()
.context(location_info!())?;
let community =
get_or_fetch_and_upsert_community(community_actor_id, context, request_counter).await?;
let community = get_to_community(page, context, request_counter).await?;
let thumbnail_url = match &page.inner.image() {
Some(any_image) => Image::from_any_base(
@ -181,8 +177,11 @@ impl FromApubToForm<PageExt> for PostForm {
let name = page
.inner
.summary()
.as_ref()
.name()
.map(|s| s.map(|s2| s2.to_owned()))
// The following is for compatibility with lemmy v0.9.9 and older
// TODO: remove it after some time (along with the map above)
.or_else(|| page.inner.summary().map(|s| s.to_owned()))
.context(location_info!())?
.as_single_xsd_string()
.context(location_info!())?

View file

@ -109,7 +109,6 @@ mod tests {
creator_id: inserted_user.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,

View file

@ -113,7 +113,6 @@ mod tests {
creator_id: inserted_user.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -138,7 +137,6 @@ mod tests {
creator_id: inserted_user.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,

View file

@ -13,6 +13,7 @@ pub struct PostAggregates {
pub downvotes: i64,
pub stickied: bool,
pub published: chrono::NaiveDateTime,
pub newest_comment_time_necro: chrono::NaiveDateTime, // A newest comment time, limited to 2 days, to prevent necrobumping
pub newest_comment_time: chrono::NaiveDateTime,
}
@ -112,7 +113,6 @@ mod tests {
creator_id: inserted_user.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,

View file

@ -94,7 +94,6 @@ mod tests {
creator_id: inserted_user.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,

View file

@ -109,7 +109,6 @@ mod tests {
creator_id: inserted_user.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,

View file

@ -165,6 +165,7 @@ pub enum SortType {
TopYear,
TopAll,
MostComments,
NewComments,
}
#[derive(EnumString, ToString, Debug, Serialize, Deserialize, Clone)]

View file

@ -110,7 +110,8 @@ impl Activity_ for Activity {
.sql(" AND activity.data -> 'object' ->> 'type' = 'Create'")
.sql(" AND activity.data -> 'object' -> 'object' ->> 'type' = 'Page'")
.sql(" AND activity.data ->> 'actor' = ")
.bind::<Text, _>(community_actor_id),
.bind::<Text, _>(community_actor_id)
.sql(" ORDER BY activity.published DESC"),
)
.limit(20)
.get_results(conn)?;

View file

@ -1,54 +0,0 @@
use crate::Crud;
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{schema::category::dsl::*, source::category::*};
impl Crud<CategoryForm> for Category {
fn read(conn: &PgConnection, category_id: i32) -> Result<Self, Error> {
category.find(category_id).first::<Self>(conn)
}
fn create(conn: &PgConnection, new_category: &CategoryForm) -> Result<Self, Error> {
insert_into(category)
.values(new_category)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
category_id: i32,
new_category: &CategoryForm,
) -> Result<Self, Error> {
diesel::update(category.find(category_id))
.set(new_category)
.get_result::<Self>(conn)
}
}
pub trait Category_ {
fn list_all(conn: &PgConnection) -> Result<Vec<Category>, Error>;
}
impl Category_ for Category {
fn list_all(conn: &PgConnection) -> Result<Vec<Category>, Error> {
category.load::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{establish_unpooled_connection, source::category::Category_};
use lemmy_db_schema::source::category::Category;
#[test]
fn test_crud() {
let conn = establish_unpooled_connection();
let categories = Category::list_all(&conn).unwrap();
let expected_first_category = Category {
id: 1,
name: "Discussion".into(),
};
assert_eq!(expected_first_category, categories[0]);
}
}

View file

@ -252,7 +252,6 @@ mod tests {
name: "test community".to_string(),
title: "nada".to_owned(),
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
deleted: None,

View file

@ -24,7 +24,6 @@ mod safe_type {
name,
title,
description,
category_id,
creator_id,
removed,
published,
@ -45,7 +44,6 @@ mod safe_type {
name,
title,
description,
category_id,
creator_id,
removed,
published,
@ -383,7 +381,6 @@ mod tests {
creator_id: inserted_user.id,
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: None,
deleted: None,
@ -409,7 +406,6 @@ mod tests {
name: "TIL".into(),
title: "nada".to_owned(),
description: None,
category_id: 1,
nsfw: false,
removed: false,
deleted: false,

View file

@ -1,5 +1,4 @@
pub mod activity;
pub mod category;
pub mod comment;
pub mod comment_report;
pub mod community;

View file

@ -271,7 +271,6 @@ mod tests {
name: "mod_community".to_string(),
title: "nada".to_owned(),
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
deleted: None,

View file

@ -271,7 +271,6 @@ mod tests {
name: "test community_3".to_string(),
title: "nada".to_owned(),
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
deleted: None,

View file

@ -152,7 +152,6 @@ mod tests {
name: "test community lake".to_string(),
title: "nada".to_owned(),
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
deleted: None,

View file

@ -10,13 +10,6 @@ table! {
}
}
table! {
category (id) {
id -> Int4,
name -> Varchar,
}
}
table! {
comment (id) {
id -> Int4,
@ -85,7 +78,6 @@ table! {
name -> Varchar,
title -> Varchar,
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
removed -> Bool,
published -> Timestamp,
@ -291,6 +283,7 @@ table! {
downvotes -> Int8,
stickied -> Bool,
published -> Timestamp,
newest_comment_time_necro -> Timestamp,
newest_comment_time -> Timestamp,
}
}
@ -545,7 +538,6 @@ joinable!(comment_like -> user_ (user_id));
joinable!(comment_report -> comment (comment_id));
joinable!(comment_saved -> comment (comment_id));
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));
@ -586,7 +578,6 @@ joinable!(user_mention -> user_ (recipient_id));
allow_tables_to_appear_in_same_query!(
activity,
category,
comment,
comment_aggregates,
comment_like,

View file

@ -1,15 +0,0 @@
use crate::schema::category;
use serde::Serialize;
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Clone)]
#[table_name = "category"]
pub struct Category {
pub id: i32,
pub name: String,
}
#[derive(Insertable, AsChangeset)]
#[table_name = "category"]
pub struct CategoryForm {
pub name: String,
}

View file

@ -11,7 +11,6 @@ pub struct Community {
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,
@ -38,7 +37,6 @@ pub struct CommunitySafe {
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,
@ -57,7 +55,6 @@ pub struct CommunityForm {
pub name: String,
pub title: String,
pub description: Option<String>,
pub category_id: i32,
pub creator_id: i32,
pub removed: Option<bool>,
pub published: Option<chrono::NaiveDateTime>,

View file

@ -1,5 +1,4 @@
pub mod activity;
pub mod category;
pub mod comment;
pub mod comment_report;
pub mod community;

View file

@ -380,7 +380,9 @@ impl<'a> CommentQueryBuilder<'a> {
SortType::Hot | SortType::Active => query
.order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
.then_order_by(comment_aggregates::published.desc()),
SortType::New | SortType::MostComments => query.order_by(comment::published.desc()),
SortType::New | SortType::MostComments | SortType::NewComments => {
query.order_by(comment::published.desc())
}
SortType::TopAll => query.order_by(comment_aggregates::score.desc()),
SortType::TopYear => query
.filter(comment::published.gt(now - 1.years()))
@ -481,7 +483,6 @@ mod tests {
name: "test community 5".to_string(),
title: "nada".to_owned(),
description: None,
category_id: 1,
creator_id: inserted_user.id,
removed: None,
deleted: None,
@ -623,7 +624,6 @@ mod tests {
title: "nada".to_owned(),
description: None,
creator_id: inserted_user.id,
category_id: 1,
updated: None,
banner: None,
published: inserted_community.published,

View file

@ -356,14 +356,19 @@ impl<'a> PostQueryBuilder<'a> {
query = match self.sort {
SortType::Active => query
.then_order_by(
hot_rank(post_aggregates::score, post_aggregates::newest_comment_time).desc(),
hot_rank(
post_aggregates::score,
post_aggregates::newest_comment_time_necro,
)
.desc(),
)
.then_order_by(post_aggregates::newest_comment_time.desc()),
.then_order_by(post_aggregates::newest_comment_time_necro.desc()),
SortType::Hot => query
.then_order_by(hot_rank(post_aggregates::score, post_aggregates::published).desc())
.then_order_by(post_aggregates::published.desc()),
SortType::New => query.then_order_by(post_aggregates::published.desc()),
SortType::MostComments => query.then_order_by(post_aggregates::comments.desc()),
SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
SortType::TopAll => query.then_order_by(post_aggregates::score.desc()),
SortType::TopYear => query
.filter(post::published.gt(now - 1.years()))
@ -474,7 +479,6 @@ mod tests {
title: "nada".to_owned(),
description: None,
creator_id: inserted_user.id,
category_id: 1,
removed: None,
deleted: None,
updated: None,
@ -609,7 +613,6 @@ mod tests {
title: "nada".to_owned(),
description: None,
creator_id: inserted_user.id,
category_id: 1,
updated: None,
banner: None,
published: inserted_community.published,
@ -623,6 +626,7 @@ mod tests {
downvotes: 0,
stickied: false,
published: agg.published,
newest_comment_time_necro: inserted_post.published,
newest_comment_time: inserted_post.published,
},
subscribed: false,

View file

@ -12,9 +12,8 @@ use lemmy_db_queries::{
ViewToVec,
};
use lemmy_db_schema::{
schema::{category, community, community_aggregates, community_follower, user_},
schema::{community, community_aggregates, community_follower, user_},
source::{
category::Category,
community::{Community, CommunityFollower, CommunitySafe},
user::{UserSafe, User_},
},
@ -25,7 +24,6 @@ use serde::Serialize;
pub struct CommunityView {
pub community: CommunitySafe,
pub creator: UserSafe,
pub category: Category,
pub subscribed: bool,
pub counts: CommunityAggregates,
}
@ -33,7 +31,6 @@ pub struct CommunityView {
type CommunityViewTuple = (
CommunitySafe,
UserSafe,
Category,
CommunityAggregates,
Option<CommunityFollower>,
);
@ -47,10 +44,9 @@ impl CommunityView {
// 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
let (community, creator, 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(
@ -62,7 +58,6 @@ impl CommunityView {
.select((
Community::safe_columns_tuple(),
User_::safe_columns_tuple(),
category::all_columns,
community_aggregates::all_columns,
community_follower::all_columns.nullable(),
))
@ -71,7 +66,6 @@ impl CommunityView {
Ok(CommunityView {
community,
creator,
category,
subscribed: follower.is_some(),
counts,
})
@ -162,7 +156,6 @@ impl<'a> CommunityQueryBuilder<'a> {
let mut query = community::table
.inner_join(user_::table)
.inner_join(category::table)
.inner_join(community_aggregates::table)
.left_join(
community_follower::table.on(
@ -174,7 +167,6 @@ impl<'a> CommunityQueryBuilder<'a> {
.select((
Community::safe_columns_tuple(),
User_::safe_columns_tuple(),
category::all_columns,
community_aggregates::all_columns,
community_follower::all_columns.nullable(),
))
@ -235,9 +227,8 @@ impl ViewToVec for CommunityView {
.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(),
counts: a.2.to_owned(),
subscribed: a.3.is_some(),
})
.collect::<Vec<Self>>()
}

View file

@ -270,7 +270,9 @@ impl<'a> UserMentionQueryBuilder<'a> {
SortType::Hot | SortType::Active => query
.order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
.then_order_by(comment_aggregates::published.desc()),
SortType::New | SortType::MostComments => query.order_by(comment::published.desc()),
SortType::New | SortType::MostComments | SortType::NewComments => {
query.order_by(comment::published.desc())
}
SortType::TopAll => query.order_by(comment_aggregates::score.desc()),
SortType::TopYear => query
.filter(comment::published.gt(now - 1.years()))

View file

@ -110,7 +110,9 @@ impl<'a> UserQueryBuilder<'a> {
SortType::Active => query
.order_by(user_aggregates::comment_score.desc())
.then_order_by(user_::published.desc()),
SortType::New | SortType::MostComments => query.order_by(user_::published.desc()),
SortType::New | SortType::MostComments | SortType::NewComments => {
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()))

View file

@ -376,6 +376,7 @@ fn create_post_items(posts: Vec<PostView>) -> Result<Vec<Item>, LemmyError> {
Settings::get().get_protocol_and_hostname(),
p.post.id
);
i.link(post_url.to_owned());
i.comments(post_url.to_owned());
let guid = GuidBuilder::default()
.permalink(true)
@ -393,10 +394,6 @@ fn create_post_items(posts: Vec<PostView>) -> Result<Vec<Item>, LemmyError> {
// 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.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,
@ -407,6 +404,12 @@ fn create_post_items(posts: Vec<PostView>) -> Result<Vec<Item>, LemmyError> {
post_url,
p.counts.comments);
// If its a url post, add it to the description
if let Some(url) = p.post.url {
let link_html = format!("<br><a href=\"{url}\">{url}</a>", url = url);
description.push_str(&link_html);
}
if let Some(body) = p.post.body {
let html = markdown_to_html(&body);
description.push_str(&html);

View file

@ -1,4 +1,4 @@
use lemmy_db_schema::source::{category::*, user::UserSafeSettings};
use lemmy_db_schema::source::user::UserSafeSettings;
use lemmy_db_views::{comment_view::CommentView, post_view::PostView, site_view::SiteView};
use lemmy_db_views_actor::{community_view::CommunityView, user_view::UserViewSafe};
use lemmy_db_views_moderator::{
@ -14,14 +14,6 @@ use lemmy_db_views_moderator::{
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
pub struct ListCategories {}
#[derive(Serialize)]
pub struct ListCategoriesResponse {
pub categories: Vec<Category>,
}
#[derive(Deserialize, Debug)]
pub struct Search {
pub q: String,

View file

@ -23,7 +23,7 @@ pub type ConnectionId = usize;
pub type PostId = i32;
pub type CommunityId = i32;
pub type UserId = i32;
pub type IPAddr = String;
pub type IpAddr = String;
#[macro_export]
macro_rules! location_info {
@ -39,13 +39,13 @@ macro_rules! location_info {
#[derive(Debug, Error)]
#[error("{{\"error\":\"{message}\"}}")]
pub struct APIError {
pub struct ApiError {
pub message: String,
}
impl APIError {
impl ApiError {
pub fn err(msg: &str) -> Self {
APIError {
ApiError {
message: msg.to_string(),
}
}

View file

@ -1,4 +1,4 @@
use crate::{APIError, IPAddr, LemmyError};
use crate::{ApiError, IpAddr, LemmyError};
use log::debug;
use std::{collections::HashMap, time::SystemTime};
use strum::IntoEnumIterator;
@ -20,13 +20,13 @@ pub(crate) enum RateLimitType {
/// Rate limiting based on rate type and IP addr
#[derive(Debug, Clone)]
pub struct RateLimiter {
buckets: HashMap<RateLimitType, HashMap<IPAddr, RateLimitBucket>>,
buckets: HashMap<RateLimitType, HashMap<IpAddr, RateLimitBucket>>,
}
impl Default for RateLimiter {
fn default() -> Self {
Self {
buckets: HashMap::<RateLimitType, HashMap<IPAddr, RateLimitBucket>>::new(),
buckets: HashMap::<RateLimitType, HashMap<IpAddr, RateLimitBucket>>::new(),
}
}
}
@ -87,7 +87,7 @@ impl RateLimiter {
rate_limit.allowance
);
Err(
APIError {
ApiError {
message: format!(
"Too many requests. type: {}, IP: {}, {} per {} seconds",
type_.as_ref(),

View file

@ -187,21 +187,6 @@ async fn is_image_content_type(client: &Client, test: &str) -> Result<(), LemmyE
#[cfg(test)]
mod tests {
use crate::request::is_image_content_type;
#[test]
fn test_image() {
actix_rt::System::new("tset_image").block_on(async move {
let client = reqwest::Client::default();
assert!(is_image_content_type(&client, "https://1734811051.rsc.cdn77.org/data/images/full/365645/as-virus-kills-navajos-in-their-homes-tribal-women-provide-lifeline.jpg?w=600?w=650").await.is_ok());
assert!(is_image_content_type(&client,
"https://twitter.com/BenjaminNorton/status/1259922424272957440?s=20"
)
.await.is_err()
);
});
}
// These helped with testing
// #[test]
// fn test_iframely() {

View file

@ -1,4 +1,4 @@
use crate::{settings::Settings, APIError};
use crate::{settings::Settings, ApiError};
use actix_web::dev::ConnectionInfo;
use chrono::{DateTime, FixedOffset, NaiveDateTime};
use itertools::Itertools;
@ -43,15 +43,15 @@ pub(crate) fn slur_check(test: &str) -> Result<(), Vec<&str>> {
}
}
pub fn check_slurs(text: &str) -> Result<(), APIError> {
pub fn check_slurs(text: &str) -> Result<(), ApiError> {
if let Err(slurs) = slur_check(text) {
Err(APIError::err(&slurs_vec_to_str(slurs)))
Err(ApiError::err(&slurs_vec_to_str(slurs)))
} else {
Ok(())
}
}
pub fn check_slurs_opt(text: &Option<String>) -> Result<(), APIError> {
pub fn check_slurs_opt(text: &Option<String>) -> Result<(), ApiError> {
match text {
Some(t) => check_slurs(t),
None => Ok(()),
@ -110,8 +110,8 @@ pub fn is_valid_username(name: &str) -> bool {
// Can't do a regex here, reverse lookarounds not supported
pub fn is_valid_preferred_username(preferred_username: &str) -> bool {
!preferred_username.starts_with('@')
&& preferred_username.len() >= 3
&& preferred_username.len() <= 20
&& preferred_username.chars().count() >= 3
&& preferred_username.chars().count() <= 20
}
pub fn is_valid_community_name(name: &str) -> bool {

View file

@ -1 +1 @@
pub const VERSION: &str = "0.9.7";
pub const VERSION: &str = "0.9.9";

View file

@ -10,10 +10,10 @@ use lemmy_structs::{comment::*, post::*};
use lemmy_utils::{
location_info,
rate_limit::RateLimit,
APIError,
ApiError,
CommunityId,
ConnectionId,
IPAddr,
IpAddr,
LemmyError,
PostId,
UserId,
@ -73,8 +73,8 @@ pub struct ChatServer {
}
pub struct SessionInfo {
pub addr: Recipient<WSMessage>,
pub ip: IPAddr,
pub addr: Recipient<WsMessage>,
pub ip: IpAddr,
}
/// `ChatServer` is an actor. It maintains list of connection client session.
@ -395,7 +395,7 @@ impl ChatServer {
fn sendit(&self, message: &str, id: ConnectionId) {
if let Some(info) = self.sessions.get(&id) {
let _ = info.addr.do_send(WSMessage(message.to_owned()));
let _ = info.addr.do_send(WsMessage(message.to_owned()));
}
}
@ -406,7 +406,7 @@ impl ChatServer {
) -> impl Future<Output = Result<String, LemmyError>> {
let rate_limiter = self.rate_limiter.clone();
let ip: IPAddr = match self.sessions.get(&msg.id) {
let ip: IpAddr = match self.sessions.get(&msg.id) {
Some(info) => info.ip.to_owned(),
None => "blank_ip".to_string(),
};
@ -421,7 +421,7 @@ impl ChatServer {
async move {
let json: Value = serde_json::from_str(&msg.msg)?;
let data = &json["data"].to_string();
let op = &json["op"].as_str().ok_or(APIError {
let op = &json["op"].as_str().ok_or(ApiError {
message: "Unknown op type".to_string(),
})?;

View file

@ -88,7 +88,6 @@ pub enum UserOperation {
CreateCommunity,
CreatePost,
ListCommunities,
ListCategories,
GetPost,
GetCommunity,
CreateComment,

View file

@ -1,13 +1,13 @@
use crate::UserOperation;
use actix::{prelude::*, Recipient};
use lemmy_structs::{comment::CommentResponse, post::PostResponse};
use lemmy_utils::{CommunityId, ConnectionId, IPAddr, PostId, UserId};
use lemmy_utils::{CommunityId, ConnectionId, IpAddr, PostId, UserId};
use serde::{Deserialize, Serialize};
/// Chat server sends this messages to session
#[derive(Message)]
#[rtype(result = "()")]
pub struct WSMessage(pub String);
pub struct WsMessage(pub String);
/// Message for chat server communications
@ -15,8 +15,8 @@ pub struct WSMessage(pub String);
#[derive(Message)]
#[rtype(usize)]
pub struct Connect {
pub addr: Recipient<WSMessage>,
pub ip: IPAddr,
pub addr: Recipient<WsMessage>,
pub ip: IpAddr,
}
/// Session is disconnected
@ -24,7 +24,7 @@ pub struct Connect {
#[rtype(result = "()")]
pub struct Disconnect {
pub id: ConnectionId,
pub ip: IPAddr,
pub ip: IpAddr,
}
/// The messages sent to websocket clients

View file

@ -1,6 +1,6 @@
use crate::{
chat_server::ChatServer,
messages::{Connect, Disconnect, StandardMessage, WSMessage},
messages::{Connect, Disconnect, StandardMessage, WsMessage},
LemmyContext,
};
use actix::prelude::*;
@ -22,7 +22,7 @@ pub async fn chat_route(
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, Error> {
ws::start(
WSSession {
WsSession {
cs_addr: context.chat_server().to_owned(),
id: 0,
hb: Instant::now(),
@ -33,7 +33,7 @@ pub async fn chat_route(
)
}
struct WSSession {
struct WsSession {
cs_addr: Addr<ChatServer>,
/// unique session id
id: usize,
@ -43,7 +43,7 @@ struct WSSession {
hb: Instant,
}
impl Actor for WSSession {
impl Actor for WsSession {
type Context = ws::WebsocketContext<Self>;
/// Method is called on actor start.
@ -87,16 +87,16 @@ impl Actor for WSSession {
/// Handle messages from chat server, we simply send it to peer websocket
/// These are room messages, IE sent to others in the room
impl Handler<WSMessage> for WSSession {
impl Handler<WsMessage> for WsSession {
type Result = ();
fn handle(&mut self, msg: WSMessage, ctx: &mut Self::Context) {
fn handle(&mut self, msg: WsMessage, ctx: &mut Self::Context) {
ctx.text(msg.0);
}
}
/// WebSocket message handler
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSSession {
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WsSession {
fn handle(&mut self, result: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
let message = match result {
Ok(m) => m,
@ -143,7 +143,7 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for WSSession {
}
}
impl WSSession {
impl WsSession {
/// helper method that sends ping to client every second.
///
/// also this method checks heartbeats from client

View file

@ -3,7 +3,7 @@ ARG RUST_BUILDER_IMAGE=ekidd/rust-musl-builder:1.47.0
# Cargo chef plan
FROM $RUST_BUILDER_IMAGE as planner
WORKDIR /app
RUN cargo install cargo-chef --version 0.1.6
RUN cargo install cargo-chef
# Copy dirs
COPY ./ ./
@ -15,7 +15,7 @@ RUN cargo chef prepare --recipe-path recipe.json
FROM $RUST_BUILDER_IMAGE as cacher
ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl
WORKDIR /app
RUN cargo install cargo-chef --version 0.1.6
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json ./recipe.json
RUN sudo chown -R rust:rust .
RUN cargo chef cook --target ${CARGO_BUILD_TARGET} --recipe-path recipe.json

View file

@ -17,7 +17,7 @@ services:
- iframely
lemmy-ui:
image: dessalines/lemmy-ui:0.9.7
image: dessalines/lemmy-ui:0.9.9
ports:
- "1235:1234"
restart: always

View file

@ -29,7 +29,7 @@ services:
- ./volumes/pictrs_alpha:/mnt
lemmy-alpha-ui:
image: dessalines/lemmy-ui:0.9.7
image: dessalines/lemmy-ui:0.9.9
environment:
- LEMMY_INTERNAL_HOST=lemmy-alpha:8541
- LEMMY_EXTERNAL_HOST=localhost:8541
@ -69,7 +69,7 @@ services:
- ./volumes/postgres_alpha:/var/lib/postgresql/data
lemmy-beta-ui:
image: dessalines/lemmy-ui:0.9.7
image: dessalines/lemmy-ui:0.9.9
environment:
- LEMMY_INTERNAL_HOST=lemmy-beta:8551
- LEMMY_EXTERNAL_HOST=localhost:8551
@ -109,7 +109,7 @@ services:
- ./volumes/postgres_beta:/var/lib/postgresql/data
lemmy-gamma-ui:
image: dessalines/lemmy-ui:0.9.7
image: dessalines/lemmy-ui:0.9.9
environment:
- LEMMY_INTERNAL_HOST=lemmy-gamma:8561
- LEMMY_EXTERNAL_HOST=localhost:8561
@ -150,7 +150,7 @@ services:
# An instance with only an allowlist for beta
lemmy-delta-ui:
image: dessalines/lemmy-ui:0.9.7
image: dessalines/lemmy-ui:0.9.9
environment:
- LEMMY_INTERNAL_HOST=lemmy-delta:8571
- LEMMY_EXTERNAL_HOST=localhost:8571
@ -191,7 +191,7 @@ services:
# An instance who has a blocklist, with lemmy-alpha blocked
lemmy-epsilon-ui:
image: dessalines/lemmy-ui:0.9.7
image: dessalines/lemmy-ui:0.9.9
environment:
- LEMMY_INTERNAL_HOST=lemmy-epsilon:8581
- LEMMY_EXTERNAL_HOST=localhost:8581

View file

@ -3,7 +3,7 @@ ARG RUST_BUILDER_IMAGE=ekidd/rust-musl-builder:1.47.0
# Cargo chef plan
FROM $RUST_BUILDER_IMAGE as planner
WORKDIR /app
RUN cargo install cargo-chef --version 0.1.6
RUN cargo install cargo-chef
# Copy dirs
COPY ./ ./
@ -15,7 +15,7 @@ RUN cargo chef prepare --recipe-path recipe.json
FROM $RUST_BUILDER_IMAGE as cacher
ARG CARGO_BUILD_TARGET=x86_64-unknown-linux-musl
WORKDIR /app
RUN cargo install cargo-chef --version 0.1.6
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json ./recipe.json
RUN sudo chown -R rust:rust .
RUN cargo chef cook --release --target ${CARGO_BUILD_TARGET} --recipe-path recipe.json

View file

@ -9,8 +9,8 @@ new_tag="$1"
# Setting the version on the front end
cd ../../
# Setting the version on the backend
echo "pub const VERSION: &str = \"$new_tag\";" > "crates/api/src/version.rs"
git add "crates/api/src/version.rs"
echo "pub const VERSION: &str = \"$new_tag\";" > "crates/utils/src/version.rs"
git add "crates/utils/src/version.rs"
# Setting the version for Ansible
echo $new_tag > "ansible/VERSION"
git add "ansible/VERSION"

View file

@ -12,7 +12,7 @@ services:
restart: always
lemmy:
image: dessalines/lemmy:0.9.7
image: dessalines/lemmy:0.9.9
ports:
- "127.0.0.1:8536:8536"
restart: always
@ -26,9 +26,9 @@ services:
- iframely
lemmy-ui:
image: dessalines/lemmy-ui:0.9.7
image: dessalines/lemmy-ui:0.9.9
ports:
- "1235:1234"
- "127.0.0.1:1235:1234"
restart: always
environment:
- LEMMY_INTERNAL_HOST=lemmy:8536

View file

@ -0,0 +1,33 @@
drop index idx_post_aggregates_newest_comment_time,
idx_post_aggregates_stickied_newest_comment_time,
idx_post_aggregates_stickied_comments;
alter table post_aggregates drop column newest_comment_time;
alter table post_aggregates rename column newest_comment_time_necro to newest_comment_time;
create or replace function post_aggregates_comment_count()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'INSERT') THEN
update post_aggregates pa
set comments = comments + 1
where pa.post_id = NEW.post_id;
-- A 2 day necro-bump limit
update post_aggregates pa
set newest_comment_time = NEW.published
where pa.post_id = NEW.post_id
and published > ('now'::timestamp - '2 days'::interval);
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 $$;

View file

@ -0,0 +1,43 @@
-- First rename current newest comment time to newest_comment_time_necro
-- necro means that time is limited to 2 days, whereas newest_comment_time ignores that.
alter table post_aggregates rename column newest_comment_time to newest_comment_time_necro;
-- Add the newest_comment_time column
alter table post_aggregates add column newest_comment_time timestamp not null default now();
-- Set the current newest_comment_time based on the old ones
update post_aggregates set newest_comment_time = newest_comment_time_necro;
-- Add the indexes for this new column
create index idx_post_aggregates_newest_comment_time on post_aggregates (newest_comment_time desc);
create index idx_post_aggregates_stickied_newest_comment_time on post_aggregates (stickied desc, newest_comment_time desc);
-- Forgot to add index w/ stickied first for most comments:
create index idx_post_aggregates_stickied_comments on post_aggregates (stickied desc, comments desc);
-- Alter the comment trigger to set the newest_comment_time, and newest_comment_time_necro
create or replace 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;
-- A 2 day necro-bump limit
update post_aggregates pa
set newest_comment_time_necro = NEW.published
where pa.post_id = NEW.post_id
and published > ('now'::timestamp - '2 days'::interval);
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 $$;

View file

@ -0,0 +1,35 @@
create or replace function comment_aggregates_comment()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'INSERT') THEN
insert into comment_aggregates (comment_id) values (NEW.id);
ELSIF (TG_OP = 'DELETE') THEN
delete from comment_aggregates where comment_id = OLD.id;
END IF;
return null;
end $$;
create or replace 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 or replace 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 $$;

View file

@ -0,0 +1,39 @@
-- The published and updated columns on the aggregates tables are using now(),
-- when they should use the correct published or updated columns
-- This is mainly a problem with federated posts being fetched
create or replace function comment_aggregates_comment()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'INSERT') THEN
insert into comment_aggregates (comment_id, published) values (NEW.id, NEW.published);
ELSIF (TG_OP = 'DELETE') THEN
delete from comment_aggregates where comment_id = OLD.id;
END IF;
return null;
end $$;
create or replace function post_aggregates_post()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'INSERT') THEN
insert into post_aggregates (post_id, published, newest_comment_time, newest_comment_time_necro) values (NEW.id, NEW.published, NEW.published, NEW.published);
ELSIF (TG_OP = 'DELETE') THEN
delete from post_aggregates where post_id = OLD.id;
END IF;
return null;
end $$;
create or replace function community_aggregates_community()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'INSERT') THEN
insert into community_aggregates (community_id, published) values (NEW.id, NEW.published);
ELSIF (TG_OP = 'DELETE') THEN
delete from community_aggregates where community_id = OLD.id;
END IF;
return null;
end $$;

View file

@ -0,0 +1,34 @@
create table category (
id serial primary key,
name varchar(100) not null unique
);
insert into category (name) values
('Discussion'),
('Humor/Memes'),
('Gaming'),
('Movies'),
('TV'),
('Music'),
('Literature'),
('Comics'),
('Photography'),
('Art'),
('Learning'),
('DIY'),
('Lifestyle'),
('News'),
('Politics'),
('Society'),
('Gender/Identity/Sexuality'),
('Race/Colonisation'),
('Religion'),
('Science/Technology'),
('Programming/Software'),
('Health/Sports/Fitness'),
('Porn'),
('Places'),
('Meta'),
('Other');
ALTER TABLE community ADD category_id int references category on update cascade on delete cascade not null default 1;

View file

@ -0,0 +1,2 @@
ALTER TABLE community DROP COLUMN category_id;
DROP TABLE category;

View file

@ -109,7 +109,6 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
name: ccommunity.name.to_owned(),
title: ccommunity.title.to_owned(),
description: ccommunity.description.to_owned(),
category_id: ccommunity.category_id,
creator_id: ccommunity.creator_id,
removed: None,
deleted: None,

View file

@ -1,231 +0,0 @@
extern crate lemmy_server;
#[macro_use]
extern crate diesel_migrations;
use activitystreams::{
activity::{
kind::{CreateType, FollowType},
ActorAndObject,
},
base::{BaseExt, ExtendsExt},
object::{Note, ObjectExt},
};
use actix::prelude::*;
use actix_web::{test::TestRequest, web, web::Path, HttpRequest};
use chrono::Utc;
use diesel::{
r2d2::{ConnectionManager, Pool},
PgConnection,
};
use http_signature_normalization_actix::PrepareVerifyError;
use lemmy_api::match_websocket_operation;
use lemmy_apub::{
activity_queue::create_activity_queue,
inbox::{
community_inbox,
community_inbox::community_inbox,
shared_inbox,
shared_inbox::shared_inbox,
user_inbox,
user_inbox::user_inbox,
},
};
use lemmy_db_queries::{get_database_url_from_env, Crud, ListingType, SortType};
use lemmy_db_schema::source::{
community::{Community, CommunityForm},
user::{UserForm, User_},
};
use lemmy_server::code_migrations::run_advanced_migrations;
use lemmy_utils::{
apub::generate_actor_keypair,
rate_limit::{rate_limiter::RateLimiter, RateLimit},
settings::Settings,
};
use lemmy_websocket::{chat_server::ChatServer, LemmyContext};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::{ops::Deref, sync::Arc};
use tokio::sync::Mutex;
use url::Url;
embed_migrations!();
fn create_context() -> LemmyContext {
let settings = Settings::get();
let db_url = match get_database_url_from_env() {
Ok(url) => url,
Err(_) => settings.get_database_url(),
};
let manager = ConnectionManager::<PgConnection>::new(&db_url);
let pool = Pool::builder()
.max_size(settings.database.pool_size)
.build(manager)
.unwrap();
embedded_migrations::run(&pool.get().unwrap()).unwrap();
run_advanced_migrations(pool.get().unwrap().deref()).unwrap();
let rate_limiter = RateLimit {
rate_limiter: Arc::new(Mutex::new(RateLimiter::default())),
};
let activity_queue = create_activity_queue();
let chat_server = ChatServer::startup(
pool.clone(),
rate_limiter,
|c, i, o, d| Box::pin(match_websocket_operation(c, i, o, d)),
Client::default(),
activity_queue,
)
.start();
LemmyContext::create(
pool,
chat_server,
Client::default(),
create_activity_queue(),
)
}
fn create_user(conn: &PgConnection, name: &str) -> User_ {
let user_keypair = generate_actor_keypair().unwrap();
let new_user = UserForm {
name: name.into(),
preferred_username: None,
password_encrypted: "nope".into(),
email: None,
matrix_user_id: None,
avatar: None,
banner: None,
admin: false,
banned: Some(false),
updated: None,
published: 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: Some(
Url::parse(&format!("http://localhost:8536/u/{}", name))
.unwrap()
.into(),
),
bio: None,
local: true,
private_key: Some(user_keypair.private_key),
public_key: Some(user_keypair.public_key),
last_refreshed_at: None,
inbox_url: None,
shared_inbox_url: None,
};
User_::create(&conn, &new_user).unwrap()
}
fn create_community(conn: &PgConnection, creator_id: i32) -> Community {
let new_community = CommunityForm {
name: "test_community".into(),
creator_id,
title: "test_community".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,
followers_url: None,
inbox_url: None,
shared_inbox_url: None,
};
Community::create(&conn, &new_community).unwrap()
}
fn create_activity<'a, Activity, Return>(user_id: Url) -> web::Json<Return>
where
for<'de> Return: Deserialize<'de> + 'a,
Activity: std::default::Default + Serialize,
{
let mut activity = ActorAndObject::<Activity>::new(user_id, Note::new().into_any_base().unwrap());
activity
.set_id(Url::parse("http://localhost:8536/create/1").unwrap())
.set_many_ccs(vec![Url::parse("http://localhost:8536/c/main").unwrap()]);
let activity = serde_json::to_value(&activity).unwrap();
let activity: Return = serde_json::from_value(activity).unwrap();
web::Json(activity)
}
fn create_http_request() -> HttpRequest {
let time1 = Utc::now().timestamp();
let time2 = Utc::now().timestamp();
let signature = format!(
r#"keyId="my-key-id",algorithm="hs2019",created="{}",expires="{}",headers="(request-target) (created) (expires) date content-type",signature="blah blah blah""#,
time1, time2
);
TestRequest::post()
.uri("http://localhost:8536/")
.header("Signature", signature)
.to_http_request()
}
// TODO: this fails with a stack overflow for some reason
#[actix_rt::test]
#[ignore]
async fn test_shared_inbox_expired_signature() {
let request = create_http_request();
let context = create_context();
let connection = &context.pool().get().unwrap();
let user = create_user(connection, "shared_inbox_rvgfd");
let activity =
create_activity::<CreateType, ActorAndObject<shared_inbox::ValidTypes>>(user.actor_id.into());
let response = shared_inbox(request, activity, web::Data::new(context)).await;
assert_eq!(
format!("{}", response.err().unwrap()),
format!("{}", PrepareVerifyError::Expired)
);
User_::delete(connection, user.id).unwrap();
}
#[actix_rt::test]
async fn test_user_inbox_expired_signature() {
let request = create_http_request();
let context = create_context();
let connection = &context.pool().get().unwrap();
let user = create_user(connection, "user_inbox_cgsax");
let activity =
create_activity::<CreateType, ActorAndObject<user_inbox::UserValidTypes>>(user.actor_id.into());
let path = Path::<String> {
0: "username".to_string(),
};
let response = user_inbox(request, activity, path, web::Data::new(context)).await;
assert_eq!(
format!("{}", response.err().unwrap()),
format!("{}", PrepareVerifyError::Expired)
);
User_::delete(connection, user.id).unwrap();
}
#[actix_rt::test]
async fn test_community_inbox_expired_signature() {
let context = create_context();
let connection = &context.pool().get().unwrap();
let user = create_user(connection, "community_inbox_hrxa");
let community = create_community(connection, user.id);
let request = create_http_request();
let activity = create_activity::<FollowType, ActorAndObject<community_inbox::CommunityValidTypes>>(
user.actor_id.into(),
);
let path = Path::<String> { 0: community.name };
let response = community_inbox(request, activity, path, web::Data::new(context)).await;
assert_eq!(
format!("{}", response.err().unwrap()),
format!("{}", PrepareVerifyError::Expired)
);
User_::delete(connection, user.id).unwrap();
Community::delete(connection, community.id).unwrap();
}