Merge branch 'main' into lemmynsfw-changes

lemmynsfw-changes
Felix Ableitner 4 months ago
commit 5fbba8a4cf

@ -2,7 +2,8 @@
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
variables:
- &rust_image "rust:1.75"
- &rust_image "rust:1.76"
- &install_pnpm "corepack enable pnpm"
- &slow_check_paths
- path:
# rust source code
@ -41,7 +42,7 @@ steps:
prettier_check:
image: tmknom/prettier:3.0.0
commands:
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations'
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml'
toml_fmt:
image: tamasfe/taplo:0.8.1
@ -152,7 +153,6 @@ steps:
environment:
CARGO_HOME: .cargo_home
commands:
# when adding new clippy lints, make sure to also add them in scripts/lint.sh
- rustup component add clippy
- cargo clippy --workspace --tests --all-targets --features console -- -D warnings
when: *slow_check_paths
@ -183,13 +183,23 @@ steps:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
DO_WRITE_HOSTS_FILE: "1"
commands:
- *install_pnpm
- apt update && apt install -y bash curl postgresql-client
- bash api_tests/prepare-drone-federation-test.sh
- cd api_tests/
- yarn
- yarn api-test
- pnpm i
- pnpm api-test
when: *slow_check_paths
federation_tests_server_output:
image: alpine:3
commands:
# `|| true` prevents this step from appearing to fail if the server output files don't exist
- cat target/log/lemmy_*.out || true
- "# If you can't see all output, then use the download button"
when:
status: [failure]
publish_release_docker:
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
@ -247,7 +257,7 @@ steps:
services:
database:
image: postgres:15.2-alpine
image: postgres:16-alpine
environment:
POSTGRES_USER: lemmy
POSTGRES_PASSWORD: password

7
Cargo.lock generated

@ -8,6 +8,12 @@ version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "accept-language"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772"
[[package]]
name = "activitypub_federation"
version = "0.5.1-beta.1"
@ -2608,6 +2614,7 @@ dependencies = [
name = "lemmy_api_crud"
version = "0.19.3"
dependencies = [
"accept-language",
"activitypub_federation",
"actix-web",
"anyhow",

@ -149,7 +149,11 @@ http = "0.2.11"
rosetta-i18n = "0.1.3"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.19.0" }
ts-rs = { version = "7.1.1", features = ["serde-compat", "chrono-impl"] }
ts-rs = { version = "7.1.1", features = [
"serde-compat",
"chrono-impl",
"no-serde-warnings",
] }
rustls = { version = "0.21.10", features = ["dangerous_configuration"] }
futures-util = "0.3.30"
tokio-postgres = "0.7.10"

@ -107,7 +107,6 @@ Each Lemmy server can set its own moderation policy; appointing site-wide admins
- NSFW post / community support.
- High performance.
- Server is written in rust.
- Front end is `~80kB` gzipped.
- Supports arm64 / Raspberry Pi.
## Installation

@ -27,7 +27,7 @@
"eslint": "^8.55.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.5.0",
"lemmy-js-client": "0.19.2-alpha.2",
"lemmy-js-client": "0.19.3-alpha.2",
"prettier": "^3.1.1",
"ts-jest": "^29.1.0",
"typescript": "^5.3.3"

File diff suppressed because it is too large Load Diff

@ -3,8 +3,13 @@
# it is expected that this script is called by run-federation-test.sh script.
set -e
if [ -n "$LEMMY_LOG_LEVEL" ];
then
LEMMY_LOG_LEVEL=warn
fi
export RUST_BACKTRACE=1
export RUST_LOG="warn,lemmy_server=debug,lemmy_federate=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL"
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
@ -46,32 +51,35 @@ fi
echo "$PWD"
LOG_DIR=target/log
mkdir -p $LOG_DIR
echo "start alpha"
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_alpha.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_alpha" \
target/lemmy_server >/tmp/lemmy_alpha.out 2>&1 &
target/lemmy_server >$LOG_DIR/lemmy_alpha.out 2>&1 &
echo "start beta"
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_beta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_beta" \
target/lemmy_server >/tmp/lemmy_beta.out 2>&1 &
target/lemmy_server >$LOG_DIR/lemmy_beta.out 2>&1 &
echo "start gamma"
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_gamma.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_gamma" \
target/lemmy_server >/tmp/lemmy_gamma.out 2>&1 &
target/lemmy_server >$LOG_DIR/lemmy_gamma.out 2>&1 &
echo "start delta"
# An instance with only an allowlist for beta
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_delta.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_delta" \
target/lemmy_server >/tmp/lemmy_delta.out 2>&1 &
target/lemmy_server >$LOG_DIR/lemmy_delta.out 2>&1 &
echo "start epsilon"
# An instance who has a blocklist, with lemmy-alpha blocked
LEMMY_CONFIG_LOCATION=./docker/federation/lemmy_epsilon.hjson \
LEMMY_DATABASE_URL="${LEMMY_DATABASE_URL}/lemmy_epsilon" \
target/lemmy_server >/tmp/lemmy_epsilon.out 2>&1 &
target/lemmy_server >$LOG_DIR/lemmy_epsilon.out 2>&1 &
echo "wait for all instances to start"
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 'lemmy-alpha:8541/api/v3/site')" != "200" ]]; do sleep 1; done

@ -10,8 +10,8 @@ killall -s1 lemmy_server || true
./api_tests/prepare-drone-federation-test.sh
popd
yarn
yarn api-test || true
pnpm i
pnpm api-test || true
killall -s1 lemmy_server || true
killall -s1 pict-rs || true

@ -32,8 +32,9 @@ import {
resolveBetaCommunity,
longDelay,
delay,
editCommunity,
} from "./shared";
import { EditSite } from "lemmy-js-client";
import { EditCommunity, EditSite } from "lemmy-js-client";
beforeAll(setupLogins);
@ -511,3 +512,24 @@ test("Fetch community, includes posts", async () => {
expect(post_listing.posts.length).toBe(1);
expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id);
});
test("Content in local-only community doesnt federate", async () => {
// create a community and set it local-only
let communityRes = (await createCommunity(alpha)).community_view.community;
let form: EditCommunity = {
community_id: communityRes.id,
visibility: "LocalOnly",
};
await editCommunity(alpha, form);
// cant resolve the community from another instance
await expect(
resolveCommunity(beta, communityRes.actor_id),
).rejects.toStrictEqual(Error("couldnt_find_object"));
// create a post, also cant resolve it
let postRes = await createPost(alpha, communityRes.id);
await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual(
Error("couldnt_find_object"),
);
});

@ -5,6 +5,7 @@ import {
BlockInstanceResponse,
CommunityId,
CreatePrivateMessageReport,
EditCommunity,
GetReplies,
GetRepliesResponse,
GetUnreadCountResponse,
@ -532,6 +533,13 @@ export async function createCommunity(
return api.createCommunity(form);
}
export async function editCommunity(
api: LemmyHttp,
form: EditCommunity,
): Promise<CommunityResponse> {
return api.editCommunity(form);
}
export async function getCommunity(
api: LemmyHttp,
id: number,

File diff suppressed because it is too large Load Diff

@ -5,7 +5,11 @@ use lemmy_api_common::{
comment::{CommentReportResponse, CreateCommentReport},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_user_action, send_new_report_email_to_admins},
utils::{
check_comment_deleted_or_removed,
check_community_user_action,
send_new_report_email_to_admins,
},
};
use lemmy_db_schema::{
source::{
@ -40,6 +44,9 @@ pub async fn create_comment_report(
)
.await?;
// Don't allow creating reports for removed / deleted comments
check_comment_deleted_or_removed(&comment_view.comment)?;
let report_form = CommentReportForm {
creator_id: person_id,
comment_id,

@ -5,7 +5,11 @@ use lemmy_api_common::{
context::LemmyContext,
post::{CreatePostReport, PostReportResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_user_action, send_new_report_email_to_admins},
utils::{
check_community_user_action,
check_post_deleted_or_removed,
send_new_report_email_to_admins,
},
};
use lemmy_db_schema::{
source::{
@ -40,6 +44,8 @@ pub async fn create_post_report(
)
.await?;
check_post_deleted_or_removed(&post_view.post)?;
let report_form = PostReportForm {
creator_id: person_id,
post_id,

@ -9,7 +9,7 @@ use lemmy_db_schema::{
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct Claims {
/// local_user_id, standard claim by RFC 7519.
pub sub: String,

@ -10,7 +10,7 @@ use serde_with::skip_serializing_none;
use ts_rs::TS;
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create a comment.
@ -22,7 +22,7 @@ pub struct CreateComment {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetch an individual comment.
@ -31,7 +31,7 @@ pub struct GetComment {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Edit a comment.
@ -42,7 +42,7 @@ pub struct EditComment {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Distinguish a comment (IE speak as moderator).
@ -52,7 +52,7 @@ pub struct DistinguishComment {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete your own comment.
@ -62,7 +62,7 @@ pub struct DeleteComment {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Remove a comment (only doable by mods).
@ -72,7 +72,7 @@ pub struct RemoveComment {
pub reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Save / bookmark a comment.
@ -91,7 +91,7 @@ pub struct CommentResponse {
pub recipient_ids: Vec<LocalUserId>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Like a comment.
@ -102,7 +102,7 @@ pub struct CreateCommentLike {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get a list of comments.
@ -146,7 +146,7 @@ pub struct CommentReportResponse {
pub comment_report_view: CommentReportView,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Resolve a comment report (only doable by mods).
@ -156,7 +156,7 @@ pub struct ResolveCommentReport {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List comment reports.
@ -178,7 +178,7 @@ pub struct ListCommentReportsResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List comment likes. Admins-only.

@ -1,6 +1,7 @@
use lemmy_db_schema::{
newtypes::{CommunityId, LanguageId, PersonId},
source::site::Site,
CommunityVisibility,
ListingType,
SortType,
};
@ -11,7 +12,7 @@ use serde_with::skip_serializing_none;
use ts_rs::TS;
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get a community. Must provide either an id, or a name.
@ -36,7 +37,7 @@ pub struct GetCommunityResponse {
#[skip_serializing_none]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
/// Create a community.
pub struct CreateCommunity {
/// The unique name.
@ -54,6 +55,7 @@ pub struct CreateCommunity {
/// Whether to restrict posting only to moderators.
pub posting_restricted_to_mods: Option<bool>,
pub discussion_languages: Option<Vec<LanguageId>>,
pub visibility: Option<CommunityVisibility>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -66,7 +68,7 @@ pub struct CommunityResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetches a list of communities.
@ -87,7 +89,7 @@ pub struct ListCommunitiesResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Ban a user from a community.
@ -97,6 +99,9 @@ pub struct BanFromCommunity {
pub ban: bool,
pub remove_data: Option<bool>,
pub reason: Option<String>,
/// A time that the ban will expire, in unix epoch seconds.
///
/// An i64 unix timestamp is used for a simpler API client implementation.
pub expires: Option<i64>,
}
@ -109,7 +114,7 @@ pub struct BanFromCommunityResponse {
pub banned: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Add a moderator to a community.
@ -128,7 +133,7 @@ pub struct AddModToCommunityResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Edit a community.
@ -147,10 +152,11 @@ pub struct EditCommunity {
/// Whether to restrict posting only to moderators.
pub posting_restricted_to_mods: Option<bool>,
pub discussion_languages: Option<Vec<LanguageId>>,
pub visibility: Option<CommunityVisibility>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Hide a community from the main view.
@ -161,7 +167,7 @@ pub struct HideCommunity {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete your own community.
@ -171,7 +177,7 @@ pub struct DeleteCommunity {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Remove a community (only doable by moderators).
@ -181,7 +187,7 @@ pub struct RemoveCommunity {
pub reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Follow / subscribe to a community.
@ -190,7 +196,7 @@ pub struct FollowCommunity {
pub follow: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Block a community.
@ -209,7 +215,7 @@ pub struct BlockCommunityResponse {
pub blocked: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Transfer a community to a new owner.

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use ts_rs::TS;
use url::Url;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create a custom emoji.
@ -18,7 +18,7 @@ pub struct CreateCustomEmoji {
pub keywords: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Edit a custom emoji.
@ -31,7 +31,7 @@ pub struct EditCustomEmoji {
pub keywords: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete a custom emoji.

@ -20,7 +20,7 @@ use serde_with::skip_serializing_none;
use ts_rs::TS;
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Logging into lemmy.
@ -32,7 +32,7 @@ pub struct Login {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Register / Sign up to lemmy.
@ -77,7 +77,7 @@ pub struct CaptchaResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Saves settings for your user.
@ -131,7 +131,7 @@ pub struct SaveUserSettings {
pub collapse_bot_comments: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Changes your account password.
@ -156,7 +156,7 @@ pub struct LoginResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Gets a person's details.
@ -186,7 +186,7 @@ pub struct GetPersonDetailsResponse {
pub moderates: Vec<CommunityModeratorView>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Adds an admin to a site.
@ -204,7 +204,7 @@ pub struct AddAdminResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Ban a person from the site.
@ -214,6 +214,9 @@ pub struct BanPerson {
/// Optionally remove all their data. Useful for new troll accounts.
pub remove_data: Option<bool>,
pub reason: Option<String>,
/// A time that the ban will expire, in unix epoch seconds.
///
/// An i64 unix timestamp is used for a simpler API client implementation.
pub expires: Option<i64>,
}
@ -235,7 +238,7 @@ pub struct BanPersonResponse {
pub banned: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Block a person.
@ -254,7 +257,7 @@ pub struct BlockPersonResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get comment replies.
@ -275,7 +278,7 @@ pub struct GetRepliesResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get mentions for your user.
@ -294,7 +297,7 @@ pub struct GetPersonMentionsResponse {
pub mentions: Vec<PersonMentionView>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Mark a person mention as read.
@ -311,7 +314,7 @@ pub struct PersonMentionResponse {
pub person_mention_view: PersonMentionView,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Mark a comment reply as read.
@ -328,7 +331,7 @@ pub struct CommentReplyResponse {
pub comment_reply_view: CommentReplyView,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete your account.
@ -337,7 +340,7 @@ pub struct DeleteAccount {
pub delete_content: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Reset your password via email.
@ -345,7 +348,7 @@ pub struct PasswordReset {
pub email: Sensitive<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Change your password after receiving a reset request.
@ -356,7 +359,7 @@ pub struct PasswordChangeAfterReset {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get a count of the number of reports.
@ -386,7 +389,7 @@ pub struct GetUnreadCountResponse {
pub private_messages: i64,
}
#[derive(Serialize, Deserialize, Clone, Default, Debug)]
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Verify your email.
@ -401,7 +404,7 @@ pub struct GenerateTotpSecretResponse {
pub totp_secret_url: Sensitive<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct UpdateTotp {

@ -13,7 +13,7 @@ use ts_rs::TS;
use url::Url;
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create a post.
@ -28,6 +28,9 @@ pub struct CreatePost {
pub honeypot: Option<String>,
pub nsfw: Option<bool>,
pub language_id: Option<LanguageId>,
#[cfg_attr(feature = "full", ts(type = "string"))]
/// Instead of fetching a thumbnail, use a custom one.
pub custom_thumbnail: Option<Url>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -38,7 +41,7 @@ pub struct PostResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get a post. Needs either the post id, or comment_id.
@ -61,7 +64,7 @@ pub struct GetPostResponse {
}
#[skip_serializing_none]
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get a list of posts.
@ -90,7 +93,7 @@ pub struct GetPostsResponse {
pub next_page: Option<PaginationCursor>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Like a post.
@ -101,7 +104,7 @@ pub struct CreatePostLike {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Edit a post.
@ -114,9 +117,12 @@ pub struct EditPost {
pub body: Option<String>,
pub nsfw: Option<bool>,
pub language_id: Option<LanguageId>,
#[cfg_attr(feature = "full", ts(type = "string"))]
/// Instead of fetching a thumbnail, use a custom one.
pub custom_thumbnail: Option<Url>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete a post.
@ -126,7 +132,7 @@ pub struct DeletePost {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Remove a post (only doable by mods).
@ -137,7 +143,7 @@ pub struct RemovePost {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Mark a post as read.
@ -148,7 +154,7 @@ pub struct MarkPostAsRead {
pub read: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Lock a post (prevent new comments).
@ -157,7 +163,7 @@ pub struct LockPost {
pub locked: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Feature a post (stickies / pins to the top).
@ -167,7 +173,7 @@ pub struct FeaturePost {
pub feature_type: PostFeatureType,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Save / bookmark a post.
@ -193,7 +199,7 @@ pub struct PostReportResponse {
pub post_report_view: PostReportView,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Resolve a post report (mods only).
@ -203,7 +209,7 @@ pub struct ResolvePostReport {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List post reports.
@ -224,7 +230,7 @@ pub struct ListPostReportsResponse {
pub post_reports: Vec<PostReportView>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get metadata for a given site.
@ -242,7 +248,7 @@ pub struct GetSiteMetadataResponse {
}
#[skip_serializing_none]
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Default)]
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Default, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Site metadata, from its opengraph tags.
@ -255,7 +261,7 @@ pub struct LinkMetadata {
}
#[skip_serializing_none]
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Default)]
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone, Default, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Site metadata, from its opengraph tags.
@ -267,7 +273,7 @@ pub struct OpenGraphData {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List post likes. Admins-only.

@ -5,7 +5,7 @@ use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create a private message.
@ -14,7 +14,7 @@ pub struct CreatePrivateMessage {
pub recipient_id: PersonId,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Edit a private message.
@ -23,7 +23,7 @@ pub struct EditPrivateMessage {
pub content: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete a private message.
@ -32,7 +32,7 @@ pub struct DeletePrivateMessage {
pub deleted: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Mark a private message as read.
@ -42,7 +42,7 @@ pub struct MarkPrivateMessageAsRead {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get your private messages.
@ -69,7 +69,7 @@ pub struct PrivateMessageResponse {
pub private_message_view: PrivateMessageView,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create a report for a private message.
@ -86,7 +86,7 @@ pub struct PrivateMessageReportResponse {
pub private_message_report_view: PrivateMessageReportView,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Resolve a private message report.
@ -96,7 +96,7 @@ pub struct ResolvePrivateMessageReport {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List private message reports.

@ -55,7 +55,7 @@ use serde_with::skip_serializing_none;
use ts_rs::TS;
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Searches the site, given a query string, and some optional filters.
@ -84,7 +84,7 @@ pub struct SearchResponse {
pub users: Vec<PersonView>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Does an apub fetch for an object.
@ -107,7 +107,7 @@ pub struct ResolveObjectResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetches the modlog.
@ -144,7 +144,7 @@ pub struct GetModlogResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Creates a site. Should be done after first running lemmy.
@ -193,7 +193,7 @@ pub struct CreateSite {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Edits a site.
@ -371,7 +371,7 @@ pub struct InstanceWithFederationState {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Purges a person from the database. This will delete all content attached to that person.
@ -381,7 +381,7 @@ pub struct PurgePerson {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Purges a community from the database. This will delete all content attached to that community.
@ -391,7 +391,7 @@ pub struct PurgeCommunity {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Purges a post from the database. This will delete all content attached to that post.
@ -401,7 +401,7 @@ pub struct PurgePost {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Purges a comment from the database. This will delete all content attached to that comment.
@ -411,7 +411,7 @@ pub struct PurgeComment {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetches a list of registration applications.
@ -431,7 +431,7 @@ pub struct ListRegistrationApplicationsResponse {
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Approves a registration application.
@ -457,7 +457,7 @@ pub struct GetUnreadRegistrationApplicationCountResponse {
pub registration_applications: i64,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Block an instance as user

@ -225,6 +225,7 @@ pub async fn check_community_mod_action(
Ok(())
}
/// Don't allow creating reports for removed / deleted posts
pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> {
if post.deleted || post.removed {
Err(LemmyErrorType::Deleted)?
@ -233,6 +234,14 @@ pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> {
}
}
pub fn check_comment_deleted_or_removed(comment: &Comment) -> Result<(), LemmyError> {
if comment.deleted || comment.removed {
Err(LemmyErrorType::Deleted)?
} else {
Ok(())
}
}
/// Throws an error if a recipient has blocked a person.
#[tracing::instrument(skip_all)]
pub async fn check_person_block(

@ -29,6 +29,7 @@ moka.workspace = true
once_cell.workspace = true
anyhow.workspace = true
webmention = "0.5.0"
accept-language = "3.1.0"
[package.metadata.cargo-machete]
ignored = ["futures"]

@ -10,10 +10,11 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
comment::{Comment, CommentUpdateForm},
comment_report::CommentReport,
moderator::{ModRemoveComment, ModRemoveCommentForm},
post::Post,
},
traits::Crud,
traits::{Crud, Reportable},
};
use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
@ -48,6 +49,9 @@ pub async fn remove_comment(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
CommentReport::resolve_all_for_object(&mut context.pool(), comment_id, local_user_view.person.id)
.await?;
// Mod tables
let form = ModRemoveCommentForm {
mod_person_id: local_user_view.person.id,

@ -92,6 +92,7 @@ pub async fn create_community(
.shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?))
.posting_restricted_to_mods(data.posting_restricted_to_mods)
.instance_id(site_view.site.instance_id)
.visibility(data.visibility)
.build();
let inserted_community = Community::create(&mut context.pool(), &community_form)

@ -72,6 +72,7 @@ pub async fn update_community(
banner,
nsfw: data.nsfw,
posting_restricted_to_mods: data.posting_restricted_to_mods,
visibility: data.visibility,
updated: Some(Some(naive_now())),
..Default::default()
};

@ -26,6 +26,7 @@ use lemmy_db_schema::{
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
},
traits::{Crud, Likeable},
CommunityVisibility,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityView;
@ -56,10 +57,12 @@ pub async fn create_post(
let data_url = data.url.as_ref();
let url = data_url.map(clean_url_params); // TODO no good way to handle a "clear"
let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params);
is_valid_post_title(&data.name)?;
is_valid_body_field(&body, true)?;
check_url_scheme(&data.url)?;
check_url_scheme(&url)?;
check_url_scheme(&custom_thumbnail)?;
check_community_user_action(
&local_user_view.person,
@ -83,9 +86,17 @@ pub async fn create_post(
}
}
// Only generate the thumbnail if there's no custom thumbnail provided,
// otherwise it will save it in pictrs
let generate_thumbnail = custom_thumbnail.is_none();
// Fetch post links and pictrs cached image
let metadata = fetch_link_metadata_opt(url.as_ref(), true, &context).await;
let metadata = fetch_link_metadata_opt(url.as_ref(), generate_thumbnail, &context).await;
let url = proxy_image_link_opt_apub(url, &context).await?;
let thumbnail_url = proxy_image_link_opt_apub(custom_thumbnail, &context)
.await?
.map(Into::into)
.or(metadata.thumbnail);
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
@ -120,7 +131,7 @@ pub async fn create_post(
.embed_description(metadata.opengraph_data.description)
.embed_video_url(metadata.opengraph_data.embed_video_url)
.language_id(language_id)
.thumbnail_url(metadata.thumbnail)
.thumbnail_url(thumbnail_url)
.build();
let inserted_post = Post::create(&mut context.pool(), &post_form)
@ -165,20 +176,22 @@ pub async fn create_post(
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
if let Some(url) = updated_post.url.clone() {
spawn_try_task(async move {
let mut webmention =
Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
webmention.set_checked(true);
match webmention
.send()
.instrument(tracing::info_span!("Sending webmention"))
.await
{
Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()),
Ok(_) => Ok(()),
Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention),
}
});
if community.visibility == CommunityVisibility::Public {
spawn_try_task(async move {
let mut webmention =
Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
webmention.set_checked(true);
match webmention
.send()
.instrument(tracing::info_span!("Sending webmention"))
.await
{
Err(WebmentionError::NoEndpointDiscovered(_)) => Ok(()),
Ok(_) => Ok(()),
Err(e) => Err(e).with_lemmy_type(LemmyErrorType::CouldntSendWebmention),
}
});
}
};
build_post_response(&context, community_id, &local_user_view.person, post_id).await

@ -52,7 +52,7 @@ pub async fn delete_post(
.await?;
ActivityChannel::submit_activity(
SendActivityData::DeletePost(post, local_user_view.person.clone(), data.0.clone()),
SendActivityData::DeletePost(post, local_user_view.person.clone(), data.0),
&context,
)
.await?;

@ -11,8 +11,9 @@ use lemmy_db_schema::{
source::{
moderator::{ModRemovePost, ModRemovePostForm},
post::{Post, PostUpdateForm},
post_report::PostReport,
},
traits::Crud,
traits::{Crud, Reportable},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
@ -47,6 +48,9 @@ pub async fn remove_post(
)
.await?;
PostReport::resolve_all_for_object(&mut context.pool(), post_id, local_user_view.person.id)
.await?;
// Mod tables
let form = ModRemovePostForm {
mod_person_id: local_user_view.person.id,

@ -43,6 +43,7 @@ pub async fn update_post(
// TODO No good way to handle a clear.
// Issue link: https://github.com/LemmyNet/lemmy/issues/2287
let url = data.url.as_ref().map(clean_url_params);
let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params);
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs_opt(&data.name, &slur_regex)?;
@ -53,7 +54,8 @@ pub async fn update_post(
}
is_valid_body_field(&body, true)?;
check_url_scheme(&data.url)?;
check_url_scheme(&url)?;
check_url_scheme(&custom_thumbnail)?;
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
@ -70,10 +72,14 @@ pub async fn update_post(
Err(LemmyErrorType::NoPostEditAllowed)?
}
// Fetch post links and Pictrs cached image if url was updated
let (embed_title, embed_description, embed_video_url, thumbnail_url) = match &url {
// Fetch post links and thumbnail if url was updated
let (embed_title, embed_description, embed_video_url, metadata_thumbnail) = match &url {
Some(url) => {
let metadata = fetch_link_metadata(url, true, &context).await?;
// Only generate the thumbnail if there's no custom thumbnail provided,
// otherwise it will save it in pictrs
let generate_thumbnail = custom_thumbnail.is_none();
let metadata = fetch_link_metadata(url, generate_thumbnail, &context).await?;
(
Some(metadata.opengraph_data.title),
Some(metadata.opengraph_data.description),
@ -83,11 +89,21 @@ pub async fn update_post(
}
_ => Default::default(),
};
let url = match url {
Some(url) => Some(proxy_image_link_opt_apub(Some(url), &context).await?),
_ => Default::default(),
};
let custom_thumbnail = match custom_thumbnail {
Some(custom_thumbnail) => {
Some(proxy_image_link_opt_apub(Some(custom_thumbnail), &context).await?)
}
_ => Default::default(),
};
let thumbnail_url = custom_thumbnail.or(metadata_thumbnail);
let language_id = data.language_id;
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),

@ -79,7 +79,7 @@ pub async fn get_site(
|pool| CommunityBlockView::for_person(pool, person_id),
|pool| InstanceBlockView::for_person(pool, person_id),
|pool| PersonBlockView::for_person(pool, person_id),
|pool| CommunityModeratorView::for_person(pool, person_id),
|pool| CommunityModeratorView::for_person(pool, person_id, true),
|pool| LocalUserLanguage::read(pool, local_user_id)
))
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;

@ -126,6 +126,14 @@ pub async fn register(
// Also fixes a bug which allows users to log in when registrations are changed to closed.
let accepted_application = Some(!require_registration_application);
// Get the user's preferred language using the Accept-Language header
let language_tag = req.headers().get("Accept-Language").and_then(|hdr| {
accept_language::parse(hdr.to_str().unwrap_or_default())
.first()
// Remove the optional region code
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
});
// Create the local user
let local_user_form = LocalUserInsertForm::builder()
.person_id(inserted_person.id)
@ -135,6 +143,7 @@ pub async fn register(
.accepted_application(accepted_application)
.default_listing_type(Some(local_site.default_post_listing_type))
.post_listing_mode(Some(local_site.default_post_listing_mode))
.interface_language(language_tag)
// If its the initial site setup, they are an admin
.admin(Some(!local_site.site_setup))
.build();

@ -22,7 +22,10 @@ use activitypub_federation::{
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::{activity::ActivitySendTargets, community::CommunityFollower};
use lemmy_db_schema::{
source::{activity::ActivitySendTargets, community::CommunityFollower},
CommunityVisibility,
};
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
use serde_json::Value;
use url::Url;
@ -210,6 +213,10 @@ async fn can_accept_activity_in_community(
{
Err(LemmyErrorType::CommunityHasNoFollowers)?
}
// Local only community can't federate
if community.visibility != CommunityVisibility::Public {
return Err(LemmyErrorType::CouldntFindCommunity.into());
}
}
Ok(())
}

@ -6,7 +6,10 @@ use crate::{
};
use activitypub_federation::{config::Data, traits::Actor};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::{activity::ActivitySendTargets, person::PersonFollower};
use lemmy_db_schema::{
source::{activity::ActivitySendTargets, person::PersonFollower},
CommunityVisibility,
};
use lemmy_utils::error::LemmyError;
pub mod announce;
@ -37,6 +40,11 @@ pub(crate) async fn send_activity_in_community(
is_mod_action: bool,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
// If community is local only, don't send anything out
if community.visibility != CommunityVisibility::Public {
return Ok(());
}
// send to any users which are mentioned or affected directly
let mut inboxes = extra_inboxes;

@ -14,7 +14,10 @@ use activitypub_federation::{
kinds::activity::FlagType,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{
context::LemmyContext,
utils::{check_comment_deleted_or_removed, check_post_deleted_or_removed},
};
use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
@ -104,6 +107,8 @@ impl ActivityHandler for Report {
let reason = self.reason()?;
match self.object.dereference(context).await? {
PostOrComment::Post(post) => {
check_post_deleted_or_removed(&post)?;
let report_form = PostReportForm {
creator_id: actor.id,
post_id: post.id,
@ -115,6 +120,8 @@ impl ActivityHandler for Report {
PostReport::report(&mut context.pool(), &report_form).await?;
}
PostOrComment::Comment(comment) => {
check_comment_deleted_or_removed(&comment)?;
let report_form = CommentReportForm {
creator_id: actor.id,
comment_id: comment.id,

@ -97,15 +97,10 @@ impl ActivityHandler for UpdateCommunity {
&None,
&self.object.source,
)),
removed: None,
published: self.object.published.map(Into::into),
updated: Some(self.object.updated.map(Into::into)),
deleted: None,
nsfw: Some(self.object.sensitive.unwrap_or(false)),
actor_id: Some(self.object.id.into()),
local: None,
private_key: None,
hidden: None,
public_key: Some(self.object.public_key.public_key_pem),
last_refreshed_at: Some(naive_now()),
icon: Some(self.object.icon.map(|i| i.url.into())),
@ -116,6 +111,7 @@ impl ActivityHandler for UpdateCommunity {
moderators_url: self.object.attributed_to.map(Into::into),
posting_restricted_to_mods: self.object.posting_restricted_to_mods,
featured_url: self.object.featured.map(Into::into),
..Default::default()
};
Community::update(&mut context.pool(), community.id, &community_update_form).await?;

@ -12,6 +12,7 @@ use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
comment::{Comment, CommentUpdateForm},
comment_report::CommentReport,
community::{Community, CommunityUpdateForm},
moderator::{
ModRemoveComment,
@ -22,8 +23,9 @@ use lemmy_db_schema::{
ModRemovePostForm,
},
post::{Post, PostUpdateForm},
post_report::PostReport,
},
traits::Crud,
traits::{Crud, Reportable},
};
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use url::Url;
@ -136,6 +138,7 @@ pub(in crate::activities) async fn receive_remove_action(
.await?;
}
DeletableObjects::Post(post) => {
PostReport::resolve_all_for_object(&mut context.pool(), post.id, actor.id).await?;
let form = ModRemovePostForm {
mod_person_id: actor.id,
post_id: post.id,
@ -154,6 +157,7 @@ pub(in crate::activities) async fn receive_remove_action(
.await?;
}
DeletableObjects::Comment(comment) => {
CommentReport::resolve_all_for_object(&mut context.pool(), comment.id, actor.id).await?;
let form = ModRemoveCommentForm {
mod_person_id: actor.id,
comment_id: comment.id,

@ -24,8 +24,9 @@ use lemmy_db_schema::{
person::{PersonFollower, PersonFollowerForm},
},
traits::Followable,
CommunityVisibility,
};
use lemmy_utils::error::LemmyError;
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use url::Url;
impl Follow {
@ -103,6 +104,10 @@ impl ActivityHandler for Follow {
PersonFollower::follow(&mut context.pool(), &form).await?;
}
UserOrCommunity::Community(c) => {
// Dont allow following local-only community via federation.
if c.visibility != CommunityVisibility::Public {
return Err(LemmyErrorType::CouldntFindCommunity.into());
}
let form = CommunityFollowerForm {
community_id: c.id,
person_id: actor.id,

@ -87,8 +87,12 @@ pub async fn read_person(
.list(&mut context.pool())
.await?;
let moderates =
CommunityModeratorView::for_person(&mut context.pool(), person_details_id).await?;
let moderates = CommunityModeratorView::for_person(
&mut context.pool(),
person_details_id,
local_user_view.is_some(),
)
.await?;
let site = read_site_for_actor(person_view.person.actor_id.clone(), &context).await?;

@ -14,7 +14,7 @@ use lemmy_db_schema::{source::post::Post, utils::FETCH_LIMIT_MAX};
use lemmy_utils::error::LemmyError;
use url::Url;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ApubCommunityFeatured(());
#[async_trait::async_trait]

@ -1,11 +1,20 @@
use crate::{
http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object},
http::{
check_community_public,
create_apub_response,
create_apub_tombstone_response,
redirect_remote_object,
},
objects::comment::ApubComment,
};
use activitypub_federation::{config::Data, traits::Object};
use actix_web::{web::Path, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{newtypes::CommentId, source::comment::Comment, traits::Crud};
use lemmy_db_schema::{
newtypes::CommentId,
source::{comment::Comment, community::Community, post::Post},
traits::Crud,
};
use lemmy_utils::error::LemmyError;
use serde::Deserialize;
@ -21,7 +30,12 @@ pub(crate) async fn get_apub_comment(
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let id = CommentId(info.comment_id.parse::<i32>()?);
// Can't use CommentView here because it excludes deleted/removed/local-only items
let comment: ApubComment = Comment::read(&mut context.pool(), id).await?.into();
let post = Post::read(&mut context.pool(), comment.post_id).await?;
let community = Community::read(&mut context.pool(), post.community_id).await?;
check_community_public(&community)?;
if !comment.local {
Ok(redirect_remote_object(&comment.ap_id))
} else if !comment.deleted && !comment.removed {

@ -6,7 +6,7 @@ use crate::{
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
},
http::{create_apub_response, create_apub_tombstone_response},
http::{check_community_public, create_apub_response, create_apub_tombstone_response},
objects::{community::ApubCommunity, person::ApubPerson},
};
use activitypub_federation::{
@ -18,10 +18,10 @@ use activitypub_federation::{
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use lemmy_utils::error::LemmyError;
use serde::Deserialize;
#[derive(Deserialize)]
#[derive(Deserialize, Clone)]
pub(crate) struct CommunityQuery {
community_name: String,
}
@ -37,13 +37,13 @@ pub(crate) async fn get_apub_community_http(
.await?
.into();
if !community.deleted && !community.removed {
let apub = community.into_json(&context).await?;
create_apub_response(&apub)
} else {
create_apub_tombstone_response(community.actor_id.clone())
if community.deleted || community.removed {
return create_apub_tombstone_response(community.actor_id.clone());
}
check_community_public(&community)?;
let apub = community.into_json(&context).await?;
create_apub_response(&apub)
}
/// Handler for all incoming receive to community inboxes.
@ -66,6 +66,7 @@ pub(crate) async fn get_apub_community_followers(
) -> Result<HttpResponse, LemmyError> {
let community =
Community::read_from_name(&mut context.pool(), &info.community_name, false).await?;
check_community_public(&community)?;
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
create_apub_response(&followers)
}
@ -80,9 +81,7 @@ pub(crate) async fn get_apub_community_outbox(
Community::read_from_name(&mut context.pool(), &info.community_name, false)
.await?
.into();
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
}
check_community_public(&community)?;
let outbox = ApubCommunityOutbox::read_local(&community, &context).await?;
create_apub_response(&outbox)
}
@ -96,9 +95,7 @@ pub(crate) async fn get_apub_community_moderators(
Community::read_from_name(&mut context.pool(), &info.community_name, false)
.await?
.into();
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
}
check_community_public(&community)?;
let moderators = ApubCommunityModerators::read_local(&community, &context).await?;
create_apub_response(&moderators)
}
@ -112,9 +109,153 @@ pub(crate) async fn get_apub_community_featured(
Community::read_from_name(&mut context.pool(), &info.community_name, false)
.await?
.into();
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
}
check_community_public(&community)?;
let featured = ApubCommunityFeatured::read_local(&community, &context).await?;
create_apub_response(&featured)
}
#[cfg(test)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::protocol::objects::{group::Group, tombstone::Tombstone};
use actix_web::body::to_bytes;
use lemmy_db_schema::{
source::{
community::{Community, CommunityInsertForm},
instance::Instance,
},
traits::Crud,
CommunityVisibility,
};
use lemmy_utils::error::LemmyResult;
use serde::de::DeserializeOwned;
use serial_test::serial;
async fn init(
deleted: bool,
visibility: CommunityVisibility,
context: &Data<LemmyContext>,
) -> Result<(Instance, Community), LemmyError> {
let instance =
Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?;
let community_form = CommunityInsertForm::builder()
.name("testcom6".to_string())
.title("nada".to_owned())
.public_key("pubkey".to_string())
.instance_id(instance.id)
.deleted(Some(deleted))
.visibility(Some(visibility))
.build();
let community = Community::create(&mut context.pool(), &community_form).await?;
Ok((instance, community))
}
async fn decode_response<T: DeserializeOwned>(res: HttpResponse) -> Result<T, LemmyError> {
let body = to_bytes(res.into_body()).await.unwrap();
let body = std::str::from_utf8(&body)?;
Ok(serde_json::from_str(body)?)
}
#[tokio::test]
#[serial]
async fn test_get_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
// fetch invalid community
let query = CommunityQuery {
community_name: "asd".to_string(),
};
let res = get_apub_community_http(query.into(), context.reset_request_count()).await;
assert!(res.is_err());
let (instance, community) = init(false, CommunityVisibility::Public, &context).await?;
// fetch valid community
let query = CommunityQuery {
community_name: community.name.clone(),
};
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
let res_group: Group = decode_response(res).await?;
let community: ApubCommunity = community.into();
let group = community.clone().into_json(&context).await?;
assert_eq!(group, res_group);
let res =
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await?;
assert_eq!(200, res.status());
Instance::delete(&mut context.pool(), instance.id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_get_deleted_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, community) = init(true, CommunityVisibility::LocalOnly, &context).await?;
// should return tombstone
let query = CommunityQuery {
community_name: community.name.clone(),
};
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?;
assert_eq!(410, res.status());
let res_tombstone = decode_response::<Tombstone>(res).await;
assert!(res_tombstone.is_ok());
let res =
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await;
assert!(res.is_err());
//Community::delete(&mut context.pool(), community.id).await?;
Instance::delete(&mut context.pool(), instance.id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_get_local_only_community() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?;
let query = CommunityQuery {
community_name: community.name.clone(),
};
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res =
get_apub_community_featured(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res =
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res =
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
assert!(res.is_err());
let res = get_apub_community_outbox(query.into(), context.reset_request_count()).await;
assert!(res.is_err());
Instance::delete(&mut context.pool(), instance.id).await?;
Ok(())
}
}

@ -13,8 +13,12 @@ use activitypub_federation::{
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use http::{header::LOCATION, StatusCode};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{newtypes::DbUrl, source::activity::SentActivity};
use lemmy_utils::error::{LemmyError, LemmyResult};
use lemmy_db_schema::{
newtypes::DbUrl,
source::{activity::SentActivity, community::Community},
CommunityVisibility,
};
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use url::Url;
@ -102,3 +106,14 @@ pub(crate) async fn get_activity(
create_apub_response(&activity.data)
}
}
/// Ensure that the community is public and not removed/deleted.
fn check_community_public(community: &Community) -> LemmyResult<()> {
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
}
if community.visibility != CommunityVisibility::Public {
return Err(LemmyErrorType::CouldntFindCommunity.into());
}
Ok(())
}

@ -1,11 +1,20 @@
use crate::{
http::{create_apub_response, create_apub_tombstone_response, redirect_remote_object},
http::{
check_community_public,
create_apub_response,
create_apub_tombstone_response,
redirect_remote_object,
},
objects::post::ApubPost,
};
use activitypub_federation::{config::Data, traits::Object};
use actix_web::{web, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{newtypes::PostId, source::post::Post, traits::Crud};
use lemmy_db_schema::{
newtypes::PostId,
source::{community::Community, post::Post},
traits::Crud,
};
use lemmy_utils::error::LemmyError;
use serde::Deserialize;
@ -21,7 +30,11 @@ pub(crate) async fn get_apub_post(
context: Data<LemmyContext>,
) -> Result<HttpResponse, LemmyError> {
let id = PostId(info.post_id.parse::<i32>()?);
// Can't use PostView here because it excludes deleted/removed/local-only items
let post: ApubPost = Post::read(&mut context.pool(), id).await?.into();
let community = Community::read(&mut context.pool(), post.community_id).await?;
check_community_public(&community)?;
if !post.local {
Ok(redirect_remote_object(&post.ap_id))
} else if !post.deleted && !post.removed {

@ -150,15 +150,12 @@ impl Object for ApubCommunity {
name: group.preferred_username.clone(),
title: group.name.unwrap_or(group.preferred_username.clone()),
description,
removed: None,
published: group.published,
updated: group.updated,
deleted: Some(false),
nsfw: Some(group.sensitive.unwrap_or(false)),
actor_id: Some(group.id.into()),
local: Some(false),
private_key: None,
hidden: None,
public_key: group.public_key.public_key_pem,
last_refreshed_at: Some(naive_now()),
icon,
@ -170,6 +167,7 @@ impl Object for ApubCommunity {
posting_restricted_to_mods: group.posting_restricted_to_mods,
instance_id,
featured_url: group.featured.map(Into::into),
..Default::default()
};
let languages =
LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).await?;

@ -54,7 +54,7 @@ use url::Url;
const MAX_TITLE_LENGTH: usize = 200;
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct ApubPost(pub(crate) Post);
impl Deref for ApubPost {

@ -16,7 +16,7 @@ pub mod activities;
pub(crate) mod collections;
pub(crate) mod objects;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Source {
pub(crate) content: String,
@ -32,7 +32,7 @@ impl Source {
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ImageObject {
#[serde(rename = "type")]

@ -35,7 +35,7 @@ use std::fmt::Debug;
use url::Url;
#[skip_serializing_none]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Group {
#[serde(rename = "type")]

@ -16,14 +16,14 @@ pub(crate) mod page;
pub(crate) mod person;
pub(crate) mod tombstone;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Endpoints {
pub shared_inbox: Url,
}
/// As specified in https://schema.org/Language
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LanguageTag {
pub(crate) identifier: String,

@ -132,6 +132,10 @@ async fn try_main() -> LemmyResult<()> {
// Make sure the println above shows the correct amount
assert_eq!(num_inserted_posts, num_posts as usize);
// Manually trigger and wait for a statistics update to ensure consistent and high amount of accuracy in the statistics used for query planning
println!("🧮 updating database statistics");
conn.batch_execute("ANALYZE;").await?;
// Enable auto_explain
conn
.batch_execute(

@ -1,6 +1,9 @@
use crate::{
newtypes::{CommentReportId, PersonId},
schema::comment_report::dsl::{comment_report, resolved, resolver_id, updated},
newtypes::{CommentId, CommentReportId, PersonId},
schema::comment_report::{
comment_id,
dsl::{comment_report, resolved, resolver_id, updated},
},
source::comment_report::{CommentReport, CommentReportForm},
traits::Reportable,
utils::{get_conn, naive_now, DbPool},
@ -17,6 +20,7 @@ use diesel_async::RunQueryDsl;
impl Reportable for CommentReport {
type Form = CommentReportForm;
type IdType = CommentReportId;
type ObjectIdType = CommentId;
/// creates a comment report and returns it
///
/// * `conn` - the postgres connection
@ -53,6 +57,22 @@ impl Reportable for CommentReport {
.await
}
async fn resolve_all_for_object(
pool: &mut DbPool<'_>,
comment_id_: CommentId,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
update(comment_report.filter(comment_id.eq(comment_id_)))
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
))
.execute(conn)
.await
}
/// unresolve a comment report
///
/// * `conn` - the postgres connection

@ -365,6 +365,7 @@ mod tests {
},
traits::{Bannable, Crud, Followable, Joinable},
utils::build_db_pool_for_tests,
CommunityVisibility,
};
use pretty_assertions::assert_eq;
use serial_test::serial;
@ -421,6 +422,7 @@ mod tests {
hidden: false,
posting_restricted_to_mods: false,
instance_id: inserted_instance.id,
visibility: CommunityVisibility::Public,
};
let community_follower_form = CommunityFollowerForm {

@ -1,12 +1,6 @@
use crate::{
newtypes::{DbUrl, LocalUserId, PersonId},
schema::local_user::dsl::{
accepted_application,
email,
email_verified,
local_user,
password_encrypted,
},
schema::{local_user, person, registration_application},
source::{
actor_language::{LocalUserLanguage, SiteLanguage},
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
@ -15,11 +9,18 @@ use crate::{
utils::{
functions::{coalesce, lower},
get_conn,
now,
DbPool,
},
};
use bcrypt::{hash, DEFAULT_COST};
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, JoinOnDsl, QueryDsl};
use diesel::{
dsl::{insert_into, not, IntervalDsl},
result::Error,
ExpressionMethods,
JoinOnDsl,
QueryDsl,
};
use diesel_async::RunQueryDsl;
impl LocalUser {
@ -31,16 +32,16 @@ impl LocalUser {
let conn = &mut get_conn(pool).await?;
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
diesel::update(local_user.find(local_user_id))
.set((password_encrypted.eq(password_hash),))
diesel::update(local_user::table.find(local_user_id))
.set((local_user::password_encrypted.eq(password_hash),))
.get_result::<Self>(conn)
.await
}
pub async fn set_all_users_email_verified(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(local_user)
.set(email_verified.eq(true))
diesel::update(local_user::table)
.set(local_user::email_verified.eq(true))
.get_results::<Self>(conn)
.await
}
@ -49,18 +50,43 @@ impl LocalUser {
pool: &mut DbPool<'_>,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(local_user)
.set(accepted_application.eq(true))
diesel::update(local_user::table)
.set(local_user::accepted_application.eq(true))
.get_results::<Self>(conn)
.await
}
pub async fn is_email_taken(pool: &mut DbPool<'_>, email_: &str) -> Result<bool, Error> {
pub async fn delete_old_denied_local_users(pool: &mut DbPool<'_>) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
// Make sure:
// - The deny reason exists
// - The app is older than a week
// - The accepted_application is false
let old_denied_registrations = registration_application::table
.filter(registration_application::deny_reason.is_not_null())
.filter(registration_application::published.lt(now() - 1.week()))
.select(registration_application::local_user_id);
// Delete based on join logic is here:
// https://stackoverflow.com/questions/60836040/how-do-i-perform-a-delete-with-sub-query-in-diesel-against-a-postgres-database
let local_users = local_user::table
.filter(local_user::id.eq_any(old_denied_registrations))
.filter(not(local_user::accepted_application))
.select(local_user::person_id);
// Delete the person rows, which should automatically clear the local_user ones
let persons = person::table.filter(person::id.eq_any(local_users));
diesel::delete(persons).execute(conn).await
}
pub async fn is_email_taken(pool: &mut DbPool<'_>, email: &str) -> Result<bool, Error> {
use diesel::dsl::{exists, select};
let conn = &mut get_conn(pool).await?;
select(exists(
local_user.filter(lower(coalesce(email, "")).eq(email_.to_lowercase())),
))
select(exists(local_user::table.filter(
lower(coalesce(local_user::email, "")).eq(email.to_lowercase()),
)))
.get_result(conn)
.await
}
@ -78,7 +104,6 @@ impl LocalUser {
community_follower,
instance,
instance_block,
person,
person_block,
post,
post_saved,
@ -140,6 +165,15 @@ impl LocalUser {
}
}
impl LocalUserInsertForm {
pub fn test_form(person_id: PersonId) -> Self {
Self::builder()
.person_id(person_id)
.password_encrypted(String::new())
.build()
}
}
pub struct UserBackupLists {
pub followed_communities: Vec<DbUrl>,
pub saved_posts: Vec<DbUrl>,
@ -162,7 +196,7 @@ impl Crud for LocalUser {
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
form_with_encrypted_password.password_encrypted = password_hash;
let local_user_ = insert_into(local_user)
let local_user_ = insert_into(local_user::table)
.values(form_with_encrypted_password)
.get_result::<Self>(conn)
.await?;
@ -185,7 +219,7 @@ impl Crud for LocalUser {
form: &Self::UpdateForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(local_user.find(local_user_id))
diesel::update(local_user::table.find(local_user_id))
.set(form)
.get_result::<Self>(conn)
.await

@ -1,5 +1,5 @@
use crate::{
newtypes::{CommunityId, DbUrl, PersonId},
newtypes::{CommunityId, DbUrl, InstanceId, PersonId},
schema::{instance, local_user, person, person_follower},
source::person::{
Person,
@ -86,6 +86,16 @@ impl Person {
}
}
impl PersonInsertForm {
pub fn test_form(instance_id: InstanceId, name: &str) -> Self {
Self::builder()
.name(name.to_owned())
.public_key("pubkey".to_string())
.instance_id(instance_id)
.build()
}
}
#[async_trait]
impl ApubActor for Person {
async fn read_from_apub_id(

@ -1,6 +1,9 @@
use crate::{
newtypes::{PersonId, PostReportId},
schema::post_report::dsl::{post_report, resolved, resolver_id, updated},
newtypes::{PersonId, PostId, PostReportId},
schema::post_report::{
dsl::{post_report, resolved, resolver_id, updated},
post_id,
},
source::post_report::{PostReport, PostReportForm},
traits::Reportable,
utils::{get_conn, naive_now, DbPool},
@ -17,6 +20,7 @@ use diesel_async::RunQueryDsl;
impl Reportable for PostReport {
type Form = PostReportForm;
type IdType = PostReportId;
type ObjectIdType = PostId;
async fn report(pool: &mut DbPool<'_>, post_report_form: &PostReportForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
@ -42,6 +46,22 @@ impl Reportable for PostReport {
.await
}
async fn resolve_all_for_object(
pool: &mut DbPool<'_>,
post_id_: PostId,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
update(post_report.filter(post_id.eq(post_id_)))
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
))
.execute(conn)
.await
}
async fn unresolve(
pool: &mut DbPool<'_>,
report_id: Self::IdType,
@ -75,7 +95,6 @@ mod tests {
traits::Crud,
utils::build_db_pool_for_tests,
};
use pretty_assertions::assert_eq;
use serial_test::serial;
async fn init(pool: &mut DbPool<'_>) -> (Person, PostReport) {
@ -135,4 +154,21 @@ mod tests {
Person::delete(pool, person.id).await.unwrap();
Post::delete(pool, report.post_id).await.unwrap();
}
#[tokio::test]
#[serial]
async fn test_resolve_all_post_reports() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (person, report) = init(pool).await;
let resolved_count = PostReport::resolve_all_for_object(pool, report.post_id, person.id)
.await
.unwrap();
assert_eq!(resolved_count, 1);
Person::delete(pool, person.id).await.unwrap();
Post::delete(pool, report.post_id).await.unwrap();
}
}

@ -1,5 +1,5 @@
use crate::{
newtypes::{PersonId, PrivateMessageReportId},
newtypes::{PersonId, PrivateMessageId, PrivateMessageReportId},
schema::private_message_report::dsl::{private_message_report, resolved, resolver_id, updated},
source::private_message_report::{PrivateMessageReport, PrivateMessageReportForm},
traits::Reportable,
@ -17,6 +17,7 @@ use diesel_async::RunQueryDsl;
impl Reportable for PrivateMessageReport {
type Form = PrivateMessageReportForm;
type IdType = PrivateMessageReportId;
type ObjectIdType = PrivateMessageId;
async fn report(
pool: &mut DbPool<'_>,
@ -45,6 +46,15 @@ impl Reportable for PrivateMessageReport {
.await
}
// TODO: this is unused because private message doesnt have remove handler
async fn resolve_all_for_object(
_pool: &mut DbPool<'_>,
_pm_id_: PrivateMessageId,
_by_resolver_id: PersonId,
) -> Result<usize, Error> {
unimplemented!()
}
async fn unresolve(
pool: &mut DbPool<'_>,
report_id: Self::IdType,

@ -49,7 +49,7 @@ use strum_macros::{Display, EnumString};
use ts_rs::TS;
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default,
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash,
)]
#[cfg_attr(feature = "full", derive(DbEnum, TS))]
#[cfg_attr(
@ -83,7 +83,7 @@ pub enum SortType {
Scaled,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)]
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The comment sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html
@ -96,7 +96,7 @@ pub enum CommentSortType {
}
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default,
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash,
)]
#[cfg_attr(feature = "full", derive(DbEnum, TS))]
#[cfg_attr(
@ -119,7 +119,7 @@ pub enum ListingType {
}
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default,
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash,
)]
#[cfg_attr(feature = "full", derive(DbEnum, TS))]
#[cfg_attr(
@ -140,7 +140,7 @@ pub enum RegistrationMode {
}
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq,
EnumString, Display, Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Hash,
)]
#[cfg_attr(feature = "full", derive(DbEnum, TS))]
#[cfg_attr(
@ -160,7 +160,7 @@ pub enum PostListingMode {
SmallCard,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)]
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The type of content returned from a search.
@ -173,7 +173,7 @@ pub enum SearchType {
Url,
}
#[derive(EnumString, Display, Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)]
#[derive(EnumString, Display, Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A type / status for a community subscribe.
@ -183,7 +183,7 @@ pub enum SubscribedType {
Pending,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A list of possible types for the various modlog actions.
@ -207,7 +207,7 @@ pub enum ModlogActionType {
}
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq,
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash,
)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
@ -220,6 +220,27 @@ pub enum PostFeatureType {
Community,
}
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash,
)]
#[cfg_attr(feature = "full", derive(DbEnum, TS))]
#[cfg_attr(
feature = "full",
ExistingTypePath = "crate::schema::sql_types::CommunityVisibility"
)]
#[cfg_attr(feature = "full", DbValueStyle = "verbatim")]
#[cfg_attr(feature = "full", ts(export))]
/// Defines who can browse and interact with content in a community.
///
/// TODO: Also use this to define private communities
pub enum CommunityVisibility {
/// Public community, any local or federated user can interact.
#[default]
Public,
/// Unfederated community, only local users can interact.
LocalOnly,
}
/// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the
/// vec on failure.
#[macro_export]

@ -5,6 +5,10 @@ pub mod sql_types {
#[diesel(postgres_type(name = "actor_type_enum"))]
pub struct ActorTypeEnum;
#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "community_visibility"))]
pub struct CommunityVisibility;
#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "listing_type_enum"))]
pub struct ListingTypeEnum;
@ -150,6 +154,9 @@ diesel::table! {
}
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::CommunityVisibility;
community (id) {
id -> Int4,
#[max_length = 255]
@ -183,6 +190,7 @@ diesel::table! {
moderators_url -> Nullable<Varchar>,
#[max_length = 255]
featured_url -> Nullable<Varchar>,
visibility -> CommunityVisibility,
}
}

@ -3,6 +3,7 @@ use crate::schema::{community, community_follower, community_moderator, communit
use crate::{
newtypes::{CommunityId, DbUrl, InstanceId, PersonId},
source::placeholder_apub_url,
CommunityVisibility,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
@ -66,9 +67,10 @@ pub struct Community {
/// Url where featured posts collection is served over Activitypub
#[serde(skip)]
pub featured_url: Option<DbUrl>,
pub visibility: CommunityVisibility,
}
#[derive(Debug, Clone, TypedBuilder)]
#[derive(Debug, Clone, TypedBuilder, Default)]
#[builder(field_defaults(default))]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = community))]
@ -99,6 +101,7 @@ pub struct CommunityInsertForm {
pub posting_restricted_to_mods: Option<bool>,
#[builder(!default)]
pub instance_id: InstanceId,
pub visibility: Option<CommunityVisibility>,
}
#[derive(Debug, Clone, Default)]
@ -126,6 +129,7 @@ pub struct CommunityUpdateForm {
pub featured_url: Option<DbUrl>,
pub hidden: Option<bool>,
pub posting_restricted_to_mods: Option<bool>,
pub visibility: Option<CommunityVisibility>,
}
#[derive(PartialEq, Eq, Debug)]

@ -144,6 +144,7 @@ pub trait Blockable {
pub trait Reportable {
type Form;
type IdType;
type ObjectIdType;
async fn report(pool: &mut DbPool<'_>, form: &Self::Form) -> Result<Self, Error>
where
Self: Sized;
@ -152,6 +153,13 @@ pub trait Reportable {
report_id: Self::IdType,
resolver_id: PersonId,
) -> Result<usize, Error>
where
Self: Sized;
async fn resolve_all_for_object(
pool: &mut DbPool<'_>,
comment_id_: Self::ObjectIdType,
by_resolver_id: PersonId,
) -> Result<usize, Error>
where
Self: Sized;
async fn unresolve(

@ -25,10 +25,11 @@ use diesel::{
use diesel_async::{
pg::AsyncPgConnection,
pooled_connection::{
deadpool::{Object as PooledConnection, Pool},
deadpool::{Hook, HookError, Object as PooledConnection, Pool},
AsyncDieselConnectionManager,
ManagerConfig,
},
SimpleAsyncConnection,
};
use diesel_migrations::EmbeddedMigrations;
use futures_util::{future::BoxFuture, Future, FutureExt};
@ -46,7 +47,7 @@ use rustls::{
use std::{
ops::{Deref, DerefMut},
sync::Arc,
time::SystemTime,
time::{Duration, SystemTime},
};
use tracing::{error, info};
use url::Url;
@ -335,7 +336,14 @@ fn establish_connection(config: &str) -> BoxFuture<ConnectionResult<AsyncPgConne
error!("Database connection failed: {e}");
}
});
AsyncPgConnection::try_from(client).await
let mut conn = AsyncPgConnection::try_from(client).await?;
// * Change geqo_threshold back to default value if it was changed, so it's higher than the collapse limits
// * Change collapse limits from 8 to 11 so the query planner can find a better table join order for more complicated queries
conn
.batch_execute("SET geqo_threshold=12;SET from_collapse_limit=11;SET join_collapse_limit=11;")
.await
.map_err(ConnectionError::CouldntSetupConfiguration)?;
Ok(conn)
};
fut.boxed()
}
@ -389,6 +397,16 @@ pub async fn build_db_pool() -> Result<ActualDbPool, LemmyError> {
let pool = Pool::builder(manager)
.max_size(SETTINGS.database.pool_size)
.runtime(Runtime::Tokio1)
// Limit connection age to prevent use of prepared statements that have query plans based on very old statistics
.pre_recycle(Hook::sync_fn(|_conn, metrics| {
// Preventing the first recycle can cause an infinite loop when trying to get a new connection from the pool
let conn_was_used = metrics.recycled.is_some();
if metrics.age() > Duration::from_secs(3 * 24 * 60 * 60) && conn_was_used {
Err(HookError::Continue(None))
} else {
Ok(())
}
}))
.build()?;
run_migrations(&db_url)?;

@ -233,6 +233,7 @@ mod tests {
},
traits::{Crud, Joinable, Reportable},
utils::{build_db_pool_for_tests, RANK_DEFAULT},
CommunityVisibility,
};
use pretty_assertions::assert_eq;
use serial_test::serial;
@ -379,6 +380,7 @@ mod tests {
moderators_url: inserted_community.moderators_url,
featured_url: inserted_community.featured_url,
instance_id: inserted_instance.id,
visibility: CommunityVisibility::Public,
},
creator: Person {
id: inserted_jessica.id,

@ -36,6 +36,7 @@ use lemmy_db_schema::{
},
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
CommentSortType,
CommunityVisibility,
ListingType,
};
@ -167,13 +168,16 @@ fn queries<'a>() -> Queries<
let read = move |mut conn: DbConn<'a>,
(comment_id, my_person_id): (CommentId, Option<PersonId>)| async move {
all_joins(
let mut query = all_joins(
comment::table.find(comment_id).into_boxed(),
my_person_id,
false,
)
.first::<CommentView>(&mut conn)
.await
);
// Hide local only communities from unauthenticated users
if my_person_id.is_none() {
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
}
query.first::<CommentView>(&mut conn).await
};
let list = move |mut conn: DbConn<'a>, options: CommentQuery<'a>| async move {
@ -287,6 +291,11 @@ fn queries<'a>() -> Queries<
query = query.filter(not(is_creator_blocked(person_id_join)));
};
// Hide comments in local only communities from unauthenticated users
if options.local_user.is_none() {
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
}
// A Max depth given means its a tree fetch
let (limit, offset) = if let Some(max_depth) = options.max_depth {
let depth_limit = if let Some(parent_path) = options.parent_path.as_ref() {
@ -405,7 +414,13 @@ mod tests {
source::{
actor_language::LocalUserLanguage,
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm},
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
community::{
Community,
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
CommunityUpdateForm,
},
instance::Instance,
language::Language,
local_user::{LocalUser, LocalUserInsertForm},
@ -415,6 +430,7 @@ mod tests {
},
traits::{Blockable, Crud, Joinable, Likeable},
utils::{build_db_pool_for_tests, RANK_DEFAULT},
CommunityVisibility,
SubscribedType,
};
use pretty_assertions::assert_eq;
@ -1042,6 +1058,7 @@ mod tests {
shared_inbox_url: data.inserted_community.shared_inbox_url.clone(),
moderators_url: data.inserted_community.moderators_url.clone(),
featured_url: data.inserted_community.featured_url.clone(),
visibility: CommunityVisibility::Public,
},
counts: CommentAggregates {
comment_id: data.inserted_comment_0.id,
@ -1055,4 +1072,53 @@ mod tests {
},
}
}
#[tokio::test]
#[serial]
async fn local_only_instance() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
Community::update(
pool,
data.inserted_community.id,
&CommunityUpdateForm {
visibility: Some(CommunityVisibility::LocalOnly),
..Default::default()
},
)
.await
.unwrap();
let unauthenticated_query = CommentQuery {
..Default::default()
}
.list(pool)
.await
.unwrap();
assert_eq!(0, unauthenticated_query.len());
let authenticated_query = CommentQuery {
local_user: Some(&data.timmy_local_user_view),
..Default::default()
}
.list(pool)
.await
.unwrap();
assert_eq!(5, authenticated_query.len());
let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await;
assert!(unauthenticated_comment.is_err());
let authenticated_comment = CommentView::read(
pool,
data.inserted_comment_0.id,
Some(data.timmy_local_user_view.person.id),
)
.await;
assert!(authenticated_comment.is_ok());
cleanup(data, pool).await;
}
}

@ -201,7 +201,6 @@ mod tests {
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
moderator::{ModRemovePost, ModRemovePostForm},
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
post_report::{PostReport, PostReportForm},
@ -350,14 +349,11 @@ mod tests {
.unwrap();
assert_eq!(2, report_count);
// Writing post removal to mod log should automatically resolve reports
let remove_form = ModRemovePostForm {
mod_person_id: inserted_timmy.id,
post_id: inserted_jessica_report.post_id,
reason: None,
removed: Some(true),
};
ModRemovePost::create(pool, &remove_form).await.unwrap();
// Pretend the post was removed, and resolve all reports for that object.
// This is called manually in the API for post removals
PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, inserted_timmy.id)
.await
.unwrap();
let read_jessica_report_view_after_resolve =
PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id)

@ -54,6 +54,7 @@ use lemmy_db_schema::{
ReadFn,
ReverseTimestampKey,
},
CommunityVisibility,
ListingType,
SortType,
};
@ -259,6 +260,11 @@ fn queries<'a>() -> Queries<
);
}
// Hide posts in local only communities from unauthenticated users
if my_person_id.is_none() {
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
}
Commented::new(query)
.text("PostView::read")
.first::<PostView>(&mut conn)
@ -408,6 +414,11 @@ fn queries<'a>() -> Queries<
}
};
// Hide posts in local only communities from unauthenticated users
if options.local_user.is_none() {
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
}
// Dont filter blocks or missing languages for moderator view type
if let (Some(person_id), false) = (
my_person_id,
@ -697,11 +708,17 @@ mod tests {
use lemmy_db_schema::{
aggregates::structs::PostAggregates,
impls::actor_language::UNDETERMINED_ID,
newtypes::{InstanceId, LanguageId, PersonId},
newtypes::LanguageId,
source::{
actor_language::LocalUserLanguage,
comment::{Comment, CommentInsertForm},
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
community::{
Community,
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
CommunityUpdateForm,
},
community_block::{CommunityBlock, CommunityBlockForm},
instance::Instance,
instance_block::{InstanceBlock, InstanceBlockForm},
@ -713,7 +730,8 @@ mod tests {
site::Site,
},
traits::{Blockable, Crud, Joinable, Likeable},
utils::{build_db_pool, DbPool, RANK_DEFAULT},
utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT},
CommunityVisibility,
SortType,
SubscribedType,
};
@ -752,37 +770,22 @@ mod tests {
}
}
fn default_person_insert_form(instance_id: InstanceId, name: &str) -> PersonInsertForm {
PersonInsertForm::builder()
.name(name.to_owned())
.public_key("pubkey".to_string())
.instance_id(instance_id)
.build()
}
fn default_local_user_form(person_id: PersonId) -> LocalUserInsertForm {
LocalUserInsertForm::builder()
.person_id(person_id)
.password_encrypted(String::new())
.build()
}
async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult<Data> {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = default_person_insert_form(inserted_instance.id, "tegan");
let new_person = PersonInsertForm::test_form(inserted_instance.id, "tegan");
let inserted_person = Person::create(pool, &new_person).await?;
let local_user_form = LocalUserInsertForm {
admin: Some(true),
..default_local_user_form(inserted_person.id)
..LocalUserInsertForm::test_form(inserted_person.id)
};
let inserted_local_user = LocalUser::create(pool, &local_user_form).await?;
let new_bot = PersonInsertForm {
bot_account: Some(true),
..default_person_insert_form(inserted_instance.id, "mybot")
..PersonInsertForm::test_form(inserted_instance.id, "mybot")
};
let inserted_bot = Person::create(pool, &new_bot).await?;
@ -797,12 +800,15 @@ mod tests {
let inserted_community = Community::create(pool, &new_community).await?;
// Test a person block, make sure the post query doesn't include their post
let blocked_person = default_person_insert_form(inserted_instance.id, "john");
let blocked_person = PersonInsertForm::test_form(inserted_instance.id, "john");
let inserted_blocked_person = Person::create(pool, &blocked_person).await?;
let inserted_blocked_local_user =
LocalUser::create(pool, &default_local_user_form(inserted_blocked_person.id)).await?;
let inserted_blocked_local_user = LocalUser::create(
pool,
&LocalUserInsertForm::test_form(inserted_blocked_person.id),
)
.await?;
let post_from_blocked_person = PostInsertForm::builder()
.name(POST_BY_BLOCKED_PERSON.to_string())
@ -1555,6 +1561,7 @@ mod tests {
shared_inbox_url: inserted_community.shared_inbox_url.clone(),
moderators_url: inserted_community.moderators_url.clone(),
featured_url: inserted_community.featured_url.clone(),
visibility: CommunityVisibility::Public,
},
counts: PostAggregates {
post_id: inserted_post.id,
@ -1581,4 +1588,52 @@ mod tests {
creator_blocked: false,
})
}
#[tokio::test]
#[serial]
async fn local_only_instance() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await?;
Community::update(
pool,
data.inserted_community.id,
&CommunityUpdateForm {
visibility: Some(CommunityVisibility::LocalOnly),
..Default::default()
},
)
.await?;
let unauthenticated_query = PostQuery {
..Default::default()
}
.list(pool)
.await?;
assert_eq!(0, unauthenticated_query.len());
let authenticated_query = PostQuery {
local_user: Some(&data.local_user_view),
..Default::default()
}
.list(pool)
.await?;
assert_eq!(2, authenticated_query.len());
let unauthenticated_post = PostView::read(pool, data.inserted_post.id, None, false).await;
assert!(unauthenticated_post.is_err());
let authenticated_post = PostView::read(
pool,
data.inserted_post.id,
Some(data.local_user_view.person.id),
false,
)
.await;
assert!(authenticated_post.is_ok());
cleanup(data, pool).await?;
Ok(())
}
}

@ -12,7 +12,7 @@ use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
aliases,
newtypes::{PersonId, PrivateMessageId},
schema::{person, person_block, private_message},
schema::{instance_block, person, person_block, private_message},
utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
};
use tracing::debug;
@ -34,6 +34,13 @@ fn queries<'a>() -> Queries<
.and(person_block::person_id.eq(aliases::person1.field(person::id))),
),
)
.left_join(
instance_block::table.on(
person::instance_id
.eq(instance_block::instance_id)
.and(instance_block::person_id.eq(aliases::person1.field(person::id))),
),
)
};
let selection = (
@ -55,7 +62,9 @@ fn queries<'a>() -> Queries<
let mut query = all_joins(private_message::table.into_boxed())
.select(selection)
// Dont show replies from blocked users
.filter(person_block::person_id.is_null());
.filter(person_block::person_id.is_null())
// Dont show replies from blocked instances
.filter(instance_block::person_id.is_null());
// If its unread, I only want the ones to me
if options.unread_only {
@ -116,6 +125,8 @@ impl PrivateMessageView {
use diesel::dsl::count;
let conn = &mut get_conn(pool).await?;
private_message::table
// Necessary to get the senders instance_id
.inner_join(person::table.on(private_message::creator_id.eq(person::id)))
.left_join(
person_block::table.on(
private_message::creator_id
@ -123,8 +134,17 @@ impl PrivateMessageView {
.and(person_block::person_id.eq(my_person_id)),
),
)
.left_join(
instance_block::table.on(
person::instance_id
.eq(instance_block::instance_id)
.and(instance_block::person_id.eq(my_person_id)),
),
)
// Dont count replies from blocked users
.filter(person_block::person_id.is_null())
// Dont count replies from blocked instances
.filter(instance_block::person_id.is_null())
.filter(private_message::read.eq(false))
.filter(private_message::recipient_id.eq(my_person_id))
.filter(private_message::deleted.eq(false))
@ -160,24 +180,30 @@ mod tests {
use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView};
use lemmy_db_schema::{
assert_length,
newtypes::InstanceId,
source::{
instance::Instance,
instance_block::{InstanceBlock, InstanceBlockForm},
person::{Person, PersonInsertForm},
person_block::{PersonBlock, PersonBlockForm},
private_message::{PrivateMessage, PrivateMessageInsertForm},
},
traits::{Blockable, Crud},
utils::build_db_pool_for_tests,
utils::{build_db_pool_for_tests, DbPool},
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
struct Data {
instance: Instance,
timmy: Person,
jess: Person,
sara: Person,
}
async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult<Data> {
let message_content = String::new();
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
@ -243,6 +269,32 @@ mod tests {
.await
.unwrap();
Ok(Data {
instance,
timmy,
jess,
sara,
})
}
async fn cleanup(instance_id: InstanceId, pool: &mut DbPool<'_>) -> LemmyResult<()> {
// This also deletes all persons and private messages thanks to sql `on delete cascade`
Instance::delete(pool, instance_id).await.unwrap();
Ok(())
}
#[tokio::test]
#[serial]
async fn read_private_messages() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let Data {
timmy,
jess,
sara,
instance,
} = init_data(pool).await?;
let timmy_messages = PrivateMessageQuery {
unread_only: false,
creator_id: None,
@ -303,6 +355,21 @@ mod tests {
assert_eq!(timmy_sara_unread_messages[0].creator.id, sara.id);
assert_eq!(timmy_sara_unread_messages[0].recipient.id, timmy.id);
cleanup(instance.id, pool).await
}
#[tokio::test]
#[serial]
async fn ensure_person_block() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let Data {
timmy,
sara,
instance,
jess: _,
} = init_data(pool).await?;
// Make sure blocks are working
let timmy_blocks_sara_form = PersonBlockForm {
person_id: timmy.id,
@ -336,7 +403,52 @@ mod tests {
.unwrap();
assert_eq!(timmy_unread_messages, 1);
// This also deletes all persons and private messages thanks to sql `on delete cascade`
Instance::delete(pool, instance.id).await.unwrap();
cleanup(instance.id, pool).await
}
#[tokio::test]
#[serial]
async fn ensure_instance_block() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let Data {
timmy,
jess: _,
sara,
instance,
} = init_data(pool).await?;
// Make sure instance_blocks are working
let timmy_blocks_instance_form = InstanceBlockForm {
person_id: timmy.id,
instance_id: sara.instance_id,
};
let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form)
.await
.unwrap();
let expected_instance_block = InstanceBlock {
person_id: timmy.id,
instance_id: sara.instance_id,
published: inserted_instance_block.published,
};
assert_eq!(expected_instance_block, inserted_instance_block);
let timmy_messages = PrivateMessageQuery {
unread_only: true,
creator_id: None,
..Default::default()
}
.list(pool, timmy.id)
.await
.unwrap();
assert_length!(0, &timmy_messages);
let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id)
.await
.unwrap();
assert_eq!(timmy_unread_messages, 0);
cleanup(instance.id, pool).await
}
}

@ -98,7 +98,7 @@ pub struct PostReportView {
/// currently this is just a wrapper around post id, but should be seen as opaque from the client's perspective
/// stringified since we might want to use arbitrary info later, with a P prepended to prevent ossification
/// (api users love to make assumptions (e.g. parse stuff that looks like numbers as numbers) about apis that aren't part of the spec
#[derive(Serialize, Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct PaginationCursor(pub String);

@ -5,6 +5,7 @@ use lemmy_db_schema::{
newtypes::{CommunityId, PersonId},
schema::{community, community_moderator, person},
utils::{get_conn, DbPool},
CommunityVisibility,
};
impl CommunityModeratorView {
@ -56,17 +57,24 @@ impl CommunityModeratorView {
.await
}
pub async fn for_person(pool: &mut DbPool<'_>, person_id: PersonId) -> Result<Vec<Self>, Error> {
pub async fn for_person(
pool: &mut DbPool<'_>,
person_id: PersonId,
is_authenticated: bool,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
community_moderator::table
let mut query = community_moderator::table
.inner_join(community::table)
.inner_join(person::table)
.filter(community_moderator::person_id.eq(person_id))
.filter(community::deleted.eq(false))
.filter(community::removed.eq(false))
.select((community::all_columns, person::all_columns))
.load::<CommunityModeratorView>(conn)
.await
.into_boxed();
if !is_authenticated {
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
}
query.load::<CommunityModeratorView>(conn).await
}
/// Finds all communities first mods / creators

@ -22,6 +22,7 @@ use lemmy_db_schema::{
},
source::{community::CommunityFollower, local_user::LocalUser, site::Site},
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
CommunityVisibility,
ListingType,
SortType,
};
@ -87,6 +88,11 @@ fn queries<'a>() -> Queries<
query = query.filter(not_removed_or_deleted);
}
// Hide local only communities from unauthenticated users
if my_person_id.is_none() {
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
}
query.first::<CommunityView>(&mut conn).await
};
@ -158,6 +164,8 @@ fn queries<'a>() -> Queries<
if !options.show_nsfw && !has_content_warning {
query = query.filter(community::nsfw.eq(false));
}
// Hide local only communities from unauthenticated users
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
}
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
@ -231,3 +239,129 @@ impl<'a> CommunityQuery<'a> {
queries().list(pool, (self, site)).await
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{community_view::CommunityQuery, structs::CommunityView};
use lemmy_db_schema::{
source::{
community::{Community, CommunityInsertForm, CommunityUpdateForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
},
traits::Crud,
utils::{build_db_pool_for_tests, DbPool},
CommunityVisibility,
};
use serial_test::serial;
struct Data {
inserted_instance: Instance,
local_user: LocalUser,
inserted_community: Community,
}
async fn init_data(pool: &mut DbPool<'_>) -> Data {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let person_name = "tegan".to_string();
let new_person = PersonInsertForm::builder()
.name(person_name.clone())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let local_user_form = LocalUserInsertForm::builder()
.person_id(inserted_person.id)
.password_encrypted(String::new())
.build();
let local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
let new_community = CommunityInsertForm::builder()
.name("test_community_3".to_string())
.title("nada".to_owned())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let inserted_community = Community::create(pool, &new_community).await.unwrap();
Data {
inserted_instance,
local_user,
inserted_community,
}
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
Community::delete(pool, data.inserted_community.id)
.await
.unwrap();
Person::delete(pool, data.local_user.person_id)
.await
.unwrap();
Instance::delete(pool, data.inserted_instance.id)
.await
.unwrap();
}
#[tokio::test]
#[serial]
async fn local_only_community() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
Community::update(
pool,
data.inserted_community.id,
&CommunityUpdateForm {
visibility: Some(CommunityVisibility::LocalOnly),
..Default::default()
},
)
.await
.unwrap();
let unauthenticated_query = CommunityQuery {
..Default::default()
}
.list(pool)
.await
.unwrap();
assert_eq!(0, unauthenticated_query.len());
let authenticated_query = CommunityQuery {
local_user: Some(&data.local_user),
..Default::default()
}
.list(pool)
.await
.unwrap();
assert_eq!(1, authenticated_query.len());
let unauthenticated_community =
CommunityView::read(pool, data.inserted_community.id, None, false).await;
assert!(unauthenticated_community.is_err());
let authenticated_community = CommunityView::read(
pool,
data.inserted_community.id,
Some(data.local_user.person_id),
false,
)
.await;
assert!(authenticated_community.is_ok());
cleanup(data, pool).await;
}
}

@ -7,6 +7,7 @@ use lemmy_db_schema::{
source::{community::Community, person::Person},
traits::ApubActor,
CommentSortType,
CommunityVisibility,
ListingType,
SortType,
};
@ -21,11 +22,17 @@ use lemmy_db_views_actor::{
};
use lemmy_utils::{
cache_header::cache_1hour,
error::LemmyError,
error::{LemmyError, LemmyErrorType},
utils::markdown::{markdown_to_html, sanitize_html},
};
use once_cell::sync::Lazy;
use rss::{extension::dublincore::DublinCoreExtension, Channel, Guid, Item};
use rss::{
extension::{dublincore::DublinCoreExtension, ExtensionBuilder, ExtensionMap},
Channel,
EnclosureBuilder,
Guid,
Item,
};
use serde::Deserialize;
use std::{collections::BTreeMap, str::FromStr};
@ -79,9 +86,30 @@ static RSS_NAMESPACE: Lazy<BTreeMap<String, String>> = Lazy::new(|| {
"dc".to_string(),
rss::extension::dublincore::NAMESPACE.to_string(),
);
h.insert(
"media".to_string(),
"http://search.yahoo.com/mrss/".to_string(),
);
h
});
/// Removes any characters disallowed by the XML grammar.
/// See https://www.w3.org/TR/xml/#NT-Char for details.
fn sanitize_xml(input: String) -> String {
input
.chars()
.filter(|&c| {
matches!(c,
'\u{09}'
| '\u{0A}'
| '\u{0D}'
| '\u{20}'..='\u{D7FF}'
| '\u{E000}'..='\u{FFFD}'
| '\u{10000}'..='\u{10FFFF}')
})
.collect()
}
#[tracing::instrument(skip_all)]
async fn get_all_feed(
info: web::Query<Params>,
@ -246,10 +274,9 @@ async fn get_feed_user(
.await?;
let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?;
let channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - {}", site_view.site.name, person.name),
title: format!("{} - {}", sanitize_xml(site_view.site.name), person.name),
link: person.actor_id.to_string(),
items,
..Default::default()
@ -268,6 +295,9 @@ async fn get_feed_community(
) -> Result<Channel, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let community = Community::read_from_name(&mut context.pool(), community_name, false).await?;
if community.visibility != CommunityVisibility::Public {
return Err(LemmyErrorType::CouldntFindCommunity.into());
}
check_private_instance(&None, &site_view.local_site)?;
@ -285,7 +315,7 @@ async fn get_feed_community(
let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - {}", site_view.site.name, community.name),
title: format!("{} - {}", sanitize_xml(site_view.site.name), community.name),
link: community.actor_id.to_string(),
items,
..Default::default()
@ -324,10 +354,9 @@ async fn get_feed_front(
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
let items = create_post_items(posts, &protocol_and_hostname)?;
let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - Subscribed", site_view.site.name),
title: format!("{} - Subscribed", sanitize_xml(site_view.site.name)),
link: protocol_and_hostname,
items,
..Default::default()
@ -378,7 +407,7 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> Result<Channel, Le
let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - Inbox", site_view.site.name),
title: format!("{} - Inbox", sanitize_xml(site_view.site.name)),
link: format!("{protocol_and_hostname}/inbox"),
items,
..Default::default()
@ -467,7 +496,6 @@ fn create_post_items(
let mut items: Vec<Item> = Vec::new();
for p in posts {
// TODO add images
let post_url = format!("{}/post/{}", protocol_and_hostname, p.post.id);
let community_url = format!(
"{}/c/{}",
@ -492,12 +520,21 @@ fn create_post_items(
p.counts.comments);
// If its a url post, add it to the description
let link = Some(if let Some(url) = p.post.url {
// and see if we can parse it as a media enclosure.
let enclosure_opt = p.post.url.map(|url| {
let link_html = format!("<br><a href=\"{url}\">{url}</a>");
description.push_str(&link_html);
url.to_string()
} else {
post_url.clone()
let mime_type = p
.post
.url_content_type
.unwrap_or_else(|| "application/octet-stream".to_string());
let mut enclosure_bld = EnclosureBuilder::default();
enclosure_bld.url(url.as_str().to_string());
enclosure_bld.mime_type(mime_type);
enclosure_bld.length("0".to_string());
enclosure_bld.build()
});
if let Some(body) = p.post.body {
@ -505,14 +542,34 @@ fn create_post_items(
description.push_str(&html);
}
let mut extensions = ExtensionMap::new();
// If there's a thumbnail URL, add a media:content tag to display it.
// See https://www.rssboard.org/media-rss#media-content for details.
if let Some(url) = p.post.thumbnail_url {
let mut thumbnail_ext = ExtensionBuilder::default();
thumbnail_ext.name("media:content".to_string());
thumbnail_ext.attrs(BTreeMap::from([
("url".to_string(), url.to_string()),
("medium".to_string(), "image".to_string()),
]));
extensions.insert(
"media".to_string(),
BTreeMap::from([("content".to_string(), vec![thumbnail_ext.build()])]),
);
}
let i = Item {
title: Some(sanitize_html(&p.post.name)),
title: Some(sanitize_html(sanitize_xml(p.post.name).as_str())),
pub_date: Some(p.post.published.to_rfc2822()),
comments: Some(post_url.clone()),
guid,
description: Some(description),
description: Some(sanitize_xml(description)),
dublin_core_ext,
link,
link: Some(post_url.clone()),
extensions,
enclosure: enclosure_opt,
..Default::default()
};

@ -7,6 +7,7 @@ use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{community::Community, person::Person},
traits::ApubActor,
CommunityVisibility,
};
use lemmy_utils::{cache_header::cache_3days, error::LemmyError};
use serde::Deserialize;
@ -44,7 +45,14 @@ async fn get_webfinger_response(
let community_id: Option<Url> = Community::read_from_name(&mut context.pool(), name, false)
.await
.ok()
.map(|c| c.actor_id.into());
.and_then(|c| {
if c.visibility == CommunityVisibility::Public {
let id: Url = c.actor_id.into();
Some(id)
} else {
None
}
});
// Mastodon seems to prioritize the last webfinger item in case of duplicates. Put
// community last so that it gets prioritized. For Lemmy the order doesnt matter.

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1.6
ARG RUST_VERSION=1.75
ARG RUST_VERSION=1.76
ARG CARGO_BUILD_FEATURES=default
ARG RUST_RELEASE_MODE=debug

@ -99,7 +99,7 @@ services:
logging: *default-logging
postgres:
image: postgres:15-alpine
image: postgres:16-alpine
# this needs to match the database host in lemmy.hson
# Tune your settings via
# https://pgtune.leopard.in.ua/#/

@ -20,7 +20,7 @@ x-lemmy-default: &lemmy-default
restart: always
x-postgres-default: &postgres-default
image: postgres:15-alpine
image: postgres:16-alpine
environment:
- POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password

@ -21,8 +21,7 @@
pictrs: {
url: "http://pictrs:8080/"
# api_key: "API_KEY"
image_proxy: true
cache_external_link_previews: true
image_mode: None
}
#opentelemetry_url: "http://otel:4137"

@ -0,0 +1,5 @@
ALTER TABLE community
DROP COLUMN visibility;
DROP TYPE community_visibility;

@ -0,0 +1,8 @@
CREATE TYPE community_visibility AS enum (
'Public',
'LocalOnly'
);
ALTER TABLE community
ADD COLUMN visibility community_visibility NOT NULL DEFAULT 'Public';

@ -0,0 +1,48 @@
-- Automatically resolve all reports for a given post once it is marked as removed
CREATE OR REPLACE FUNCTION post_removed_resolve_reports ()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE
post_report
SET
resolved = TRUE,
resolver_id = NEW.mod_person_id,
updated = now()
WHERE
post_report.post_id = NEW.post_id;
RETURN NULL;
END
$$;
CREATE OR REPLACE TRIGGER post_removed_resolve_reports
AFTER INSERT ON mod_remove_post
FOR EACH ROW
WHEN (NEW.removed)
EXECUTE PROCEDURE post_removed_resolve_reports ();
-- Same when comment is marked as removed
CREATE OR REPLACE FUNCTION comment_removed_resolve_reports ()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE
comment_report
SET
resolved = TRUE,
resolver_id = NEW.mod_person_id,
updated = now()
WHERE
comment_report.comment_id = NEW.comment_id;
RETURN NULL;
END
$$;
CREATE OR REPLACE TRIGGER comment_removed_resolve_reports
AFTER INSERT ON mod_remove_comment
FOR EACH ROW
WHEN (NEW.removed)
EXECUTE PROCEDURE comment_removed_resolve_reports ();

@ -0,0 +1,8 @@
DROP TRIGGER IF EXISTS post_removed_resolve_reports ON mod_remove_post;
DROP FUNCTION IF EXISTS post_removed_resolve_reports;
DROP TRIGGER IF EXISTS comment_removed_resolve_reports ON mod_remove_comment;
DROP FUNCTION IF EXISTS comment_removed_resolve_reports;

@ -52,7 +52,7 @@ ask_to_auto_reload() {
done
if [ "$auto_reload_final" = 1 ]
then
cd ui && yarn start
cd ui && pnpm dev
cd server && cargo watch -x run
fi
}
@ -62,8 +62,9 @@ ask_to_init_db
# Build the web client
cd ui
yarn
yarn build
pnpm i
pnpm prebuild:prod
pnpm build:prod
# Build and run the backend
cd ../server

@ -5,8 +5,6 @@ CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
cd $CWD/../
cargo clippy --workspace --fix --allow-staged --allow-dirty --tests --all-targets --all-features -- -D warnings
# Format rust files
cargo +nightly fmt
@ -15,3 +13,5 @@ taplo format
# Format sql files
find migrations -type f -name '*.sql' -exec pg_format -i {} +
cargo clippy --workspace --fix --allow-staged --allow-dirty --tests --all-targets --all-features -- -D warnings

@ -0,0 +1,42 @@
#!/bin/sh
set -e
echo "Do not stop in the middle of this upgrade, wait until you see the message: Upgrade complete."
echo "Stopping lemmy and all services..."
sudo docker-compose stop
echo "Make sure postgres is started..."
sudo docker-compose up -d postgres
echo "Waiting..."
sleep 20s
echo "Exporting the Database to 15_16.dump.sql ..."
sudo docker-compose exec -T postgres pg_dumpall -c -U lemmy > 15_16_dump.sql
echo "Done."
echo "Stopping postgres..."
sudo docker-compose stop postgres
echo "Waiting..."
sleep 20s
echo "Removing the old postgres folder"
sudo rm -rf volumes/postgres
echo "Updating docker-compose to use postgres version 16."
sed -i "s/image: postgres:.*/image: postgres:16-alpine/" ./docker-compose.yml
echo "Starting up new postgres..."
sudo docker-compose up -d postgres
echo "Waiting..."
sleep 20s
echo "Importing the database...."
cat 15_16_dump.sql | sudo docker-compose exec -T postgres psql -U lemmy
echo "Done."
echo "Starting up lemmy..."
sudo docker-compose up -d
echo "A copy of your old database is at 15_16.dump.sql . You can delete this file if the upgrade went smoothly."
echo "Upgrade complete."

@ -26,8 +26,6 @@ if [ ! -z "${third_semver##*[!0-9]*}" ]; then
echo $new_tag > "VERSION"
git add "VERSION"
git commit -m"Updating VERSION"
git tag $new_tag
git push origin $new_tag
git push
popd
fi

@ -22,7 +22,10 @@ use lemmy_db_schema::{
received_activity,
sent_activity,
},
source::instance::{Instance, InstanceForm},
source::{
instance::{Instance, InstanceForm},
local_user::LocalUser,
},
utils::{get_conn, naive_now, now, DbPool, DELETED_REPLACEMENT_TEXT},
};
use lemmy_routes::nodeinfo::NodeInfo;
@ -79,24 +82,19 @@ pub async fn setup(context: LemmyContext) -> Result<(), LemmyError> {
});
let context_1 = context.clone();
// Overwrite deleted & removed posts and comments every day
// Daily tasks:
// - Overwrite deleted & removed posts and comments every day
// - Delete old denied users
// - Update instance software
scheduler.every(CTimeUnits::days(1)).run(move || {
let context = context_1.clone();
async move {
overwrite_deleted_posts_and_comments(&mut context.pool()).await;
}
});
let context_1 = context.clone();
// Update the Instance Software
scheduler.every(CTimeUnits::days(1)).run(move || {
let context = context_1.clone();
async move {
delete_old_denied_users(&mut context.pool()).await;
update_instance_software(&mut context.pool(), context.client())
.await
.map_err(|e| warn!("Failed to update instance software: {e}"))
.inspect_err(|e| warn!("Failed to update instance software: {e}"))
.ok();
}
});
@ -115,6 +113,7 @@ async fn startup_jobs(pool: &mut DbPool<'_>) {
update_banned_when_expired(pool).await;
clear_old_activities(pool).await;
overwrite_deleted_posts_and_comments(pool).await;
delete_old_denied_users(pool).await;
}
/// Update the hot_rank columns for the aggregates tables
@ -277,10 +276,10 @@ async fn delete_expired_captcha_answers(pool: &mut DbPool<'_>) {
)
.execute(&mut conn)
.await
.map(|_| {
.inspect(|_| {
info!("Done.");
})
.map_err(|e| error!("Failed to clear old captcha answers: {e}"))
.inspect_err(|e| error!("Failed to clear old captcha answers: {e}"))
.ok();
}
Err(e) => {
@ -301,7 +300,7 @@ async fn clear_old_activities(pool: &mut DbPool<'_>) {
)
.execute(&mut conn)
.await
.map_err(|e| error!("Failed to clear old sent activities: {e}"))
.inspect_err(|e| error!("Failed to clear old sent activities: {e}"))
.ok();
diesel::delete(
@ -310,8 +309,8 @@ async fn clear_old_activities(pool: &mut DbPool<'_>) {
)
.execute(&mut conn)
.await
.map(|_| info!("Done."))
.map_err(|e| error!("Failed to clear old received activities: {e}"))
.inspect(|_| info!("Done."))
.inspect_err(|e| error!("Failed to clear old received activities: {e}"))
.ok();
}
Err(e) => {
@ -320,6 +319,16 @@ async fn clear_old_activities(pool: &mut DbPool<'_>) {
}
}
async fn delete_old_denied_users(pool: &mut DbPool<'_>) {
LocalUser::delete_old_denied_local_users(pool)
.await
.inspect(|_| {
info!("Done.");
})
.inspect(|e| error!("Failed to deleted old denied users: {e}"))
.ok();
}
/// overwrite posts and comments 30d after deletion
async fn overwrite_deleted_posts_and_comments(pool: &mut DbPool<'_>) {
info!("Overwriting deleted posts...");
@ -339,10 +348,10 @@ async fn overwrite_deleted_posts_and_comments(pool: &mut DbPool<'_>) {
))
.execute(&mut conn)
.await
.map(|_| {
.inspect(|_| {
info!("Done.");
})
.map_err(|e| error!("Failed to overwrite deleted posts: {e}"))
.inspect_err(|e| error!("Failed to overwrite deleted posts: {e}"))
.ok();
info!("Overwriting deleted comments...");
@ -355,10 +364,10 @@ async fn overwrite_deleted_posts_and_comments(pool: &mut DbPool<'_>) {
.set(comment::content.eq(DELETED_REPLACEMENT_TEXT))
.execute(&mut conn)
.await
.map(|_| {
.inspect(|_| {
info!("Done.");
})
.map_err(|e| error!("Failed to overwrite deleted comments: {e}"))
.inspect_err(|e| error!("Failed to overwrite deleted comments: {e}"))
.ok();
}
Err(e) => {
@ -390,14 +399,14 @@ async fn active_counts(pool: &mut DbPool<'_>) {
sql_query(update_site_stmt)
.execute(&mut conn)
.await
.map_err(|e| error!("Failed to update site stats: {e}"))
.inspect_err(|e| error!("Failed to update site stats: {e}"))
.ok();
let update_community_stmt = format!("update community_aggregates ca set users_active_{} = mv.count_ from community_aggregates_activity('{}') mv where ca.community_id = mv.community_id_", i.1, i.0);
sql_query(update_community_stmt)
.execute(&mut conn)
.await
.map_err(|e| error!("Failed to update community stats: {e}"))
.inspect_err(|e| error!("Failed to update community stats: {e}"))
.ok();
}
@ -424,7 +433,7 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) {
.set(person::banned.eq(false))
.execute(&mut conn)
.await
.map_err(|e| error!("Failed to update person.banned when expires: {e}"))
.inspect_err(|e| error!("Failed to update person.banned when expires: {e}"))
.ok();
diesel::delete(
@ -432,7 +441,7 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) {
)
.execute(&mut conn)
.await
.map_err(|e| error!("Failed to remove community_ban expired rows: {e}"))
.inspect_err(|e| error!("Failed to remove community_ban expired rows: {e}"))
.ok();
}
Err(e) => {

Loading…
Cancel
Save