mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-12 05:54:51 +00:00
Merge remote-tracking branch 'upstream/main' into migration-runner
This commit is contained in:
commit
4428c61bea
|
@ -195,6 +195,7 @@ steps:
|
|||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
RUST_BACKTRACE: "1"
|
||||
CARGO_HOME: .cargo_home
|
||||
LEMMY_TEST_FAST_FEDERATION: "1"
|
||||
commands:
|
||||
# Install pg_dump for the schema setup test (must match server version)
|
||||
- apt update && apt install -y lsb-release
|
||||
|
@ -288,7 +289,7 @@ steps:
|
|||
|
||||
services:
|
||||
database:
|
||||
image: postgres:16-alpine
|
||||
image: pgautoupgrade/pgautoupgrade:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: lemmy
|
||||
POSTGRES_USER: postgres
|
||||
|
|
1313
Cargo.lock
generated
1313
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
13
Cargo.toml
13
Cargo.toml
|
@ -99,7 +99,7 @@ lemmy_db_views = { version = "=0.19.5", path = "./crates/db_views" }
|
|||
lemmy_db_views_actor = { version = "=0.19.5", path = "./crates/db_views_actor" }
|
||||
lemmy_db_views_moderator = { version = "=0.19.5", path = "./crates/db_views_moderator" }
|
||||
lemmy_federate = { version = "=0.19.5", path = "./crates/federate" }
|
||||
activitypub_federation = { version = "0.5.6", default-features = false, features = [
|
||||
activitypub_federation = { version = "0.5.8", default-features = false, features = [
|
||||
"actix-web",
|
||||
] }
|
||||
diesel = "2.1.6"
|
||||
|
@ -121,7 +121,12 @@ tracing-error = "0.2.0"
|
|||
tracing-log = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] }
|
||||
reqwest = { version = "0.11.27", default-features = false, features = [
|
||||
"json",
|
||||
"blocking",
|
||||
"gzip",
|
||||
"rustls-tls",
|
||||
] }
|
||||
reqwest-middleware = "0.2.5"
|
||||
reqwest-tracing = "0.4.8"
|
||||
clokwerk = "0.4.0"
|
||||
|
@ -144,8 +149,7 @@ regex = "1.10.4"
|
|||
once_cell = "1.19.0"
|
||||
diesel-derive-newtype = "2.1.2"
|
||||
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
||||
strum = "0.26.2"
|
||||
strum_macros = "0.26.4"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
itertools = "0.13.0"
|
||||
futures = "0.3.30"
|
||||
http = "0.2.12"
|
||||
|
@ -198,6 +202,7 @@ opentelemetry = { workspace = true, optional = true }
|
|||
console-subscriber = { version = "0.3.0", optional = true }
|
||||
opentelemetry-otlp = { version = "0.12.0", optional = true }
|
||||
pict-rs = { version = "0.5.15", optional = true }
|
||||
rustls = { workspace = true }
|
||||
tokio.workspace = true
|
||||
actix-cors = "0.7.0"
|
||||
futures-util = { workspace = true }
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
betaUrl,
|
||||
registerUser,
|
||||
unfollows,
|
||||
delay,
|
||||
} from "./shared";
|
||||
|
||||
beforeAll(setupLogins);
|
||||
|
@ -21,39 +22,48 @@ test("Follow local community", async () => {
|
|||
let user = await registerUser(beta, betaUrl);
|
||||
|
||||
let community = (await resolveBetaCommunity(user)).community!;
|
||||
expect(community.counts.subscribers).toBe(1);
|
||||
expect(community.counts.subscribers_local).toBe(1);
|
||||
let follow = await followCommunity(user, true, community.community.id);
|
||||
|
||||
// Make sure the follow response went through
|
||||
expect(follow.community_view.community.local).toBe(true);
|
||||
expect(follow.community_view.subscribed).toBe("Subscribed");
|
||||
expect(follow.community_view.counts.subscribers).toBe(2);
|
||||
expect(follow.community_view.counts.subscribers_local).toBe(2);
|
||||
expect(follow.community_view.counts.subscribers).toBe(
|
||||
community.counts.subscribers + 1,
|
||||
);
|
||||
expect(follow.community_view.counts.subscribers_local).toBe(
|
||||
community.counts.subscribers_local + 1,
|
||||
);
|
||||
|
||||
// Test an unfollow
|
||||
let unfollow = await followCommunity(user, false, community.community.id);
|
||||
expect(unfollow.community_view.subscribed).toBe("NotSubscribed");
|
||||
expect(unfollow.community_view.counts.subscribers).toBe(1);
|
||||
expect(unfollow.community_view.counts.subscribers_local).toBe(1);
|
||||
expect(unfollow.community_view.counts.subscribers).toBe(
|
||||
community.counts.subscribers,
|
||||
);
|
||||
expect(unfollow.community_view.counts.subscribers_local).toBe(
|
||||
community.counts.subscribers_local,
|
||||
);
|
||||
});
|
||||
|
||||
test("Follow federated community", async () => {
|
||||
// It takes about 1 second for the community aggregates to federate
|
||||
let betaCommunity = (
|
||||
await delay(2000); // if this is the second test run, we don't have a way to wait for the correct number of subscribers
|
||||
const betaCommunityInitial = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c =>
|
||||
c.community?.counts.subscribers === 1 &&
|
||||
c.community.counts.subscribers_local === 0,
|
||||
c => !!c.community && c.community?.counts.subscribers >= 1,
|
||||
)
|
||||
).community;
|
||||
if (!betaCommunity) {
|
||||
if (!betaCommunityInitial) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
let follow = await followCommunity(alpha, true, betaCommunity.community.id);
|
||||
let follow = await followCommunity(
|
||||
alpha,
|
||||
true,
|
||||
betaCommunityInitial.community.id,
|
||||
);
|
||||
expect(follow.community_view.subscribed).toBe("Pending");
|
||||
betaCommunity = (
|
||||
const betaCommunity = (
|
||||
await waitUntil(
|
||||
() => resolveBetaCommunity(alpha),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
|
@ -64,20 +74,24 @@ test("Follow federated community", async () => {
|
|||
expect(betaCommunity?.community.local).toBe(false);
|
||||
expect(betaCommunity?.community.name).toBe("main");
|
||||
expect(betaCommunity?.subscribed).toBe("Subscribed");
|
||||
expect(betaCommunity?.counts.subscribers_local).toBe(1);
|
||||
expect(betaCommunity?.counts.subscribers_local).toBe(
|
||||
betaCommunityInitial.counts.subscribers_local + 1,
|
||||
);
|
||||
|
||||
// check that unfollow was federated
|
||||
let communityOnBeta1 = await resolveBetaCommunity(beta);
|
||||
expect(communityOnBeta1.community?.counts.subscribers).toBe(2);
|
||||
expect(communityOnBeta1.community?.counts.subscribers_local).toBe(1);
|
||||
expect(communityOnBeta1.community?.counts.subscribers).toBe(
|
||||
betaCommunityInitial.counts.subscribers + 1,
|
||||
);
|
||||
|
||||
// Check it from local
|
||||
let site = await getSite(alpha);
|
||||
let remoteCommunityId = site.my_user?.follows.find(
|
||||
c => c.community.local == false,
|
||||
c =>
|
||||
c.community.local == false &&
|
||||
c.community.id === betaCommunityInitial.community.id,
|
||||
)?.community.id;
|
||||
expect(remoteCommunityId).toBeDefined();
|
||||
expect(site.my_user?.follows.length).toBe(2);
|
||||
|
||||
if (!remoteCommunityId) {
|
||||
throw "Missing remote community id";
|
||||
|
@ -89,10 +103,21 @@ test("Follow federated community", async () => {
|
|||
|
||||
// Make sure you are unsubbed locally
|
||||
let siteUnfollowCheck = await getSite(alpha);
|
||||
expect(siteUnfollowCheck.my_user?.follows.length).toBe(1);
|
||||
expect(
|
||||
siteUnfollowCheck.my_user?.follows.find(
|
||||
c => c.community.id === betaCommunityInitial.community.id,
|
||||
),
|
||||
).toBe(undefined);
|
||||
|
||||
// check that unfollow was federated
|
||||
let communityOnBeta2 = await resolveBetaCommunity(beta);
|
||||
expect(communityOnBeta2.community?.counts.subscribers).toBe(1);
|
||||
let communityOnBeta2 = await waitUntil(
|
||||
() => resolveBetaCommunity(beta),
|
||||
c =>
|
||||
c.community?.counts.subscribers ===
|
||||
betaCommunityInitial.counts.subscribers,
|
||||
);
|
||||
expect(communityOnBeta2.community?.counts.subscribers).toBe(
|
||||
betaCommunityInitial.counts.subscribers,
|
||||
);
|
||||
expect(communityOnBeta2.community?.counts.subscribers_local).toBe(1);
|
||||
});
|
||||
|
|
|
@ -52,17 +52,23 @@ beforeAll(async () => {
|
|||
|
||||
afterAll(unfollows);
|
||||
|
||||
async function assertPostFederation(postOne: PostView, postTwo: PostView) {
|
||||
async function assertPostFederation(
|
||||
postOne: PostView,
|
||||
postTwo: PostView,
|
||||
waitForMeta = true,
|
||||
) {
|
||||
// Link metadata is generated in background task and may not be ready yet at this time,
|
||||
// so wait for it explicitly. For removed posts we cant refetch anything.
|
||||
postOne = await waitForPost(beta, postOne.post, res => {
|
||||
return res === null || res?.post.embed_title !== null;
|
||||
});
|
||||
postTwo = await waitForPost(
|
||||
beta,
|
||||
postTwo.post,
|
||||
res => res === null || res?.post.embed_title !== null,
|
||||
);
|
||||
if (waitForMeta) {
|
||||
postOne = await waitForPost(beta, postOne.post, res => {
|
||||
return res === null || !!res?.post.embed_title;
|
||||
});
|
||||
postTwo = await waitForPost(
|
||||
beta,
|
||||
postTwo.post,
|
||||
res => res === null || !!res?.post.embed_title,
|
||||
);
|
||||
}
|
||||
|
||||
expect(postOne?.post.ap_id).toBe(postTwo?.post.ap_id);
|
||||
expect(postOne?.post.name).toBe(postTwo?.post.name);
|
||||
|
@ -408,7 +414,11 @@ test("Remove a post from admin and community on same instance", async () => {
|
|||
p => p?.post_view.post.removed ?? false,
|
||||
);
|
||||
expect(alphaPost?.post_view.post.removed).toBe(true);
|
||||
await assertPostFederation(alphaPost.post_view, removePostRes.post_view);
|
||||
await assertPostFederation(
|
||||
alphaPost.post_view,
|
||||
removePostRes.post_view,
|
||||
false,
|
||||
);
|
||||
|
||||
// Undelete
|
||||
let undeletedPost = await removePost(beta, false, betaPost.post);
|
||||
|
|
|
@ -131,7 +131,11 @@ test("Requests with invalid auth should be treated as unauthenticated", async ()
|
|||
});
|
||||
|
||||
test("Create user with Arabic name", async () => {
|
||||
let user = await registerUser(alpha, alphaUrl, "تجريب");
|
||||
let user = await registerUser(
|
||||
alpha,
|
||||
alphaUrl,
|
||||
"تجريب" + Math.random().toString().slice(2, 10), // less than actor_name_max_length
|
||||
);
|
||||
|
||||
let site = await getSite(user);
|
||||
expect(site.my_user).toBeDefined();
|
||||
|
|
|
@ -108,10 +108,12 @@
|
|||
port: 8536
|
||||
# Whether the site is available over TLS. Needs to be true for federation to work.
|
||||
tls_enabled: true
|
||||
# The number of activitypub federation workers that can be in-flight concurrently
|
||||
worker_count: 0
|
||||
# The number of activitypub federation retry workers that can be in-flight concurrently
|
||||
retry_count: 0
|
||||
federation: {
|
||||
# Limit to the number of concurrent outgoing federation requests per target instance.
|
||||
# Set this to a higher value than 1 (e.g. 6) only if you have a huge instance (>10 activities
|
||||
# per second) and if a receiving instance is not keeping up.
|
||||
concurrent_sends_per_instance: 1
|
||||
}
|
||||
prometheus: {
|
||||
bind: "127.0.0.1"
|
||||
port: 10002
|
||||
|
|
|
@ -17,9 +17,13 @@ pub async fn distinguish_comment(
|
|||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
let orig_comment = CommentView::read(
|
||||
&mut context.pool(),
|
||||
data.comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
|
@ -54,7 +58,7 @@ pub async fn distinguish_comment(
|
|||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
data.comment_id,
|
||||
Some(local_user_view.person.id),
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
|
|
@ -35,9 +35,13 @@ pub async fn like_comment(
|
|||
check_bot_account(&local_user_view.person)?;
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
let orig_comment = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
|
|
|
@ -17,7 +17,7 @@ pub async fn list_comment_likes(
|
|||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
data.comment_id,
|
||||
Some(local_user_view.person.id),
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
|
|
@ -32,10 +32,13 @@ pub async fn save_comment(
|
|||
}
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id))
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
Ok(Json(CommentResponse {
|
||||
comment_view,
|
||||
|
|
|
@ -35,9 +35,13 @@ pub async fn create_comment_report(
|
|||
|
||||
let person_id = local_user_view.person.id;
|
||||
let comment_id = data.comment_id;
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_api_common::{
|
|||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::{Community, CommunityModerator, CommunityModeratorForm},
|
||||
local_user::LocalUser,
|
||||
moderator::{ModAddCommunity, ModAddCommunityForm},
|
||||
},
|
||||
traits::{Crud, Joinable},
|
||||
|
@ -33,6 +34,18 @@ pub async fn add_mod_to_community(
|
|||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// If its a mod removal, also check that you're a higher mod.
|
||||
if !data.added {
|
||||
LocalUser::is_higher_mod_or_admin_check(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let community = Community::read(&mut context.pool(), community_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
|
|
@ -14,6 +14,7 @@ use lemmy_db_schema::{
|
|||
CommunityPersonBan,
|
||||
CommunityPersonBanForm,
|
||||
},
|
||||
local_user::LocalUser,
|
||||
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
|
||||
},
|
||||
traits::{Bannable, Crud, Followable},
|
||||
|
@ -44,6 +45,14 @@ pub async fn ban_from_community(
|
|||
)
|
||||
.await?;
|
||||
|
||||
LocalUser::is_higher_mod_or_admin_check(
|
||||
&mut context.pool(),
|
||||
data.community_id,
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(reason) = &data.reason {
|
||||
is_valid_body_field(reason, false)?;
|
||||
}
|
||||
|
|
|
@ -50,10 +50,14 @@ pub async fn block_community(
|
|||
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
|
||||
}
|
||||
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
SendActivityData::FollowCommunity(
|
||||
|
|
|
@ -62,11 +62,14 @@ pub async fn follow_community(
|
|||
}
|
||||
|
||||
let community_id = data.community_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
|
||||
|
||||
|
|
|
@ -76,11 +76,14 @@ pub async fn transfer_community(
|
|||
ModTransferCommunity::create(&mut context.pool(), &form).await?;
|
||||
|
||||
let community_id = data.community_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let community_view =
|
||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
let community_id = data.community_id;
|
||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)
|
||||
|
|
|
@ -24,6 +24,16 @@ pub async fn add_admin(
|
|||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
// If its an admin removal, also check that you're a higher admin
|
||||
if !data.added {
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Make sure that the person_id added is local
|
||||
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
|
||||
.await?
|
||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_user::LocalUser,
|
||||
login_token::LoginToken,
|
||||
moderator::{ModBan, ModBanForm},
|
||||
person::{Person, PersonUpdateForm},
|
||||
|
@ -31,6 +32,14 @@ pub async fn ban_from_site(
|
|||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
// Also make sure you're a higher admin than the target
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(reason) = &data.reason {
|
||||
is_valid_body_field(reason, false)?;
|
||||
}
|
||||
|
|
|
@ -72,11 +72,5 @@ pub async fn feature_post(
|
|||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
&context,
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
||||
}
|
||||
|
|
|
@ -85,11 +85,5 @@ pub async fn like_post(
|
|||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
context.deref(),
|
||||
post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
build_post_response(context.deref(), post.community_id, local_user_view, post_id).await
|
||||
}
|
||||
|
|
|
@ -63,11 +63,5 @@ pub async fn lock_post(
|
|||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
&context,
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
||||
}
|
||||
|
|
|
@ -34,9 +34,14 @@ pub async fn save_post(
|
|||
|
||||
let post_id = data.post_id;
|
||||
let person_id = local_user_view.person.id;
|
||||
let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
let post_view = PostView::read(
|
||||
&mut context.pool(),
|
||||
post_id,
|
||||
Some(&local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use lemmy_api_common::{
|
|||
use lemmy_db_schema::{
|
||||
source::{
|
||||
comment::Comment,
|
||||
local_user::LocalUser,
|
||||
moderator::{AdminPurgeComment, AdminPurgeCommentForm},
|
||||
},
|
||||
traits::Crud,
|
||||
|
@ -29,9 +30,21 @@ pub async fn purge_comment(
|
|||
let comment_id = data.comment_id;
|
||||
|
||||
// Read the comment to get the post_id and community
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
let comment_view = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
// Also check that you're a higher admin
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![comment_view.creator.id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let post_id = comment_view.comment.post_id;
|
||||
|
||||
|
|
|
@ -9,13 +9,16 @@ use lemmy_api_common::{
|
|||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::PersonId,
|
||||
source::{
|
||||
community::Community,
|
||||
local_user::LocalUser,
|
||||
moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm},
|
||||
},
|
||||
traits::Crud,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
|
@ -32,6 +35,21 @@ pub async fn purge_community(
|
|||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
// Also check that you're a higher admin than all the mods
|
||||
let community_mod_person_ids =
|
||||
CommunityModeratorView::for_community(&mut context.pool(), community.id)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|cmv| cmv.moderator.id)
|
||||
.collect::<Vec<PersonId>>();
|
||||
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
community_mod_person_ids,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(banner) = &community.banner {
|
||||
purge_image_from_pictrs(banner, &context).await.ok();
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_user::LocalUser,
|
||||
moderator::{AdminPurgePerson, AdminPurgePersonForm},
|
||||
person::{Person, PersonUpdateForm},
|
||||
},
|
||||
|
@ -27,9 +28,18 @@ pub async fn purge_person(
|
|||
// Only let admin purge an item
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
// Also check that you're a higher admin
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![data.person_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let person = Person::read(&mut context.pool(), data.person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||
|
||||
ban_nonlocal_user_from_local_communities(
|
||||
&local_user_view,
|
||||
&person,
|
||||
|
|
|
@ -10,6 +10,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_user::LocalUser,
|
||||
moderator::{AdminPurgePost, AdminPurgePostForm},
|
||||
post::Post,
|
||||
},
|
||||
|
@ -32,6 +33,14 @@ pub async fn purge_post(
|
|||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
// Also check that you're a higher admin
|
||||
LocalUser::is_higher_admin_check(
|
||||
&mut context.pool(),
|
||||
local_user_view.person.id,
|
||||
vec![post.creator_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Purge image
|
||||
if let Some(url) = &post.url {
|
||||
purge_image_from_pictrs(url, &context).await.ok();
|
||||
|
|
28
crates/api/src/site/registration_applications/get.rs
Normal file
28
crates/api/src/site/registration_applications/get.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use actix_web::web::{Data, Json, Query};
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
site::{GetRegistrationApplication, RegistrationApplicationResponse},
|
||||
utils::is_admin,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
||||
/// Lists registration applications, filterable by undenied only.
|
||||
pub async fn get_registration_application(
|
||||
data: Query<GetRegistrationApplication>,
|
||||
context: Data<LemmyContext>,
|
||||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<RegistrationApplicationResponse>> {
|
||||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
// Read the view
|
||||
let registration_application =
|
||||
RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
|
||||
|
||||
Ok(Json(RegistrationApplicationResponse {
|
||||
registration_application,
|
||||
}))
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod approve;
|
||||
pub mod get;
|
||||
pub mod list;
|
||||
pub mod unread_count;
|
||||
|
|
|
@ -36,8 +36,8 @@ pub async fn build_comment_response(
|
|||
local_user_view: Option<LocalUserView>,
|
||||
recipient_ids: Vec<LocalUserId>,
|
||||
) -> LemmyResult<CommentResponse> {
|
||||
let person_id = local_user_view.map(|l| l.person.id);
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id)
|
||||
let local_user = local_user_view.map(|l| l.local_user);
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, local_user.as_ref())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
Ok(CommentResponse {
|
||||
|
@ -54,11 +54,11 @@ pub async fn build_community_response(
|
|||
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id)
|
||||
.await
|
||||
.is_ok();
|
||||
let person_id = local_user_view.person.id;
|
||||
let local_user = local_user_view.local_user;
|
||||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
Some(person_id),
|
||||
Some(&local_user),
|
||||
is_mod_or_admin,
|
||||
)
|
||||
.await?
|
||||
|
@ -74,16 +74,17 @@ pub async fn build_community_response(
|
|||
pub async fn build_post_response(
|
||||
context: &LemmyContext,
|
||||
community_id: CommunityId,
|
||||
person: &Person,
|
||||
local_user_view: LocalUserView,
|
||||
post_id: PostId,
|
||||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), person, community_id)
|
||||
let local_user = local_user_view.local_user;
|
||||
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id)
|
||||
.await
|
||||
.is_ok();
|
||||
let post_view = PostView::read(
|
||||
&mut context.pool(),
|
||||
post_id,
|
||||
Some(person.id),
|
||||
Some(&local_user),
|
||||
is_mod_or_admin,
|
||||
)
|
||||
.await?
|
||||
|
@ -103,6 +104,7 @@ pub async fn send_local_notifs(
|
|||
let mut recipient_ids = Vec::new();
|
||||
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
|
||||
|
||||
// let person = my_local_user.person;
|
||||
// Read the comment view to get extra info
|
||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
|
|
|
@ -116,10 +116,7 @@ mod tests {
|
|||
|
||||
let inserted_person = Person::create(pool, &new_person).await.unwrap();
|
||||
|
||||
let local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_person.id)
|
||||
.password_encrypted("123456".to_string())
|
||||
.build();
|
||||
let local_user_form = LocalUserInsertForm::test_form(inserted_person.id);
|
||||
|
||||
let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![])
|
||||
.await
|
||||
|
|
|
@ -55,7 +55,7 @@ impl LemmyContext {
|
|||
/// Initialize a context for use in tests which blocks federation network calls.
|
||||
///
|
||||
/// Do not use this in production code.
|
||||
pub async fn init_test_context() -> Data<LemmyContext> {
|
||||
pub async fn init_test_federation_config() -> FederationConfig<LemmyContext> {
|
||||
// call this to run migrations
|
||||
let pool = build_db_pool_for_tests().await;
|
||||
|
||||
|
@ -70,14 +70,19 @@ impl LemmyContext {
|
|||
let rate_limit_cell = RateLimitCell::with_test_config();
|
||||
|
||||
let context = LemmyContext::create(pool, client, secret, rate_limit_cell.clone());
|
||||
let config = FederationConfig::builder()
|
||||
|
||||
FederationConfig::builder()
|
||||
.domain(context.settings().hostname.clone())
|
||||
.app_data(context)
|
||||
.debug(true)
|
||||
// Dont allow any network fetches
|
||||
.http_fetch_limit(0)
|
||||
.build()
|
||||
.await
|
||||
.expect("build federation config");
|
||||
.expect("build federation config")
|
||||
}
|
||||
pub async fn init_test_context() -> Data<LemmyContext> {
|
||||
let config = Self::init_test_federation_config().await;
|
||||
config.to_request_data()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,6 +81,8 @@ pub struct GetPosts {
|
|||
pub show_hidden: Option<bool>,
|
||||
/// If true, then show the read posts (even if your user setting is to hide them)
|
||||
pub show_read: Option<bool>,
|
||||
/// If true, then show the nsfw posts (even if your user setting is to hide them)
|
||||
pub show_nsfw: Option<bool>,
|
||||
pub page_cursor: Option<PaginationCursor>,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
use crate::federate_retry_sleep_duration;
|
||||
use chrono::{DateTime, Utc};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::{CommentId, CommunityId, InstanceId, LanguageId, PersonId, PostId},
|
||||
newtypes::{
|
||||
CommentId,
|
||||
CommunityId,
|
||||
InstanceId,
|
||||
LanguageId,
|
||||
PersonId,
|
||||
PostId,
|
||||
RegistrationApplicationId,
|
||||
},
|
||||
source::{
|
||||
federation_queue_state::FederationQueueState,
|
||||
instance::Instance,
|
||||
|
@ -440,13 +448,22 @@ pub struct ListRegistrationApplicationsResponse {
|
|||
pub registration_applications: Vec<RegistrationApplicationView>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Gets a registration application for a person
|
||||
pub struct GetRegistrationApplication {
|
||||
pub person_id: PersonId,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[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.
|
||||
pub struct ApproveRegistrationApplication {
|
||||
pub id: i32,
|
||||
pub id: RegistrationApplicationId,
|
||||
pub approve: bool,
|
||||
pub deny_reason: Option<String>,
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ pub async fn create_comment(
|
|||
let post_view = PostView::read(
|
||||
&mut context.pool(),
|
||||
post_id,
|
||||
Some(local_user_view.person.id),
|
||||
Some(&local_user_view.local_user),
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
|
|
|
@ -21,9 +21,13 @@ pub async fn delete_comment(
|
|||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let comment_id = data.comment_id;
|
||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
let orig_comment = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
// Dont delete it if its already been deleted.
|
||||
if orig_comment.comment.deleted == data.deleted {
|
||||
|
|
|
@ -11,6 +11,7 @@ use lemmy_db_schema::{
|
|||
source::{
|
||||
comment::{Comment, CommentUpdateForm},
|
||||
comment_report::CommentReport,
|
||||
local_user::LocalUser,
|
||||
moderator::{ModRemoveComment, ModRemoveCommentForm},
|
||||
},
|
||||
traits::{Crud, Reportable},
|
||||
|
@ -25,9 +26,13 @@ pub async fn remove_comment(
|
|||
local_user_view: LocalUserView,
|
||||
) -> LemmyResult<Json<CommentResponse>> {
|
||||
let comment_id = data.comment_id;
|
||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
let orig_comment = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
check_community_mod_action(
|
||||
&local_user_view.person,
|
||||
|
@ -37,6 +42,14 @@ pub async fn remove_comment(
|
|||
)
|
||||
.await?;
|
||||
|
||||
LocalUser::is_higher_mod_or_admin_check(
|
||||
&mut context.pool(),
|
||||
orig_comment.community.id,
|
||||
local_user_view.person.id,
|
||||
vec![orig_comment.creator.id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Don't allow removing or restoring comment which was deleted by user, as it would reveal
|
||||
// the comment text in mod log.
|
||||
if orig_comment.comment.deleted {
|
||||
|
@ -68,14 +81,8 @@ pub async fn remove_comment(
|
|||
};
|
||||
ModRemoveComment::create(&mut context.pool(), &form).await?;
|
||||
|
||||
let recipient_ids = send_local_notifs(
|
||||
vec![],
|
||||
comment_id,
|
||||
&local_user_view.person.clone(),
|
||||
false,
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
let recipient_ids =
|
||||
send_local_notifs(vec![], comment_id, &local_user_view.person, false, &context).await?;
|
||||
let updated_comment_id = updated_comment.id;
|
||||
|
||||
ActivityChannel::submit_activity(
|
||||
|
|
|
@ -36,9 +36,13 @@ pub async fn update_comment(
|
|||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
let orig_comment = CommentView::read(
|
||||
&mut context.pool(),
|
||||
comment_id,
|
||||
Some(&local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
|
|
|
@ -188,5 +188,5 @@ pub async fn create_post(
|
|||
}
|
||||
};
|
||||
|
||||
build_post_response(&context, community_id, &local_user_view.person, post_id).await
|
||||
build_post_response(&context, community_id, local_user_view, post_id).await
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ pub async fn delete_post(
|
|||
build_post_response(
|
||||
&context,
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
local_user_view,
|
||||
data.post_id,
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -55,9 +55,15 @@ pub async fn get_post(
|
|||
.await
|
||||
.is_ok();
|
||||
|
||||
let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
let local_user = local_user_view.map(|l| l.local_user);
|
||||
let post_view = PostView::read(
|
||||
&mut context.pool(),
|
||||
post_id,
|
||||
local_user.as_ref(),
|
||||
is_mod_or_admin,
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
let post_id = post_view.post.id;
|
||||
if let Some(person_id) = person_id {
|
||||
|
@ -76,20 +82,19 @@ pub async fn get_post(
|
|||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
person_id,
|
||||
local_user.as_ref(),
|
||||
is_mod_or_admin,
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
||||
let local_user = local_user_view.as_ref().map(|u| &u.local_user);
|
||||
|
||||
// Fetch the cross_posts
|
||||
let cross_posts = if let Some(url) = &post_view.post.url {
|
||||
let mut x_posts = PostQuery {
|
||||
url_search: Some(url.inner().as_str().into()),
|
||||
local_user,
|
||||
local_user: local_user.as_ref(),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
local_user::LocalUser,
|
||||
moderator::{ModRemovePost, ModRemovePostForm},
|
||||
post::{Post, PostUpdateForm},
|
||||
post_report::PostReport,
|
||||
|
@ -37,6 +38,14 @@ pub async fn remove_post(
|
|||
)
|
||||
.await?;
|
||||
|
||||
LocalUser::is_higher_mod_or_admin_check(
|
||||
&mut context.pool(),
|
||||
orig_post.community_id,
|
||||
local_user_view.person.id,
|
||||
vec![orig_post.creator_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Update the post
|
||||
let post_id = data.post_id;
|
||||
let removed = data.removed;
|
||||
|
@ -73,11 +82,5 @@ pub async fn remove_post(
|
|||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
&context,
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ pub async fn update_post(
|
|||
build_post_response(
|
||||
context.deref(),
|
||||
orig_post.community_id,
|
||||
&local_user_view.person,
|
||||
local_user_view,
|
||||
post_id,
|
||||
)
|
||||
.await
|
||||
|
|
|
@ -84,7 +84,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, true),
|
||||
|pool| CommunityModeratorView::for_person(pool, person_id, Some(&local_user_view.local_user)),
|
||||
|pool| LocalUserLanguage::read(pool, local_user_id)
|
||||
))
|
||||
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
||||
|
|
|
@ -150,18 +150,18 @@ pub async fn register(
|
|||
.unwrap_or(site_view.site.content_warning.is_some());
|
||||
|
||||
// Create the local user
|
||||
let local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_person.id)
|
||||
.email(data.email.as_deref().map(str::to_lowercase))
|
||||
.password_encrypted(data.password.to_string())
|
||||
.show_nsfw(Some(show_nsfw))
|
||||
.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_tags.first().cloned())
|
||||
let local_user_form = LocalUserInsertForm {
|
||||
email: data.email.as_deref().map(str::to_lowercase),
|
||||
password_encrypted: data.password.to_string(),
|
||||
show_nsfw: Some(show_nsfw),
|
||||
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_tags.first().cloned(),
|
||||
// If its the initial site setup, they are an admin
|
||||
.admin(Some(!local_site.site_setup))
|
||||
.build();
|
||||
admin: Some(!local_site.site_setup),
|
||||
..LocalUserInsertForm::new(inserted_person.id, data.password.to_string())
|
||||
};
|
||||
|
||||
let all_languages = Language::read_all(&mut context.pool()).await?;
|
||||
// use hashset to avoid duplicates
|
||||
|
|
|
@ -31,7 +31,7 @@ serde = { workspace = true }
|
|||
actix-web = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
url = { workspace = true }
|
||||
http = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
|
|
|
@ -43,6 +43,7 @@ pub async fn list_posts(
|
|||
let saved_only = data.saved_only;
|
||||
let show_hidden = data.show_hidden;
|
||||
let show_read = data.show_read;
|
||||
let show_nsfw = data.show_nsfw;
|
||||
|
||||
let liked_only = data.liked_only;
|
||||
let disliked_only = data.disliked_only;
|
||||
|
@ -84,6 +85,7 @@ pub async fn list_posts(
|
|||
limit,
|
||||
show_hidden,
|
||||
show_read,
|
||||
show_nsfw,
|
||||
..Default::default()
|
||||
}
|
||||
.list(&local_site.site, &mut context.pool())
|
||||
|
|
|
@ -29,7 +29,7 @@ pub async fn get_community(
|
|||
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
|
||||
let person_id = local_user_view.as_ref().map(|u| u.person.id);
|
||||
let local_user = local_user_view.as_ref().map(|u| &u.local_user);
|
||||
|
||||
let community_id = match data.id {
|
||||
Some(id) => id,
|
||||
|
@ -53,7 +53,7 @@ pub async fn get_community(
|
|||
let community_view = CommunityView::read(
|
||||
&mut context.pool(),
|
||||
community_id,
|
||||
person_id,
|
||||
local_user,
|
||||
is_mod_or_admin,
|
||||
)
|
||||
.await?
|
||||
|
|
|
@ -96,7 +96,7 @@ pub async fn read_person(
|
|||
let moderates = CommunityModeratorView::for_person(
|
||||
&mut context.pool(),
|
||||
person_details_id,
|
||||
local_user_view.is_some(),
|
||||
local_user_view.map(|l| l.local_user).as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ use lemmy_api_common::{
|
|||
site::{ResolveObject, ResolveObjectResponse},
|
||||
utils::check_private_instance,
|
||||
};
|
||||
use lemmy_db_schema::{newtypes::PersonId, source::local_site::LocalSite, utils::DbPool};
|
||||
use lemmy_db_schema::{source::local_site::LocalSite, utils::DbPool};
|
||||
use lemmy_db_views::structs::{CommentView, LocalUserView, PostView};
|
||||
use lemmy_db_views_actor::structs::{CommunityView, PersonView};
|
||||
use lemmy_utils::error::{LemmyErrorExt2, LemmyErrorType, LemmyResult};
|
||||
|
@ -23,10 +23,9 @@ pub async fn resolve_object(
|
|||
) -> LemmyResult<Json<ResolveObjectResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
check_private_instance(&local_user_view, &local_site)?;
|
||||
let person_id = local_user_view.map(|v| v.person.id);
|
||||
// If we get a valid personId back we can safely assume that the user is authenticated,
|
||||
// if there's no personId then the JWT was missing or invalid.
|
||||
let is_authenticated = person_id.is_some();
|
||||
let is_authenticated = local_user_view.is_some();
|
||||
|
||||
let res = if is_authenticated {
|
||||
// user is fully authenticated; allow remote lookups as well.
|
||||
|
@ -37,24 +36,26 @@ pub async fn resolve_object(
|
|||
}
|
||||
.with_lemmy_type(LemmyErrorType::CouldntFindObject)?;
|
||||
|
||||
convert_response(res, person_id, &mut context.pool())
|
||||
convert_response(res, local_user_view, &mut context.pool())
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntFindObject)
|
||||
}
|
||||
|
||||
async fn convert_response(
|
||||
object: SearchableObjects,
|
||||
user_id: Option<PersonId>,
|
||||
local_user_view: Option<LocalUserView>,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> LemmyResult<Json<ResolveObjectResponse>> {
|
||||
use SearchableObjects::*;
|
||||
let removed_or_deleted;
|
||||
let mut res = ResolveObjectResponse::default();
|
||||
let local_user = local_user_view.map(|l| l.local_user);
|
||||
|
||||
match object {
|
||||
Post(p) => {
|
||||
removed_or_deleted = p.deleted || p.removed;
|
||||
res.post = Some(
|
||||
PostView::read(pool, p.id, user_id, false)
|
||||
PostView::read(pool, p.id, local_user.as_ref(), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?,
|
||||
)
|
||||
|
@ -62,7 +63,7 @@ async fn convert_response(
|
|||
Comment(c) => {
|
||||
removed_or_deleted = c.deleted || c.removed;
|
||||
res.comment = Some(
|
||||
CommentView::read(pool, c.id, user_id)
|
||||
CommentView::read(pool, c.id, local_user.as_ref())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?,
|
||||
)
|
||||
|
@ -79,7 +80,7 @@ async fn convert_response(
|
|||
UserOrCommunity::Community(c) => {
|
||||
removed_or_deleted = c.deleted || c.removed;
|
||||
res.community = Some(
|
||||
CommunityView::read(pool, c.id, user_id, false)
|
||||
CommunityView::read(pool, c.id, local_user.as_ref(), false)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?,
|
||||
)
|
||||
|
|
|
@ -345,10 +345,7 @@ mod tests {
|
|||
};
|
||||
let person = Person::create(&mut context.pool(), &person_form).await?;
|
||||
|
||||
let user_form = LocalUserInsertForm::builder()
|
||||
.person_id(person.id)
|
||||
.password_encrypted("pass".to_string())
|
||||
.build();
|
||||
let user_form = LocalUserInsertForm::test_form(person.id);
|
||||
let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?;
|
||||
|
||||
Ok(
|
||||
|
|
|
@ -109,7 +109,7 @@ impl Object for ApubSite {
|
|||
icon: self.icon.clone().map(ImageObject::new),
|
||||
image: self.banner.clone().map(ImageObject::new),
|
||||
inbox: self.inbox_url.clone().into(),
|
||||
outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?,
|
||||
outbox: Url::parse(&format!("{}site_outbox", self.actor_id))?,
|
||||
public_key: self.public_key(),
|
||||
language,
|
||||
content_warning: self.content_warning.clone(),
|
||||
|
|
|
@ -13,10 +13,10 @@ use lemmy_api_common::context::LemmyContext;
|
|||
use lemmy_db_schema::{source::community::Community, traits::Crud};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::Display;
|
||||
use strum::Display;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Display)]
|
||||
#[derive(Clone, Debug, Display, Deserialize, Serialize)]
|
||||
pub enum LockType {
|
||||
Lock,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::Display;
|
||||
use strum::Display;
|
||||
|
||||
pub mod block;
|
||||
pub mod community;
|
||||
|
|
|
@ -8,7 +8,7 @@ use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
|
|||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::Display;
|
||||
use strum::Display;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
|
|
|
@ -46,7 +46,6 @@ serde = { workspace = true }
|
|||
serde_with = { workspace = true }
|
||||
url = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
activitypub_federation = { workspace = true, optional = true }
|
||||
lemmy_utils = { workspace = true, optional = true }
|
||||
|
|
|
@ -533,10 +533,7 @@ mod tests {
|
|||
|
||||
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
|
||||
let person = Person::create(pool, &person_form).await.unwrap();
|
||||
let local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(person.id)
|
||||
.password_encrypted("my_pw".to_string())
|
||||
.build();
|
||||
let local_user_form = LocalUserInsertForm::test_form(person.id);
|
||||
|
||||
let local_user = LocalUser::create(pool, &local_user_form, vec![])
|
||||
.await
|
||||
|
@ -645,10 +642,7 @@ mod tests {
|
|||
|
||||
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
|
||||
let person = Person::create(pool, &person_form).await.unwrap();
|
||||
let local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(person.id)
|
||||
.password_encrypted("my_pw".to_string())
|
||||
.build();
|
||||
let local_user_form = LocalUserInsertForm::test_form(person.id);
|
||||
let local_user = LocalUser::create(pool, &local_user_form, vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
use crate::{
|
||||
diesel::{DecoratableTarget, OptionalExtension},
|
||||
newtypes::{CommunityId, DbUrl, PersonId},
|
||||
schema::{community, community_follower, instance},
|
||||
schema::{
|
||||
community,
|
||||
community_follower,
|
||||
community_moderator,
|
||||
community_person_ban,
|
||||
instance,
|
||||
post,
|
||||
},
|
||||
source::{
|
||||
actor_language::CommunityLanguage,
|
||||
community::{
|
||||
|
@ -42,6 +49,7 @@ use diesel::{
|
|||
Queryable,
|
||||
};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[async_trait]
|
||||
impl Crud for Community {
|
||||
|
@ -83,9 +91,8 @@ impl Joinable for CommunityModerator {
|
|||
pool: &mut DbPool<'_>,
|
||||
community_moderator_form: &CommunityModeratorForm,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::community_moderator::dsl::community_moderator;
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(community_moderator)
|
||||
insert_into(community_moderator::table)
|
||||
.values(community_moderator_form)
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
|
@ -95,9 +102,8 @@ impl Joinable for CommunityModerator {
|
|||
pool: &mut DbPool<'_>,
|
||||
community_moderator_form: &CommunityModeratorForm,
|
||||
) -> Result<usize, Error> {
|
||||
use crate::schema::community_moderator::dsl::community_moderator;
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::delete(community_moderator.find((
|
||||
diesel::delete(community_moderator::table.find((
|
||||
community_moderator_form.person_id,
|
||||
community_moderator_form.community_id,
|
||||
)))
|
||||
|
@ -147,25 +153,23 @@ impl Community {
|
|||
pool: &mut DbPool<'_>,
|
||||
url: &DbUrl,
|
||||
) -> Result<Option<(Community, CollectionType)>, Error> {
|
||||
use crate::schema::community::dsl::{featured_url, moderators_url};
|
||||
use CollectionType::*;
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
let res = community::table
|
||||
.filter(moderators_url.eq(url))
|
||||
.filter(community::moderators_url.eq(url))
|
||||
.first(conn)
|
||||
.await
|
||||
.optional()?;
|
||||
|
||||
if let Some(c) = res {
|
||||
Ok(Some((c, Moderators)))
|
||||
Ok(Some((c, CollectionType::Moderators)))
|
||||
} else {
|
||||
let res = community::table
|
||||
.filter(featured_url.eq(url))
|
||||
.filter(community::featured_url.eq(url))
|
||||
.first(conn)
|
||||
.await
|
||||
.optional()?;
|
||||
if let Some(c) = res {
|
||||
Ok(Some((c, Featured)))
|
||||
Ok(Some((c, CollectionType::Featured)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
|
@ -177,7 +181,6 @@ impl Community {
|
|||
posts: Vec<Post>,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> Result<(), Error> {
|
||||
use crate::schema::post;
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
for p in &posts {
|
||||
debug_assert!(p.community_id == community_id);
|
||||
|
@ -185,10 +188,10 @@ impl Community {
|
|||
// Mark the given posts as featured and all other posts as not featured.
|
||||
let post_ids = posts.iter().map(|p| p.id);
|
||||
update(post::table)
|
||||
.filter(post::dsl::community_id.eq(community_id))
|
||||
.filter(post::community_id.eq(community_id))
|
||||
// This filter is just for performance
|
||||
.filter(post::dsl::featured_community.or(post::dsl::id.eq_any(post_ids.clone())))
|
||||
.set(post::dsl::featured_community.eq(post::dsl::id.eq_any(post_ids)))
|
||||
.filter(post::featured_community.or(post::id.eq_any(post_ids.clone())))
|
||||
.set(post::featured_community.eq(post::id.eq_any(post_ids)))
|
||||
.execute(conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
@ -200,37 +203,68 @@ impl CommunityModerator {
|
|||
pool: &mut DbPool<'_>,
|
||||
for_community_id: CommunityId,
|
||||
) -> Result<usize, Error> {
|
||||
use crate::schema::community_moderator::dsl::{community_id, community_moderator};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
diesel::delete(community_moderator.filter(community_id.eq(for_community_id)))
|
||||
.execute(conn)
|
||||
.await
|
||||
diesel::delete(
|
||||
community_moderator::table.filter(community_moderator::community_id.eq(for_community_id)),
|
||||
)
|
||||
.execute(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn leave_all_communities(
|
||||
pool: &mut DbPool<'_>,
|
||||
for_person_id: PersonId,
|
||||
) -> Result<usize, Error> {
|
||||
use crate::schema::community_moderator::dsl::{community_moderator, person_id};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::delete(community_moderator.filter(person_id.eq(for_person_id)))
|
||||
.execute(conn)
|
||||
.await
|
||||
diesel::delete(
|
||||
community_moderator::table.filter(community_moderator::person_id.eq(for_person_id)),
|
||||
)
|
||||
.execute(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_person_moderated_communities(
|
||||
pool: &mut DbPool<'_>,
|
||||
for_person_id: PersonId,
|
||||
) -> Result<Vec<CommunityId>, Error> {
|
||||
use crate::schema::community_moderator::dsl::{community_id, community_moderator, person_id};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
community_moderator
|
||||
.filter(person_id.eq(for_person_id))
|
||||
.select(community_id)
|
||||
community_moderator::table
|
||||
.filter(community_moderator::person_id.eq(for_person_id))
|
||||
.select(community_moderator::community_id)
|
||||
.load::<CommunityId>(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Checks to make sure the acting moderator was added earlier than the target moderator
|
||||
pub async fn is_higher_mod_check(
|
||||
pool: &mut DbPool<'_>,
|
||||
for_community_id: CommunityId,
|
||||
mod_person_id: PersonId,
|
||||
target_person_ids: Vec<PersonId>,
|
||||
) -> LemmyResult<()> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
// Build the list of persons
|
||||
let mut persons = target_person_ids;
|
||||
persons.push(mod_person_id);
|
||||
persons.dedup();
|
||||
|
||||
let res = community_moderator::table
|
||||
.filter(community_moderator::community_id.eq(for_community_id))
|
||||
.filter(community_moderator::person_id.eq_any(persons))
|
||||
.order_by(community_moderator::published)
|
||||
// This does a limit 1 select first
|
||||
.first::<CommunityModerator>(conn)
|
||||
.await?;
|
||||
|
||||
// If the first result sorted by published is the acting mod
|
||||
if res.person_id == mod_person_id {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LemmyErrorType::NotHigherMod)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
@ -240,11 +274,13 @@ impl Bannable for CommunityPersonBan {
|
|||
pool: &mut DbPool<'_>,
|
||||
community_person_ban_form: &CommunityPersonBanForm,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::community_person_ban::dsl::{community_id, community_person_ban, person_id};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(community_person_ban)
|
||||
insert_into(community_person_ban::table)
|
||||
.values(community_person_ban_form)
|
||||
.on_conflict((community_id, person_id))
|
||||
.on_conflict((
|
||||
community_person_ban::community_id,
|
||||
community_person_ban::person_id,
|
||||
))
|
||||
.do_update()
|
||||
.set(community_person_ban_form)
|
||||
.get_result::<Self>(conn)
|
||||
|
@ -255,9 +291,8 @@ impl Bannable for CommunityPersonBan {
|
|||
pool: &mut DbPool<'_>,
|
||||
community_person_ban_form: &CommunityPersonBanForm,
|
||||
) -> Result<usize, Error> {
|
||||
use crate::schema::community_person_ban::dsl::community_person_ban;
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::delete(community_person_ban.find((
|
||||
diesel::delete(community_person_ban::table.find((
|
||||
community_person_ban_form.person_id,
|
||||
community_person_ban_form.community_id,
|
||||
)))
|
||||
|
@ -291,11 +326,10 @@ impl CommunityFollower {
|
|||
pool: &mut DbPool<'_>,
|
||||
remote_community_id: CommunityId,
|
||||
) -> Result<bool, Error> {
|
||||
use crate::schema::community_follower::dsl::{community_follower, community_id};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
select(exists(
|
||||
community_follower.filter(community_id.eq(remote_community_id)),
|
||||
))
|
||||
select(exists(community_follower::table.filter(
|
||||
community_follower::community_id.eq(remote_community_id),
|
||||
)))
|
||||
.get_result(conn)
|
||||
.await
|
||||
}
|
||||
|
@ -316,11 +350,13 @@ impl Queryable<sql_types::Nullable<sql_types::Bool>, Pg> for SubscribedType {
|
|||
impl Followable for CommunityFollower {
|
||||
type Form = CommunityFollowerForm;
|
||||
async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result<Self, Error> {
|
||||
use crate::schema::community_follower::dsl::{community_follower, community_id, person_id};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(community_follower)
|
||||
insert_into(community_follower::table)
|
||||
.values(form)
|
||||
.on_conflict((community_id, person_id))
|
||||
.on_conflict((
|
||||
community_follower::community_id,
|
||||
community_follower::person_id,
|
||||
))
|
||||
.do_update()
|
||||
.set(form)
|
||||
.get_result::<Self>(conn)
|
||||
|
@ -331,17 +367,16 @@ impl Followable for CommunityFollower {
|
|||
community_id: CommunityId,
|
||||
person_id: PersonId,
|
||||
) -> Result<Self, Error> {
|
||||
use crate::schema::community_follower::dsl::{community_follower, pending};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::update(community_follower.find((person_id, community_id)))
|
||||
.set(pending.eq(false))
|
||||
diesel::update(community_follower::table.find((person_id, community_id)))
|
||||
.set(community_follower::pending.eq(false))
|
||||
.get_result::<Self>(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result<usize, Error> {
|
||||
use crate::schema::community_follower::dsl::community_follower;
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::delete(community_follower.find((form.person_id, form.community_id)))
|
||||
diesel::delete(community_follower::table.find((form.person_id, form.community_id)))
|
||||
.execute(conn)
|
||||
.await
|
||||
}
|
||||
|
@ -397,10 +432,8 @@ impl ApubActor for Community {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
|
||||
use crate::{
|
||||
source::{
|
||||
community::{
|
||||
|
@ -415,28 +448,30 @@ mod tests {
|
|||
CommunityUpdateForm,
|
||||
},
|
||||
instance::Instance,
|
||||
local_user::LocalUser,
|
||||
person::{Person, PersonInsertForm},
|
||||
},
|
||||
traits::{Bannable, Crud, Followable, Joinable},
|
||||
utils::build_db_pool_for_tests,
|
||||
CommunityVisibility,
|
||||
};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serial_test::serial;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_crud() {
|
||||
async fn test_crud() -> LemmyResult<()> {
|
||||
let pool = &build_db_pool_for_tests().await;
|
||||
let pool = &mut pool.into();
|
||||
|
||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
||||
|
||||
let new_person = PersonInsertForm::test_form(inserted_instance.id, "bobbee");
|
||||
let bobby_person = PersonInsertForm::test_form(inserted_instance.id, "bobby");
|
||||
let inserted_bobby = Person::create(pool, &bobby_person).await?;
|
||||
|
||||
let inserted_person = Person::create(pool, &new_person).await.unwrap();
|
||||
let artemis_person = PersonInsertForm::test_form(inserted_instance.id, "artemis");
|
||||
let inserted_artemis = Person::create(pool, &artemis_person).await?;
|
||||
|
||||
let new_community = CommunityInsertForm::builder()
|
||||
.name("TIL".into())
|
||||
|
@ -445,7 +480,7 @@ mod tests {
|
|||
.instance_id(inserted_instance.id)
|
||||
.build();
|
||||
|
||||
let inserted_community = Community::create(pool, &new_community).await.unwrap();
|
||||
let inserted_community = Community::create(pool, &new_community).await?;
|
||||
|
||||
let expected_community = Community {
|
||||
id: inserted_community.id,
|
||||
|
@ -477,91 +512,120 @@ mod tests {
|
|||
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
community_id: inserted_community.id,
|
||||
person_id: inserted_person.id,
|
||||
person_id: inserted_bobby.id,
|
||||
pending: false,
|
||||
};
|
||||
|
||||
let inserted_community_follower = CommunityFollower::follow(pool, &community_follower_form)
|
||||
.await
|
||||
.unwrap();
|
||||
let inserted_community_follower =
|
||||
CommunityFollower::follow(pool, &community_follower_form).await?;
|
||||
|
||||
let expected_community_follower = CommunityFollower {
|
||||
community_id: inserted_community.id,
|
||||
person_id: inserted_person.id,
|
||||
person_id: inserted_bobby.id,
|
||||
pending: false,
|
||||
published: inserted_community_follower.published,
|
||||
};
|
||||
|
||||
let community_moderator_form = CommunityModeratorForm {
|
||||
let bobby_moderator_form = CommunityModeratorForm {
|
||||
community_id: inserted_community.id,
|
||||
person_id: inserted_person.id,
|
||||
person_id: inserted_bobby.id,
|
||||
};
|
||||
|
||||
let inserted_community_moderator = CommunityModerator::join(pool, &community_moderator_form)
|
||||
.await
|
||||
.unwrap();
|
||||
let inserted_bobby_moderator = CommunityModerator::join(pool, &bobby_moderator_form).await?;
|
||||
|
||||
let artemis_moderator_form = CommunityModeratorForm {
|
||||
community_id: inserted_community.id,
|
||||
person_id: inserted_artemis.id,
|
||||
};
|
||||
|
||||
let _inserted_artemis_moderator =
|
||||
CommunityModerator::join(pool, &artemis_moderator_form).await?;
|
||||
|
||||
let expected_community_moderator = CommunityModerator {
|
||||
community_id: inserted_community.id,
|
||||
person_id: inserted_person.id,
|
||||
published: inserted_community_moderator.published,
|
||||
person_id: inserted_bobby.id,
|
||||
published: inserted_bobby_moderator.published,
|
||||
};
|
||||
|
||||
let moderator_person_ids = vec![inserted_bobby.id, inserted_artemis.id];
|
||||
|
||||
// Make sure bobby is marked as a higher mod than artemis, and vice versa
|
||||
let bobby_higher_check = CommunityModerator::is_higher_mod_check(
|
||||
pool,
|
||||
inserted_community.id,
|
||||
inserted_bobby.id,
|
||||
moderator_person_ids.clone(),
|
||||
)
|
||||
.await;
|
||||
assert!(bobby_higher_check.is_ok());
|
||||
|
||||
// Also check the other is_higher_mod_or_admin function just in case
|
||||
let bobby_higher_check_2 = LocalUser::is_higher_mod_or_admin_check(
|
||||
pool,
|
||||
inserted_community.id,
|
||||
inserted_bobby.id,
|
||||
moderator_person_ids.clone(),
|
||||
)
|
||||
.await;
|
||||
assert!(bobby_higher_check_2.is_ok());
|
||||
|
||||
// This should throw an error, since artemis was added later
|
||||
let artemis_higher_check = CommunityModerator::is_higher_mod_check(
|
||||
pool,
|
||||
inserted_community.id,
|
||||
inserted_artemis.id,
|
||||
moderator_person_ids,
|
||||
)
|
||||
.await;
|
||||
assert!(artemis_higher_check.is_err());
|
||||
|
||||
let community_person_ban_form = CommunityPersonBanForm {
|
||||
community_id: inserted_community.id,
|
||||
person_id: inserted_person.id,
|
||||
person_id: inserted_bobby.id,
|
||||
expires: None,
|
||||
};
|
||||
|
||||
let inserted_community_person_ban = CommunityPersonBan::ban(pool, &community_person_ban_form)
|
||||
.await
|
||||
.unwrap();
|
||||
let inserted_community_person_ban =
|
||||
CommunityPersonBan::ban(pool, &community_person_ban_form).await?;
|
||||
|
||||
let expected_community_person_ban = CommunityPersonBan {
|
||||
community_id: inserted_community.id,
|
||||
person_id: inserted_person.id,
|
||||
person_id: inserted_bobby.id,
|
||||
published: inserted_community_person_ban.published,
|
||||
expires: None,
|
||||
};
|
||||
|
||||
let read_community = Community::read(pool, inserted_community.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
let update_community_form = CommunityUpdateForm {
|
||||
title: Some("nada".to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
let updated_community = Community::update(pool, inserted_community.id, &update_community_form)
|
||||
.await
|
||||
.unwrap();
|
||||
let updated_community =
|
||||
Community::update(pool, inserted_community.id, &update_community_form).await?;
|
||||
|
||||
let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form)
|
||||
.await
|
||||
.unwrap();
|
||||
let left_community = CommunityModerator::leave(pool, &community_moderator_form)
|
||||
.await
|
||||
.unwrap();
|
||||
let unban = CommunityPersonBan::unban(pool, &community_person_ban_form)
|
||||
.await
|
||||
.unwrap();
|
||||
let num_deleted = Community::delete(pool, inserted_community.id)
|
||||
.await
|
||||
.unwrap();
|
||||
Person::delete(pool, inserted_person.id).await.unwrap();
|
||||
Instance::delete(pool, inserted_instance.id).await.unwrap();
|
||||
let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form).await?;
|
||||
let left_community = CommunityModerator::leave(pool, &bobby_moderator_form).await?;
|
||||
let unban = CommunityPersonBan::unban(pool, &community_person_ban_form).await?;
|
||||
let num_deleted = Community::delete(pool, inserted_community.id).await?;
|
||||
Person::delete(pool, inserted_bobby.id).await?;
|
||||
Person::delete(pool, inserted_artemis.id).await?;
|
||||
Instance::delete(pool, inserted_instance.id).await?;
|
||||
|
||||
assert_eq!(expected_community, read_community);
|
||||
assert_eq!(expected_community, inserted_community);
|
||||
assert_eq!(expected_community, updated_community);
|
||||
assert_eq!(expected_community_follower, inserted_community_follower);
|
||||
assert_eq!(expected_community_moderator, inserted_community_moderator);
|
||||
assert_eq!(expected_community_moderator, inserted_bobby_moderator);
|
||||
assert_eq!(expected_community_person_ban, inserted_community_person_ban);
|
||||
assert_eq!(1, ignored_community);
|
||||
assert_eq!(1, left_community);
|
||||
assert_eq!(1, unban);
|
||||
// assert_eq!(2, loaded_count);
|
||||
assert_eq!(1, num_deleted);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
newtypes::{DbUrl, LanguageId, LocalUserId, PersonId},
|
||||
schema::{local_user, person, registration_application},
|
||||
newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId},
|
||||
schema::{community, community_moderator, local_user, person, registration_application},
|
||||
source::{
|
||||
actor_language::LocalUserLanguage,
|
||||
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
|
||||
|
@ -13,16 +13,19 @@ use crate::{
|
|||
now,
|
||||
DbPool,
|
||||
},
|
||||
CommunityVisibility,
|
||||
};
|
||||
use bcrypt::{hash, DEFAULT_COST};
|
||||
use diesel::{
|
||||
dsl::{insert_into, not, IntervalDsl},
|
||||
result::Error,
|
||||
CombineDsl,
|
||||
ExpressionMethods,
|
||||
JoinOnDsl,
|
||||
QueryDsl,
|
||||
};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
impl LocalUser {
|
||||
pub async fn create(
|
||||
|
@ -215,6 +218,72 @@ impl LocalUser {
|
|||
blocked_instances,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks to make sure the acting admin is higher than the target admin
|
||||
pub async fn is_higher_admin_check(
|
||||
pool: &mut DbPool<'_>,
|
||||
admin_person_id: PersonId,
|
||||
target_person_ids: Vec<PersonId>,
|
||||
) -> LemmyResult<()> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
// Build the list of persons
|
||||
let mut persons = target_person_ids;
|
||||
persons.push(admin_person_id);
|
||||
persons.dedup();
|
||||
|
||||
let res = local_user::table
|
||||
.filter(local_user::admin.eq(true))
|
||||
.filter(local_user::person_id.eq_any(persons))
|
||||
.order_by(local_user::id)
|
||||
// This does a limit 1 select first
|
||||
.first::<LocalUser>(conn)
|
||||
.await?;
|
||||
|
||||
// If the first result sorted by published is the acting admin
|
||||
if res.person_id == admin_person_id {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LemmyErrorType::NotHigherAdmin)?
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks to make sure the acting moderator is higher than the target moderator
|
||||
pub async fn is_higher_mod_or_admin_check(
|
||||
pool: &mut DbPool<'_>,
|
||||
for_community_id: CommunityId,
|
||||
admin_person_id: PersonId,
|
||||
target_person_ids: Vec<PersonId>,
|
||||
) -> LemmyResult<()> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
||||
// Build the list of persons
|
||||
let mut persons = target_person_ids;
|
||||
persons.push(admin_person_id);
|
||||
persons.dedup();
|
||||
|
||||
let admins = local_user::table
|
||||
.filter(local_user::admin.eq(true))
|
||||
.filter(local_user::person_id.eq_any(&persons))
|
||||
.order_by(local_user::id)
|
||||
.select(local_user::person_id);
|
||||
|
||||
let mods = community_moderator::table
|
||||
.filter(community_moderator::community_id.eq(for_community_id))
|
||||
.filter(community_moderator::person_id.eq_any(&persons))
|
||||
.order_by(community_moderator::published)
|
||||
.select(community_moderator::person_id);
|
||||
|
||||
let res = admins.union_all(mods).get_results::<PersonId>(conn).await?;
|
||||
let first_person = res.as_slice().first().ok_or(LemmyErrorType::NotHigherMod)?;
|
||||
|
||||
// If the first result sorted by published is the acting mod
|
||||
if *first_person == admin_person_id {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LemmyErrorType::NotHigherMod)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds some helper functions for an optional LocalUser
|
||||
|
@ -225,6 +294,12 @@ pub trait LocalUserOptionHelper {
|
|||
fn show_read_posts(&self) -> bool;
|
||||
fn is_admin(&self) -> bool;
|
||||
fn show_nsfw(&self, site: &Site) -> bool;
|
||||
fn visible_communities_only<Q>(&self, query: Q) -> Q
|
||||
where
|
||||
Q: diesel::query_dsl::methods::FilterDsl<
|
||||
diesel::dsl::Eq<community::visibility, CommunityVisibility>,
|
||||
Output = Q,
|
||||
>;
|
||||
}
|
||||
|
||||
impl LocalUserOptionHelper for Option<&LocalUser> {
|
||||
|
@ -253,14 +328,32 @@ impl LocalUserOptionHelper for Option<&LocalUser> {
|
|||
.map(|l| l.show_nsfw)
|
||||
.unwrap_or(site.content_warning.is_some())
|
||||
}
|
||||
|
||||
fn visible_communities_only<Q>(&self, query: Q) -> Q
|
||||
where
|
||||
Q: diesel::query_dsl::methods::FilterDsl<
|
||||
diesel::dsl::Eq<community::visibility, CommunityVisibility>,
|
||||
Output = Q,
|
||||
>,
|
||||
{
|
||||
if self.is_none() {
|
||||
query.filter(community::visibility.eq(CommunityVisibility::Public))
|
||||
} else {
|
||||
query
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalUserInsertForm {
|
||||
pub fn test_form(person_id: PersonId) -> Self {
|
||||
Self::builder()
|
||||
.person_id(person_id)
|
||||
.password_encrypted(String::new())
|
||||
.build()
|
||||
Self::new(person_id, String::new())
|
||||
}
|
||||
|
||||
pub fn test_form_admin(person_id: PersonId) -> Self {
|
||||
LocalUserInsertForm {
|
||||
admin: Some(true),
|
||||
..Self::test_form(person_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -272,3 +365,58 @@ pub struct UserBackupLists {
|
|||
pub blocked_users: Vec<DbUrl>,
|
||||
pub blocked_instances: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
source::{
|
||||
instance::Instance,
|
||||
local_user::{LocalUser, LocalUserInsertForm},
|
||||
person::{Person, PersonInsertForm},
|
||||
},
|
||||
traits::Crud,
|
||||
utils::build_db_pool_for_tests,
|
||||
};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
use serial_test::serial;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_admin_higher_check() -> LemmyResult<()> {
|
||||
let pool = &build_db_pool_for_tests().await;
|
||||
let pool = &mut pool.into();
|
||||
|
||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
||||
|
||||
let fiona_person = PersonInsertForm::test_form(inserted_instance.id, "fiona");
|
||||
let inserted_fiona_person = Person::create(pool, &fiona_person).await?;
|
||||
|
||||
let fiona_local_user_form = LocalUserInsertForm::test_form_admin(inserted_fiona_person.id);
|
||||
let _inserted_fiona_local_user =
|
||||
LocalUser::create(pool, &fiona_local_user_form, vec![]).await?;
|
||||
|
||||
let delores_person = PersonInsertForm::test_form(inserted_instance.id, "delores");
|
||||
let inserted_delores_person = Person::create(pool, &delores_person).await?;
|
||||
let delores_local_user_form = LocalUserInsertForm::test_form_admin(inserted_delores_person.id);
|
||||
let _inserted_delores_local_user =
|
||||
LocalUser::create(pool, &delores_local_user_form, vec![]).await?;
|
||||
|
||||
let admin_person_ids = vec![inserted_fiona_person.id, inserted_delores_person.id];
|
||||
|
||||
// Make sure fiona is marked as a higher admin than delores, and vice versa
|
||||
let fiona_higher_check =
|
||||
LocalUser::is_higher_admin_check(pool, inserted_fiona_person.id, admin_person_ids.clone())
|
||||
.await;
|
||||
assert!(fiona_higher_check.is_ok());
|
||||
|
||||
// This should throw an error, since delores was added later
|
||||
let delores_higher_check =
|
||||
LocalUser::is_higher_admin_check(pool, inserted_delores_person.id, admin_person_ids).await;
|
||||
assert!(delores_higher_check.is_err());
|
||||
|
||||
Instance::delete(pool, inserted_instance.id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ impl LocalUserVoteDisplayMode {
|
|||
.get_result::<Self>(conn)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
pool: &mut DbPool<'_>,
|
||||
local_user_id: LocalUserId,
|
||||
|
|
|
@ -72,10 +72,7 @@ mod tests {
|
|||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
||||
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy prw");
|
||||
let inserted_person = Person::create(pool, &new_person).await?;
|
||||
let new_local_user = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_person.id)
|
||||
.password_encrypted("pass".to_string())
|
||||
.build();
|
||||
let new_local_user = LocalUserInsertForm::test_form(inserted_person.id);
|
||||
let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?;
|
||||
|
||||
// Create password reset token
|
||||
|
|
|
@ -182,11 +182,10 @@ impl ApubActor for Person {
|
|||
impl Followable for PersonFollower {
|
||||
type Form = PersonFollowerForm;
|
||||
async fn follow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result<Self, Error> {
|
||||
use crate::schema::person_follower::dsl::{follower_id, person_follower, person_id};
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
insert_into(person_follower)
|
||||
insert_into(person_follower::table)
|
||||
.values(form)
|
||||
.on_conflict((follower_id, person_id))
|
||||
.on_conflict((person_follower::follower_id, person_follower::person_id))
|
||||
.do_update()
|
||||
.set(form)
|
||||
.get_result::<Self>(conn)
|
||||
|
@ -196,9 +195,8 @@ impl Followable for PersonFollower {
|
|||
unimplemented!()
|
||||
}
|
||||
async fn unfollow(pool: &mut DbPool<'_>, form: &PersonFollowerForm) -> Result<usize, Error> {
|
||||
use crate::schema::person_follower::dsl::person_follower;
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
diesel::delete(person_follower.find((form.follower_id, form.person_id)))
|
||||
diesel::delete(person_follower::table.find((form.follower_id, form.person_id)))
|
||||
.execute(conn)
|
||||
.await
|
||||
}
|
||||
|
@ -220,7 +218,6 @@ impl PersonFollower {
|
|||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
|
||||
|
@ -232,22 +229,21 @@ mod tests {
|
|||
traits::{Crud, Followable},
|
||||
utils::build_db_pool_for_tests,
|
||||
};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
use pretty_assertions::assert_eq;
|
||||
use serial_test::serial;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_crud() {
|
||||
async fn test_crud() -> LemmyResult<()> {
|
||||
let pool = &build_db_pool_for_tests().await;
|
||||
let pool = &mut pool.into();
|
||||
|
||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
||||
|
||||
let new_person = PersonInsertForm::test_form(inserted_instance.id, "holly");
|
||||
|
||||
let inserted_person = Person::create(pool, &new_person).await.unwrap();
|
||||
let inserted_person = Person::create(pool, &new_person).await?;
|
||||
|
||||
let expected_person = Person {
|
||||
id: inserted_person.id,
|
||||
|
@ -274,57 +270,54 @@ mod tests {
|
|||
};
|
||||
|
||||
let read_person = Person::read(pool, inserted_person.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||
|
||||
let update_person_form = PersonUpdateForm {
|
||||
actor_id: Some(inserted_person.actor_id.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
let updated_person = Person::update(pool, inserted_person.id, &update_person_form)
|
||||
.await
|
||||
.unwrap();
|
||||
let updated_person = Person::update(pool, inserted_person.id, &update_person_form).await?;
|
||||
|
||||
let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
|
||||
Instance::delete(pool, inserted_instance.id).await.unwrap();
|
||||
let num_deleted = Person::delete(pool, inserted_person.id).await?;
|
||||
Instance::delete(pool, inserted_instance.id).await?;
|
||||
|
||||
assert_eq!(expected_person, read_person);
|
||||
assert_eq!(expected_person, inserted_person);
|
||||
assert_eq!(expected_person, updated_person);
|
||||
assert_eq!(1, num_deleted);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn follow() {
|
||||
async fn follow() -> LemmyResult<()> {
|
||||
let pool = &build_db_pool_for_tests().await;
|
||||
let pool = &mut pool.into();
|
||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
|
||||
.await
|
||||
.unwrap();
|
||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
||||
|
||||
let person_form_1 = PersonInsertForm::test_form(inserted_instance.id, "erich");
|
||||
let person_1 = Person::create(pool, &person_form_1).await.unwrap();
|
||||
let person_1 = Person::create(pool, &person_form_1).await?;
|
||||
let person_form_2 = PersonInsertForm::test_form(inserted_instance.id, "michele");
|
||||
let person_2 = Person::create(pool, &person_form_2).await.unwrap();
|
||||
let person_2 = Person::create(pool, &person_form_2).await?;
|
||||
|
||||
let follow_form = PersonFollowerForm {
|
||||
person_id: person_1.id,
|
||||
follower_id: person_2.id,
|
||||
pending: false,
|
||||
};
|
||||
let person_follower = PersonFollower::follow(pool, &follow_form).await.unwrap();
|
||||
let person_follower = PersonFollower::follow(pool, &follow_form).await?;
|
||||
assert_eq!(person_1.id, person_follower.person_id);
|
||||
assert_eq!(person_2.id, person_follower.follower_id);
|
||||
assert!(!person_follower.pending);
|
||||
|
||||
let followers = PersonFollower::list_followers(pool, person_1.id)
|
||||
.await
|
||||
.unwrap();
|
||||
let followers = PersonFollower::list_followers(pool, person_1.id).await?;
|
||||
assert_eq!(vec![person_2], followers);
|
||||
|
||||
let unfollow = PersonFollower::unfollow(pool, &follow_form).await.unwrap();
|
||||
let unfollow = PersonFollower::unfollow(pool, &follow_form).await?;
|
||||
assert_eq!(1, unfollow);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
diesel::OptionalExtension,
|
||||
newtypes::LocalUserId,
|
||||
newtypes::{LocalUserId, RegistrationApplicationId},
|
||||
schema::registration_application::dsl::{local_user_id, registration_application},
|
||||
source::registration_application::{
|
||||
RegistrationApplication,
|
||||
|
@ -17,7 +17,7 @@ use diesel_async::RunQueryDsl;
|
|||
impl Crud for RegistrationApplication {
|
||||
type InsertForm = RegistrationApplicationInsertForm;
|
||||
type UpdateForm = RegistrationApplicationUpdateForm;
|
||||
type IdType = i32;
|
||||
type IdType = RegistrationApplicationId;
|
||||
|
||||
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result<Self, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
|
|
|
@ -43,7 +43,7 @@ pub mod utils;
|
|||
pub mod schema_setup;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::{Display, EnumString};
|
||||
use strum::{Display, EnumString};
|
||||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ pub struct PrivateMessageReportId(i32);
|
|||
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// The site id.
|
||||
pub struct SiteId(i32);
|
||||
pub struct SiteId(pub i32);
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
|
||||
|
@ -148,6 +148,12 @@ pub struct LocalSiteId(i32);
|
|||
/// The custom emoji id.
|
||||
pub struct CustomEmojiId(i32);
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// The registration application id.
|
||||
pub struct RegistrationApplicationId(i32);
|
||||
|
||||
#[cfg(feature = "full")]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "Ltree")]
|
||||
|
|
|
@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_with::skip_serializing_none;
|
||||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
||||
|
@ -69,38 +68,59 @@ pub struct LocalUser {
|
|||
pub collapse_bot_comments: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, TypedBuilder)]
|
||||
#[builder(field_defaults(default))]
|
||||
#[derive(Clone, derive_new::new)]
|
||||
#[cfg_attr(feature = "full", derive(Insertable))]
|
||||
#[cfg_attr(feature = "full", diesel(table_name = local_user))]
|
||||
pub struct LocalUserInsertForm {
|
||||
#[builder(!default)]
|
||||
pub person_id: PersonId,
|
||||
#[builder(!default)]
|
||||
pub password_encrypted: String,
|
||||
#[new(default)]
|
||||
pub email: Option<String>,
|
||||
#[new(default)]
|
||||
pub show_nsfw: Option<bool>,
|
||||
#[new(default)]
|
||||
pub theme: Option<String>,
|
||||
#[new(default)]
|
||||
pub default_sort_type: Option<SortType>,
|
||||
#[new(default)]
|
||||
pub default_listing_type: Option<ListingType>,
|
||||
#[new(default)]
|
||||
pub interface_language: Option<String>,
|
||||
#[new(default)]
|
||||
pub show_avatars: Option<bool>,
|
||||
#[new(default)]
|
||||
pub send_notifications_to_email: Option<bool>,
|
||||
#[new(default)]
|
||||
pub show_bot_accounts: Option<bool>,
|
||||
#[new(default)]
|
||||
pub show_scores: Option<bool>,
|
||||
#[new(default)]
|
||||
pub show_read_posts: Option<bool>,
|
||||
#[new(default)]
|
||||
pub email_verified: Option<bool>,
|
||||
#[new(default)]
|
||||
pub accepted_application: Option<bool>,
|
||||
#[new(default)]
|
||||
pub totp_2fa_secret: Option<Option<String>>,
|
||||
#[new(default)]
|
||||
pub open_links_in_new_tab: Option<bool>,
|
||||
#[new(default)]
|
||||
pub blur_nsfw: Option<bool>,
|
||||
#[new(default)]
|
||||
pub auto_expand: Option<bool>,
|
||||
#[new(default)]
|
||||
pub infinite_scroll_enabled: Option<bool>,
|
||||
#[new(default)]
|
||||
pub admin: Option<bool>,
|
||||
#[new(default)]
|
||||
pub post_listing_mode: Option<PostListingMode>,
|
||||
#[new(default)]
|
||||
pub totp_2fa_enabled: Option<bool>,
|
||||
#[new(default)]
|
||||
pub enable_keyboard_navigation: Option<bool>,
|
||||
#[new(default)]
|
||||
pub enable_animated_images: Option<bool>,
|
||||
#[new(default)]
|
||||
pub collapse_bot_comments: Option<bool>,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::newtypes::{LocalUserId, PersonId};
|
||||
use crate::newtypes::{LocalUserId, PersonId, RegistrationApplicationId};
|
||||
#[cfg(feature = "full")]
|
||||
use crate::schema::registration_application;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
@ -15,7 +15,7 @@ use ts_rs::TS;
|
|||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// A registration application.
|
||||
pub struct RegistrationApplication {
|
||||
pub id: i32,
|
||||
pub id: RegistrationApplicationId,
|
||||
pub local_user_id: LocalUserId,
|
||||
pub answer: String,
|
||||
pub admin_id: Option<PersonId>,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
diesel::ExpressionMethods,
|
||||
newtypes::{DbUrl, PersonId},
|
||||
newtypes::{DbUrl, CommentSortType, PersonId},
|
||||
schema::community,
|
||||
schema_setup,
|
||||
CommentSortType,
|
||||
|
@ -10,7 +10,6 @@ use crate::{
|
|||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
use deadpool::Runtime;
|
||||
use diesel::{
|
||||
dsl,
|
||||
helper_types::AsExprOf,
|
||||
pg::Pg,
|
||||
query_builder::{Query, QueryFragment},
|
||||
|
@ -336,10 +335,6 @@ fn establish_connection(config: &str) -> BoxFuture<ConnectionResult<AsyncPgConne
|
|||
let fut = async {
|
||||
// We only support TLS with sslmode=require currently
|
||||
let mut conn = if config.contains("sslmode=require") {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
|
||||
let rustls_config = DangerousClientConfigBuilder {
|
||||
cfg: ClientConfig::builder(),
|
||||
}
|
||||
|
@ -593,20 +588,6 @@ impl<RF, LF> Queries<RF, LF> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn visible_communities_only<Q>(my_person_id: Option<PersonId>, query: Q) -> Q
|
||||
where
|
||||
Q: diesel::query_dsl::methods::FilterDsl<
|
||||
dsl::Eq<community::visibility, CommunityVisibility>,
|
||||
Output = Q,
|
||||
>,
|
||||
{
|
||||
if my_person_id.is_none() {
|
||||
query.filter(community::visibility.eq(CommunityVisibility::Public))
|
||||
} else {
|
||||
query
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
|
|
|
@ -301,10 +301,7 @@ mod tests {
|
|||
|
||||
let inserted_timmy = Person::create(pool, &new_person).await.unwrap();
|
||||
|
||||
let new_local_user = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_timmy.id)
|
||||
.password_encrypted("123".to_string())
|
||||
.build();
|
||||
let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id);
|
||||
let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -36,22 +36,13 @@ use lemmy_db_schema::{
|
|||
post,
|
||||
},
|
||||
source::local_user::LocalUser,
|
||||
utils::{
|
||||
fuzzy_search,
|
||||
limit_and_offset,
|
||||
visible_communities_only,
|
||||
DbConn,
|
||||
DbPool,
|
||||
ListFn,
|
||||
Queries,
|
||||
ReadFn,
|
||||
},
|
||||
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
||||
CommentSortType,
|
||||
ListingType,
|
||||
};
|
||||
|
||||
fn queries<'a>() -> Queries<
|
||||
impl ReadFn<'a, CommentView, (CommentId, Option<PersonId>)>,
|
||||
impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>,
|
||||
impl ListFn<'a, CommentView, CommentQuery<'a>>,
|
||||
> {
|
||||
let is_creator_banned_from_community = exists(
|
||||
|
@ -182,9 +173,12 @@ fn queries<'a>() -> Queries<
|
|||
};
|
||||
|
||||
let read = move |mut conn: DbConn<'a>,
|
||||
(comment_id, my_person_id): (CommentId, Option<PersonId>)| async move {
|
||||
let mut query = all_joins(comment::table.find(comment_id).into_boxed(), my_person_id);
|
||||
query = visible_communities_only(my_person_id, query);
|
||||
(comment_id, my_local_user): (CommentId, Option<&'a LocalUser>)| async move {
|
||||
let mut query = all_joins(
|
||||
comment::table.find(comment_id).into_boxed(),
|
||||
my_local_user.person_id(),
|
||||
);
|
||||
query = my_local_user.visible_communities_only(query);
|
||||
query.first(&mut conn).await
|
||||
};
|
||||
|
||||
|
@ -301,7 +295,7 @@ fn queries<'a>() -> Queries<
|
|||
query = query.filter(not(is_creator_blocked(person_id_join)));
|
||||
};
|
||||
|
||||
query = visible_communities_only(options.local_user.person_id(), query);
|
||||
query = options.local_user.visible_communities_only(query);
|
||||
|
||||
// A Max depth given means its a tree fetch
|
||||
let (limit, offset) = if let Some(max_depth) = options.max_depth {
|
||||
|
@ -366,16 +360,16 @@ fn queries<'a>() -> Queries<
|
|||
}
|
||||
|
||||
impl CommentView {
|
||||
pub async fn read(
|
||||
pub async fn read<'a>(
|
||||
pool: &mut DbPool<'_>,
|
||||
comment_id: CommentId,
|
||||
my_person_id: Option<PersonId>,
|
||||
my_local_user: Option<&'a LocalUser>,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
// If a person is given, then my_vote (res.9), if None, should be 0, not null
|
||||
// Necessary to differentiate between other person's votes
|
||||
if let Ok(Some(res)) = queries().read(pool, (comment_id, my_person_id)).await {
|
||||
if let Ok(Some(res)) = queries().read(pool, (comment_id, my_local_user)).await {
|
||||
let mut new_view = res.clone();
|
||||
if my_person_id.is_some() && res.my_vote.is_none() {
|
||||
if my_local_user.is_some() && res.my_vote.is_none() {
|
||||
new_view.my_vote = Some(0);
|
||||
}
|
||||
if res.comment.deleted || res.comment.removed {
|
||||
|
@ -490,11 +484,8 @@ mod tests {
|
|||
|
||||
let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy");
|
||||
let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?;
|
||||
let timmy_local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_timmy_person.id)
|
||||
.admin(Some(true))
|
||||
.password_encrypted(String::new())
|
||||
.build();
|
||||
let timmy_local_user_form = LocalUserInsertForm::test_form_admin(inserted_timmy_person.id);
|
||||
|
||||
let inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?;
|
||||
|
||||
let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara");
|
||||
|
@ -676,7 +667,7 @@ mod tests {
|
|||
let read_comment_from_blocked_person = CommentView::read(
|
||||
pool,
|
||||
data.inserted_comment_1.id,
|
||||
Some(data.timmy_local_user_view.person.id),
|
||||
Some(&data.timmy_local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
@ -1171,7 +1162,7 @@ mod tests {
|
|||
let authenticated_comment = CommentView::read(
|
||||
pool,
|
||||
data.inserted_comment_0.id,
|
||||
Some(data.timmy_local_user_view.person.id),
|
||||
Some(&data.timmy_local_user_view.local_user),
|
||||
)
|
||||
.await;
|
||||
assert!(authenticated_comment.is_ok());
|
||||
|
@ -1211,7 +1202,7 @@ mod tests {
|
|||
let comment_view = CommentView::read(
|
||||
pool,
|
||||
data.inserted_comment_0.id,
|
||||
Some(inserted_banned_from_comm_local_user.person_id),
|
||||
Some(&inserted_banned_from_comm_local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
@ -1232,7 +1223,7 @@ mod tests {
|
|||
let comment_view = CommentView::read(
|
||||
pool,
|
||||
data.inserted_comment_0.id,
|
||||
Some(data.timmy_local_user_view.person.id),
|
||||
Some(&data.timmy_local_user_view.local_user),
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||
|
|
|
@ -323,10 +323,7 @@ mod tests {
|
|||
|
||||
let inserted_timmy = Person::create(pool, &new_person).await.unwrap();
|
||||
|
||||
let new_local_user = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_timmy.id)
|
||||
.password_encrypted("123".to_string())
|
||||
.build();
|
||||
let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id);
|
||||
let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -49,7 +49,6 @@ use lemmy_db_schema::{
|
|||
get_conn,
|
||||
limit_and_offset,
|
||||
now,
|
||||
visible_communities_only,
|
||||
Commented,
|
||||
DbConn,
|
||||
DbPool,
|
||||
|
@ -64,7 +63,7 @@ use lemmy_db_schema::{
|
|||
use tracing::debug;
|
||||
|
||||
fn queries<'a>() -> Queries<
|
||||
impl ReadFn<'a, PostView, (PostId, Option<PersonId>, bool)>,
|
||||
impl ReadFn<'a, PostView, (PostId, Option<&'a LocalUser>, bool)>,
|
||||
impl ListFn<'a, PostView, (PostQuery<'a>, &'a Site)>,
|
||||
> {
|
||||
let is_creator_banned_from_community = exists(
|
||||
|
@ -142,6 +141,7 @@ fn queries<'a>() -> Queries<
|
|||
.single_value()
|
||||
};
|
||||
|
||||
// TODO maybe this should go to localuser also
|
||||
let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>,
|
||||
my_person_id: Option<PersonId>| {
|
||||
let is_local_user_banned_from_community_selection: Box<
|
||||
|
@ -250,52 +250,56 @@ fn queries<'a>() -> Queries<
|
|||
))
|
||||
};
|
||||
|
||||
let read =
|
||||
move |mut conn: DbConn<'a>,
|
||||
(post_id, my_person_id, is_mod_or_admin): (PostId, Option<PersonId>, bool)| async move {
|
||||
// The left join below will return None in this case
|
||||
let person_id_join = my_person_id.unwrap_or(PersonId(-1));
|
||||
let read = move |mut conn: DbConn<'a>,
|
||||
(post_id, my_local_user, is_mod_or_admin): (
|
||||
PostId,
|
||||
Option<&'a LocalUser>,
|
||||
bool,
|
||||
)| async move {
|
||||
// The left join below will return None in this case
|
||||
let my_person_id = my_local_user.person_id();
|
||||
let person_id_join = my_person_id.unwrap_or(PersonId(-1));
|
||||
|
||||
let mut query = all_joins(
|
||||
post_aggregates::table
|
||||
.filter(post_aggregates::post_id.eq(post_id))
|
||||
.into_boxed(),
|
||||
my_person_id,
|
||||
);
|
||||
let mut query = all_joins(
|
||||
post_aggregates::table
|
||||
.filter(post_aggregates::post_id.eq(post_id))
|
||||
.into_boxed(),
|
||||
my_person_id,
|
||||
);
|
||||
|
||||
// Hide deleted and removed for non-admins or mods
|
||||
if !is_mod_or_admin {
|
||||
query = query
|
||||
.filter(
|
||||
community::removed
|
||||
.eq(false)
|
||||
.or(post::creator_id.eq(person_id_join)),
|
||||
)
|
||||
.filter(
|
||||
post::removed
|
||||
.eq(false)
|
||||
.or(post::creator_id.eq(person_id_join)),
|
||||
)
|
||||
// users can see their own deleted posts
|
||||
.filter(
|
||||
community::deleted
|
||||
.eq(false)
|
||||
.or(post::creator_id.eq(person_id_join)),
|
||||
)
|
||||
.filter(
|
||||
post::deleted
|
||||
.eq(false)
|
||||
.or(post::creator_id.eq(person_id_join)),
|
||||
);
|
||||
}
|
||||
// Hide deleted and removed for non-admins or mods
|
||||
if !is_mod_or_admin {
|
||||
query = query
|
||||
.filter(
|
||||
community::removed
|
||||
.eq(false)
|
||||
.or(post::creator_id.eq(person_id_join)),
|
||||
)
|
||||
.filter(
|
||||
post::removed
|
||||
.eq(false)
|
||||
.or(post::creator_id.eq(person_id_join)),
|
||||
)
|
||||
// users can see their own deleted posts
|
||||
.filter(
|
||||
community::deleted
|
||||
.eq(false)
|
||||
.or(post::creator_id.eq(person_id_join)),
|
||||
)
|
||||
.filter(
|
||||
post::deleted
|
||||
.eq(false)
|
||||
.or(post::creator_id.eq(person_id_join)),
|
||||
);
|
||||
}
|
||||
|
||||
query = visible_communities_only(my_person_id, query);
|
||||
query = my_local_user.visible_communities_only(query);
|
||||
|
||||
Commented::new(query)
|
||||
.text("PostView::read")
|
||||
.first(&mut conn)
|
||||
.await
|
||||
};
|
||||
Commented::new(query)
|
||||
.text("PostView::read")
|
||||
.first(&mut conn)
|
||||
.await
|
||||
};
|
||||
|
||||
let list = move |mut conn: DbConn<'a>, (options, site): (PostQuery<'a>, &'a Site)| async move {
|
||||
// The left join below will return None in this case
|
||||
|
@ -392,7 +396,10 @@ fn queries<'a>() -> Queries<
|
|||
.filter(not(post::removed.or(post::deleted)));
|
||||
}
|
||||
|
||||
if !options.local_user.show_nsfw(site) {
|
||||
if !options
|
||||
.show_nsfw
|
||||
.unwrap_or(options.local_user.show_nsfw(site))
|
||||
{
|
||||
query = query
|
||||
.filter(post::nsfw.eq(false))
|
||||
.filter(community::nsfw.eq(false));
|
||||
|
@ -437,7 +444,7 @@ fn queries<'a>() -> Queries<
|
|||
}
|
||||
};
|
||||
|
||||
query = visible_communities_only(options.local_user.person_id(), query);
|
||||
query = options.local_user.visible_communities_only(query);
|
||||
|
||||
// Dont filter blocks or missing languages for moderator view type
|
||||
if let (Some(person_id), false) = (
|
||||
|
@ -552,14 +559,14 @@ fn queries<'a>() -> Queries<
|
|||
}
|
||||
|
||||
impl PostView {
|
||||
pub async fn read(
|
||||
pub async fn read<'a>(
|
||||
pool: &mut DbPool<'_>,
|
||||
post_id: PostId,
|
||||
my_person_id: Option<PersonId>,
|
||||
my_local_user: Option<&'a LocalUser>,
|
||||
is_mod_or_admin: bool,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
queries()
|
||||
.read(pool, (post_id, my_person_id, is_mod_or_admin))
|
||||
.read(pool, (post_id, my_local_user, is_mod_or_admin))
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
@ -617,6 +624,7 @@ pub struct PostQuery<'a> {
|
|||
pub page_back: Option<bool>,
|
||||
pub show_hidden: Option<bool>,
|
||||
pub show_read: Option<bool>,
|
||||
pub show_nsfw: Option<bool>,
|
||||
}
|
||||
|
||||
impl<'a> PostQuery<'a> {
|
||||
|
@ -938,7 +946,7 @@ mod tests {
|
|||
let post_listing_single_with_person = PostView::read(
|
||||
pool,
|
||||
data.inserted_post.id,
|
||||
Some(data.local_user_view.person.id),
|
||||
Some(&data.local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
|
@ -1067,7 +1075,7 @@ mod tests {
|
|||
let post_listing_single_with_person = PostView::read(
|
||||
pool,
|
||||
data.inserted_post.id,
|
||||
Some(data.local_user_view.person.id),
|
||||
Some(&data.local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
|
@ -1585,6 +1593,48 @@ mod tests {
|
|||
cleanup(data, pool).await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn post_listings_hide_nsfw() -> LemmyResult<()> {
|
||||
let pool = &build_db_pool().await?;
|
||||
let pool = &mut pool.into();
|
||||
let data = init_data(pool).await?;
|
||||
|
||||
// Mark a post as nsfw
|
||||
let update_form = PostUpdateForm {
|
||||
nsfw: Some(true),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Post::update(pool, data.inserted_bot_post.id, &update_form).await?;
|
||||
|
||||
// Make sure you don't see the nsfw post in the regular results
|
||||
let post_listings_hide_nsfw = data.default_post_query().list(&data.site, pool).await?;
|
||||
assert_eq!(vec![POST], names(&post_listings_hide_nsfw));
|
||||
|
||||
// Make sure it does come back with the show_nsfw option
|
||||
let post_listings_show_nsfw = PostQuery {
|
||||
sort: Some(SortType::New),
|
||||
show_nsfw: Some(true),
|
||||
local_user: Some(&data.local_user_view.local_user),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&data.site, pool)
|
||||
.await?;
|
||||
assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_nsfw));
|
||||
|
||||
// Make sure that nsfw field is true.
|
||||
assert!(
|
||||
&post_listings_show_nsfw
|
||||
.first()
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?
|
||||
.post
|
||||
.nsfw
|
||||
);
|
||||
|
||||
cleanup(data, pool).await
|
||||
}
|
||||
|
||||
async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
|
||||
let num_deleted = Post::delete(pool, data.inserted_post.id).await?;
|
||||
Community::delete(pool, data.inserted_community.id).await?;
|
||||
|
@ -1755,7 +1805,7 @@ mod tests {
|
|||
let authenticated_post = PostView::read(
|
||||
pool,
|
||||
data.inserted_post.id,
|
||||
Some(data.local_user_view.person.id),
|
||||
Some(&data.local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
@ -1797,7 +1847,7 @@ mod tests {
|
|||
let post_view = PostView::read(
|
||||
pool,
|
||||
data.inserted_post.id,
|
||||
Some(inserted_banned_from_comm_local_user.person_id),
|
||||
Some(&inserted_banned_from_comm_local_user),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
|
@ -1819,7 +1869,7 @@ mod tests {
|
|||
let post_view = PostView::read(
|
||||
pool,
|
||||
data.inserted_post.id,
|
||||
Some(data.local_user_view.person.id),
|
||||
Some(&data.local_user_view.local_user),
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
|
|
|
@ -11,12 +11,18 @@ use diesel::{
|
|||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema::{
|
||||
aliases,
|
||||
newtypes::{PersonId, RegistrationApplicationId},
|
||||
schema::{local_user, person, registration_application},
|
||||
utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
||||
};
|
||||
|
||||
enum ReadBy {
|
||||
Id(RegistrationApplicationId),
|
||||
Person(PersonId),
|
||||
}
|
||||
|
||||
fn queries<'a>() -> Queries<
|
||||
impl ReadFn<'a, RegistrationApplicationView, i32>,
|
||||
impl ReadFn<'a, RegistrationApplicationView, ReadBy>,
|
||||
impl ListFn<'a, RegistrationApplicationView, RegistrationApplicationQuery>,
|
||||
> {
|
||||
let all_joins = |query: registration_application::BoxedQuery<'a, Pg>| {
|
||||
|
@ -36,14 +42,15 @@ fn queries<'a>() -> Queries<
|
|||
))
|
||||
};
|
||||
|
||||
let read = move |mut conn: DbConn<'a>, registration_application_id: i32| async move {
|
||||
all_joins(
|
||||
registration_application::table
|
||||
.find(registration_application_id)
|
||||
.into_boxed(),
|
||||
)
|
||||
.first(&mut conn)
|
||||
.await
|
||||
let read = move |mut conn: DbConn<'a>, search: ReadBy| async move {
|
||||
let mut query = all_joins(registration_application::table.into_boxed());
|
||||
|
||||
query = match search {
|
||||
ReadBy::Id(id) => query.filter(registration_application::id.eq(id)),
|
||||
ReadBy::Person(person_id) => query.filter(person::id.eq(person_id)),
|
||||
};
|
||||
|
||||
query.first(&mut conn).await
|
||||
};
|
||||
|
||||
let list = move |mut conn: DbConn<'a>, options: RegistrationApplicationQuery| async move {
|
||||
|
@ -76,11 +83,17 @@ fn queries<'a>() -> Queries<
|
|||
impl RegistrationApplicationView {
|
||||
pub async fn read(
|
||||
pool: &mut DbPool<'_>,
|
||||
registration_application_id: i32,
|
||||
id: RegistrationApplicationId,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
queries().read(pool, registration_application_id).await
|
||||
queries().read(pool, ReadBy::Id(id)).await
|
||||
}
|
||||
|
||||
pub async fn read_by_person(
|
||||
pool: &mut DbPool<'_>,
|
||||
person_id: PersonId,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
queries().read(pool, ReadBy::Person(person_id)).await
|
||||
}
|
||||
/// Returns the current unread registration_application count
|
||||
pub async fn get_unread_count(
|
||||
pool: &mut DbPool<'_>,
|
||||
|
@ -167,11 +180,7 @@ mod tests {
|
|||
|
||||
let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap();
|
||||
|
||||
let timmy_local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_timmy_person.id)
|
||||
.password_encrypted("nada".to_string())
|
||||
.admin(Some(true))
|
||||
.build();
|
||||
let timmy_local_user_form = LocalUserInsertForm::test_form_admin(inserted_timmy_person.id);
|
||||
|
||||
let _inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![])
|
||||
.await
|
||||
|
@ -181,10 +190,7 @@ mod tests {
|
|||
|
||||
let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap();
|
||||
|
||||
let sara_local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_sara_person.id)
|
||||
.password_encrypted("nada".to_string())
|
||||
.build();
|
||||
let sara_local_user_form = LocalUserInsertForm::test_form(inserted_sara_person.id);
|
||||
|
||||
let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![])
|
||||
.await
|
||||
|
@ -209,10 +215,7 @@ mod tests {
|
|||
|
||||
let inserted_jess_person = Person::create(pool, &jess_person_form).await.unwrap();
|
||||
|
||||
let jess_local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_jess_person.id)
|
||||
.password_encrypted("nada".to_string())
|
||||
.build();
|
||||
let jess_local_user_form = LocalUserInsertForm::test_form(inserted_jess_person.id);
|
||||
|
||||
let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![])
|
||||
.await
|
||||
|
|
|
@ -33,7 +33,6 @@ serde_with = { workspace = true }
|
|||
ts-rs = { workspace = true, optional = true }
|
||||
chrono.workspace = true
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = { workspace = true }
|
||||
|
|
|
@ -2,10 +2,11 @@ use crate::structs::CommunityModeratorView;
|
|||
use diesel::{dsl::exists, result::Error, select, ExpressionMethods, QueryDsl};
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_db_schema::{
|
||||
impls::local_user::LocalUserOptionHelper,
|
||||
newtypes::{CommunityId, PersonId},
|
||||
schema::{community, community_moderator, person},
|
||||
source::local_user::LocalUser,
|
||||
utils::{get_conn, DbPool},
|
||||
CommunityVisibility,
|
||||
};
|
||||
|
||||
impl CommunityModeratorView {
|
||||
|
@ -60,20 +61,28 @@ impl CommunityModeratorView {
|
|||
pub async fn for_person(
|
||||
pool: &mut DbPool<'_>,
|
||||
person_id: PersonId,
|
||||
is_authenticated: bool,
|
||||
local_user: Option<&LocalUser>,
|
||||
) -> Result<Vec<Self>, Error> {
|
||||
let conn = &mut get_conn(pool).await?;
|
||||
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))
|
||||
.into_boxed();
|
||||
if !is_authenticated {
|
||||
query = query.filter(community::visibility.eq(CommunityVisibility::Public));
|
||||
|
||||
query = local_user.visible_communities_only(query);
|
||||
|
||||
// only show deleted communities to creator
|
||||
if Some(person_id) != local_user.person_id() {
|
||||
query = query.filter(community::deleted.eq(false));
|
||||
}
|
||||
|
||||
// Show removed communities to admins only
|
||||
if !local_user.is_admin() {
|
||||
query = query.filter(community::removed.eq(false))
|
||||
}
|
||||
|
||||
query.load::<CommunityModeratorView>(conn).await
|
||||
}
|
||||
|
||||
|
|
|
@ -22,27 +22,18 @@ use lemmy_db_schema::{
|
|||
instance_block,
|
||||
},
|
||||
source::{community::CommunityFollower, local_user::LocalUser, site::Site},
|
||||
utils::{
|
||||
fuzzy_search,
|
||||
limit_and_offset,
|
||||
visible_communities_only,
|
||||
DbConn,
|
||||
DbPool,
|
||||
ListFn,
|
||||
Queries,
|
||||
ReadFn,
|
||||
},
|
||||
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
|
||||
ListingType,
|
||||
SortType,
|
||||
};
|
||||
|
||||
fn queries<'a>() -> Queries<
|
||||
impl ReadFn<'a, CommunityView, (CommunityId, Option<PersonId>, bool)>,
|
||||
impl ReadFn<'a, CommunityView, (CommunityId, Option<&'a LocalUser>, bool)>,
|
||||
impl ListFn<'a, CommunityView, (CommunityQuery<'a>, &'a Site)>,
|
||||
> {
|
||||
let all_joins = |query: community::BoxedQuery<'a, Pg>, my_person_id: Option<PersonId>| {
|
||||
let all_joins = |query: community::BoxedQuery<'a, Pg>, my_local_user: Option<&'a LocalUser>| {
|
||||
// The left join below will return None in this case
|
||||
let person_id_join = my_person_id.unwrap_or(PersonId(-1));
|
||||
let person_id_join = my_local_user.person_id().unwrap_or(PersonId(-1));
|
||||
|
||||
query
|
||||
.inner_join(community_aggregates::table)
|
||||
|
@ -89,14 +80,14 @@ fn queries<'a>() -> Queries<
|
|||
.and(community::deleted.eq(false));
|
||||
|
||||
let read = move |mut conn: DbConn<'a>,
|
||||
(community_id, my_person_id, is_mod_or_admin): (
|
||||
(community_id, my_local_user, is_mod_or_admin): (
|
||||
CommunityId,
|
||||
Option<PersonId>,
|
||||
Option<&'a LocalUser>,
|
||||
bool,
|
||||
)| async move {
|
||||
let mut query = all_joins(
|
||||
community::table.find(community_id).into_boxed(),
|
||||
my_person_id,
|
||||
my_local_user,
|
||||
)
|
||||
.select(selection);
|
||||
|
||||
|
@ -105,7 +96,7 @@ fn queries<'a>() -> Queries<
|
|||
query = query.filter(not_removed_or_deleted);
|
||||
}
|
||||
|
||||
query = visible_communities_only(my_person_id, query);
|
||||
query = my_local_user.visible_communities_only(query);
|
||||
|
||||
query.first(&mut conn).await
|
||||
};
|
||||
|
@ -116,11 +107,7 @@ fn queries<'a>() -> Queries<
|
|||
// The left join below will return None in this case
|
||||
let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1));
|
||||
|
||||
let mut query = all_joins(
|
||||
community::table.into_boxed(),
|
||||
options.local_user.person_id(),
|
||||
)
|
||||
.select(selection);
|
||||
let mut query = all_joins(community::table.into_boxed(), options.local_user).select(selection);
|
||||
|
||||
if let Some(search_term) = options.search_term {
|
||||
let searcher = fuzzy_search(&search_term);
|
||||
|
@ -173,7 +160,7 @@ fn queries<'a>() -> Queries<
|
|||
query = query.filter(community::nsfw.eq(false));
|
||||
}
|
||||
|
||||
query = visible_communities_only(options.local_user.person_id(), query);
|
||||
query = options.local_user.visible_communities_only(query);
|
||||
|
||||
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
|
||||
query
|
||||
|
@ -187,14 +174,14 @@ fn queries<'a>() -> Queries<
|
|||
}
|
||||
|
||||
impl CommunityView {
|
||||
pub async fn read(
|
||||
pub async fn read<'a>(
|
||||
pool: &mut DbPool<'_>,
|
||||
community_id: CommunityId,
|
||||
my_person_id: Option<PersonId>,
|
||||
my_local_user: Option<&'a LocalUser>,
|
||||
is_mod_or_admin: bool,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
queries()
|
||||
.read(pool, (community_id, my_person_id, is_mod_or_admin))
|
||||
.read(pool, (community_id, my_local_user, is_mod_or_admin))
|
||||
.await
|
||||
}
|
||||
|
||||
|
@ -288,10 +275,7 @@ mod tests {
|
|||
|
||||
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_form = LocalUserInsertForm::test_form(inserted_person.id);
|
||||
let local_user = LocalUser::create(pool, &local_user_form, vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -388,7 +372,7 @@ mod tests {
|
|||
let authenticated_community = CommunityView::read(
|
||||
pool,
|
||||
data.inserted_community.id,
|
||||
Some(data.local_user.person_id),
|
||||
Some(&data.local_user),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
|
|
@ -27,7 +27,7 @@ use lemmy_db_schema::{
|
|||
SortType,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::{Display, EnumString};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
enum ListMode {
|
||||
Admins,
|
||||
|
@ -196,10 +196,7 @@ mod tests {
|
|||
..PersonInsertForm::test_form(inserted_instance.id, "alice")
|
||||
};
|
||||
let alice = Person::create(pool, &alice_form).await?;
|
||||
let alice_local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(alice.id)
|
||||
.password_encrypted(String::new())
|
||||
.build();
|
||||
let alice_local_user_form = LocalUserInsertForm::test_form(alice.id);
|
||||
let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?;
|
||||
|
||||
let bob_form = PersonInsertForm {
|
||||
|
@ -208,10 +205,7 @@ mod tests {
|
|||
..PersonInsertForm::test_form(inserted_instance.id, "bob")
|
||||
};
|
||||
let bob = Person::create(pool, &bob_form).await?;
|
||||
let bob_local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(bob.id)
|
||||
.password_encrypted(String::new())
|
||||
.build();
|
||||
let bob_local_user_form = LocalUserInsertForm::test_form(bob.id);
|
||||
let bob_local_user = LocalUser::create(pool, &bob_local_user_form, vec![]).await?;
|
||||
|
||||
Ok(Data {
|
||||
|
|
|
@ -34,6 +34,13 @@ tokio = { workspace = true, features = ["full"] }
|
|||
tracing.workspace = true
|
||||
moka.workspace = true
|
||||
tokio-util = "0.7.11"
|
||||
async-trait.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = { workspace = true }
|
||||
url.workspace = true
|
||||
actix-web.workspace = true
|
||||
tracing-test = "0.2.5"
|
||||
uuid.workspace = true
|
||||
test-context = "0.3.0"
|
||||
mockall = "0.12.1"
|
||||
|
|
572
crates/federate/src/inboxes.rs
Normal file
572
crates/federate/src/inboxes.rs
Normal file
|
@ -0,0 +1,572 @@
|
|||
use crate::util::LEMMY_TEST_FAST_FEDERATION;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::{CommunityId, DbUrl, InstanceId},
|
||||
source::{activity::SentActivity, site::Site},
|
||||
utils::{ActualDbPool, DbPool},
|
||||
};
|
||||
use lemmy_db_views_actor::structs::CommunityFollowerView;
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Url;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// interval with which new additions to community_followers are queried.
|
||||
///
|
||||
/// The first time some user on an instance follows a specific remote community (or, more precisely:
|
||||
/// the first time a (followed_community_id, follower_inbox_url) tuple appears), this delay limits
|
||||
/// the maximum time until the follow actually results in activities from that community id being
|
||||
/// sent to that inbox url. This delay currently needs to not be too small because the DB load is
|
||||
/// currently fairly high because of the current structure of storing inboxes for every person, not
|
||||
/// having a separate list of shared_inboxes, and the architecture of having every instance queue be
|
||||
/// fully separate. (see https://github.com/LemmyNet/lemmy/issues/3958)
|
||||
static FOLLOW_ADDITIONS_RECHECK_DELAY: Lazy<chrono::TimeDelta> = Lazy::new(|| {
|
||||
if *LEMMY_TEST_FAST_FEDERATION {
|
||||
chrono::TimeDelta::try_seconds(1).expect("TimeDelta out of bounds")
|
||||
} else {
|
||||
chrono::TimeDelta::try_minutes(2).expect("TimeDelta out of bounds")
|
||||
}
|
||||
});
|
||||
/// The same as FOLLOW_ADDITIONS_RECHECK_DELAY, but triggering when the last person on an instance
|
||||
/// unfollows a specific remote community. This is expected to happen pretty rarely and updating it
|
||||
/// in a timely manner is not too important.
|
||||
static FOLLOW_REMOVALS_RECHECK_DELAY: Lazy<chrono::TimeDelta> =
|
||||
Lazy::new(|| chrono::TimeDelta::try_hours(1).expect("TimeDelta out of bounds"));
|
||||
|
||||
#[async_trait]
|
||||
pub trait DataSource: Send + Sync {
|
||||
async fn read_site_from_instance_id(
|
||||
&self,
|
||||
instance_id: InstanceId,
|
||||
) -> Result<Option<Site>, diesel::result::Error>;
|
||||
async fn get_instance_followed_community_inboxes(
|
||||
&self,
|
||||
instance_id: InstanceId,
|
||||
last_fetch: DateTime<Utc>,
|
||||
) -> Result<Vec<(CommunityId, DbUrl)>, diesel::result::Error>;
|
||||
}
|
||||
pub struct DbDataSource {
|
||||
pool: ActualDbPool,
|
||||
}
|
||||
|
||||
impl DbDataSource {
|
||||
pub fn new(pool: ActualDbPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DataSource for DbDataSource {
|
||||
async fn read_site_from_instance_id(
|
||||
&self,
|
||||
instance_id: InstanceId,
|
||||
) -> Result<Option<Site>, diesel::result::Error> {
|
||||
Site::read_from_instance_id(&mut DbPool::Pool(&self.pool), instance_id).await
|
||||
}
|
||||
|
||||
async fn get_instance_followed_community_inboxes(
|
||||
&self,
|
||||
instance_id: InstanceId,
|
||||
last_fetch: DateTime<Utc>,
|
||||
) -> Result<Vec<(CommunityId, DbUrl)>, diesel::result::Error> {
|
||||
CommunityFollowerView::get_instance_followed_community_inboxes(
|
||||
&mut DbPool::Pool(&self.pool),
|
||||
instance_id,
|
||||
last_fetch,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct CommunityInboxCollector<T: DataSource> {
|
||||
// load site lazily because if an instance is first seen due to being on allowlist,
|
||||
// the corresponding row in `site` may not exist yet since that is only added once
|
||||
// `fetch_instance_actor_for_object` is called.
|
||||
// (this should be unlikely to be relevant outside of the federation tests)
|
||||
site_loaded: bool,
|
||||
site: Option<Site>,
|
||||
followed_communities: HashMap<CommunityId, HashSet<Url>>,
|
||||
last_full_communities_fetch: DateTime<Utc>,
|
||||
last_incremental_communities_fetch: DateTime<Utc>,
|
||||
instance_id: InstanceId,
|
||||
domain: String,
|
||||
pub(crate) data_source: T,
|
||||
}
|
||||
|
||||
pub type RealCommunityInboxCollector = CommunityInboxCollector<DbDataSource>;
|
||||
|
||||
impl<T: DataSource> CommunityInboxCollector<T> {
|
||||
pub fn new_real(
|
||||
pool: ActualDbPool,
|
||||
instance_id: InstanceId,
|
||||
domain: String,
|
||||
) -> RealCommunityInboxCollector {
|
||||
CommunityInboxCollector::new(DbDataSource::new(pool), instance_id, domain)
|
||||
}
|
||||
pub fn new(
|
||||
data_source: T,
|
||||
instance_id: InstanceId,
|
||||
domain: String,
|
||||
) -> CommunityInboxCollector<T> {
|
||||
CommunityInboxCollector {
|
||||
data_source,
|
||||
site_loaded: false,
|
||||
site: None,
|
||||
followed_communities: HashMap::new(),
|
||||
last_full_communities_fetch: Utc.timestamp_nanos(0),
|
||||
last_incremental_communities_fetch: Utc.timestamp_nanos(0),
|
||||
instance_id,
|
||||
domain,
|
||||
}
|
||||
}
|
||||
/// get inbox urls of sending the given activity to the given instance
|
||||
/// most often this will return 0 values (if instance doesn't care about the activity)
|
||||
/// or 1 value (the shared inbox)
|
||||
/// > 1 values only happens for non-lemmy software
|
||||
pub async fn get_inbox_urls(&mut self, activity: &SentActivity) -> Result<Vec<Url>> {
|
||||
let mut inbox_urls: HashSet<Url> = HashSet::new();
|
||||
|
||||
if activity.send_all_instances {
|
||||
if !self.site_loaded {
|
||||
self.site = self
|
||||
.data_source
|
||||
.read_site_from_instance_id(self.instance_id)
|
||||
.await?;
|
||||
self.site_loaded = true;
|
||||
}
|
||||
if let Some(site) = &self.site {
|
||||
// Nutomic: Most non-lemmy software wont have a site row. That means it cant handle these
|
||||
// activities. So handling it like this is fine.
|
||||
inbox_urls.insert(site.inbox_url.inner().clone());
|
||||
}
|
||||
}
|
||||
if let Some(t) = &activity.send_community_followers_of {
|
||||
if let Some(urls) = self.followed_communities.get(t) {
|
||||
inbox_urls.extend(urls.iter().cloned());
|
||||
}
|
||||
}
|
||||
inbox_urls.extend(
|
||||
activity
|
||||
.send_inboxes
|
||||
.iter()
|
||||
.filter_map(std::option::Option::as_ref)
|
||||
// a similar filter also happens within the activitypub-federation crate. but that filter
|
||||
// happens much later - by doing it here, we can ensure that in the happy case, this
|
||||
// function returns 0 urls which means the system doesn't have to create a tokio
|
||||
// task for sending at all (since that task has a fair amount of overhead)
|
||||
.filter(|&u| (u.domain() == Some(&self.domain)))
|
||||
.map(|u| u.inner().clone()),
|
||||
);
|
||||
tracing::trace!(
|
||||
"get_inbox_urls: {:?}, send_inboxes: {:?}",
|
||||
inbox_urls,
|
||||
activity.send_inboxes
|
||||
);
|
||||
Ok(inbox_urls.into_iter().collect())
|
||||
}
|
||||
|
||||
pub async fn update_communities(&mut self) -> Result<()> {
|
||||
if (Utc::now() - self.last_full_communities_fetch) > *FOLLOW_REMOVALS_RECHECK_DELAY {
|
||||
tracing::debug!("{}: fetching full list of communities", self.domain);
|
||||
// process removals every hour
|
||||
(self.followed_communities, self.last_full_communities_fetch) = self
|
||||
.get_communities(self.instance_id, Utc.timestamp_nanos(0))
|
||||
.await?;
|
||||
self.last_incremental_communities_fetch = self.last_full_communities_fetch;
|
||||
}
|
||||
if (Utc::now() - self.last_incremental_communities_fetch) > *FOLLOW_ADDITIONS_RECHECK_DELAY {
|
||||
// process additions every minute
|
||||
let (news, time) = self
|
||||
.get_communities(self.instance_id, self.last_incremental_communities_fetch)
|
||||
.await?;
|
||||
if !news.is_empty() {
|
||||
tracing::debug!(
|
||||
"{}: fetched {} incremental new followed communities",
|
||||
self.domain,
|
||||
news.len()
|
||||
);
|
||||
}
|
||||
self.followed_communities.extend(news);
|
||||
self.last_incremental_communities_fetch = time;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get a list of local communities with the remote inboxes on the given instance that cares about
|
||||
/// them
|
||||
async fn get_communities(
|
||||
&mut self,
|
||||
instance_id: InstanceId,
|
||||
last_fetch: DateTime<Utc>,
|
||||
) -> Result<(HashMap<CommunityId, HashSet<Url>>, DateTime<Utc>)> {
|
||||
// update to time before fetch to ensure overlap. subtract some time to ensure overlap even if
|
||||
// published date is not exact
|
||||
let new_last_fetch = Utc::now() - *FOLLOW_ADDITIONS_RECHECK_DELAY / 2;
|
||||
|
||||
let inboxes = self
|
||||
.data_source
|
||||
.get_instance_followed_community_inboxes(instance_id, last_fetch)
|
||||
.await?;
|
||||
|
||||
let map: HashMap<CommunityId, HashSet<Url>> =
|
||||
inboxes.into_iter().fold(HashMap::new(), |mut map, (c, u)| {
|
||||
map.entry(c).or_default().insert(u.into());
|
||||
map
|
||||
});
|
||||
|
||||
Ok((map, new_last_fetch))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use lemmy_db_schema::{
|
||||
newtypes::{ActivityId, CommunityId, InstanceId, SiteId},
|
||||
source::activity::{ActorType, SentActivity},
|
||||
};
|
||||
use mockall::{mock, predicate::*};
|
||||
use serde_json::json;
|
||||
mock! {
|
||||
DataSource {}
|
||||
#[async_trait]
|
||||
impl DataSource for DataSource {
|
||||
async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> Result<Option<Site>, diesel::result::Error>;
|
||||
async fn get_instance_followed_community_inboxes(
|
||||
&self,
|
||||
instance_id: InstanceId,
|
||||
last_fetch: DateTime<Utc>,
|
||||
) -> Result<Vec<(CommunityId, DbUrl)>, diesel::result::Error>;
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_collector() -> CommunityInboxCollector<MockDataSource> {
|
||||
let mock_data_source = MockDataSource::new();
|
||||
let instance_id = InstanceId(1);
|
||||
let domain = "example.com".to_string();
|
||||
CommunityInboxCollector::new(mock_data_source, instance_id, domain)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_inbox_urls_empty() {
|
||||
let mut collector = setup_collector();
|
||||
let activity = SentActivity {
|
||||
id: ActivityId(1),
|
||||
ap_id: Url::parse("https://example.com/activities/1")
|
||||
.unwrap()
|
||||
.into(),
|
||||
data: json!({}),
|
||||
sensitive: false,
|
||||
published: Utc::now(),
|
||||
send_inboxes: vec![],
|
||||
send_community_followers_of: None,
|
||||
send_all_instances: false,
|
||||
actor_type: ActorType::Person,
|
||||
actor_apub_id: None,
|
||||
};
|
||||
|
||||
let result = collector.get_inbox_urls(&activity).await.unwrap();
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_inbox_urls_send_all_instances() {
|
||||
let mut collector = setup_collector();
|
||||
let site_inbox = Url::parse("https://example.com/inbox").unwrap();
|
||||
let site = Site {
|
||||
id: SiteId(1),
|
||||
name: "Test Site".to_string(),
|
||||
sidebar: None,
|
||||
published: Utc::now(),
|
||||
updated: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
description: None,
|
||||
actor_id: Url::parse("https://example.com/site").unwrap().into(),
|
||||
last_refreshed_at: Utc::now(),
|
||||
inbox_url: site_inbox.clone().into(),
|
||||
private_key: None,
|
||||
public_key: "test_key".to_string(),
|
||||
instance_id: InstanceId(1),
|
||||
content_warning: None,
|
||||
};
|
||||
|
||||
collector
|
||||
.data_source
|
||||
.expect_read_site_from_instance_id()
|
||||
.return_once(move |_| Ok(Some(site)));
|
||||
|
||||
let activity = SentActivity {
|
||||
id: ActivityId(1),
|
||||
ap_id: Url::parse("https://example.com/activities/1")
|
||||
.unwrap()
|
||||
.into(),
|
||||
data: json!({}),
|
||||
sensitive: false,
|
||||
published: Utc::now(),
|
||||
send_inboxes: vec![],
|
||||
send_community_followers_of: None,
|
||||
send_all_instances: true,
|
||||
actor_type: ActorType::Person,
|
||||
actor_apub_id: None,
|
||||
};
|
||||
|
||||
let result = collector.get_inbox_urls(&activity).await.unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0], site_inbox);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_inbox_urls_community_followers() {
|
||||
let mut collector = setup_collector();
|
||||
let community_id = CommunityId(1);
|
||||
let url1 = "https://follower1.example.com/inbox";
|
||||
let url2 = "https://follower2.example.com/inbox";
|
||||
|
||||
collector
|
||||
.data_source
|
||||
.expect_get_instance_followed_community_inboxes()
|
||||
.return_once(move |_, _| {
|
||||
Ok(vec![
|
||||
(community_id, Url::parse(url1).unwrap().into()),
|
||||
(community_id, Url::parse(url2).unwrap().into()),
|
||||
])
|
||||
});
|
||||
|
||||
collector.update_communities().await.unwrap();
|
||||
|
||||
let activity = SentActivity {
|
||||
id: ActivityId(1),
|
||||
ap_id: Url::parse("https://example.com/activities/1")
|
||||
.unwrap()
|
||||
.into(),
|
||||
data: json!({}),
|
||||
sensitive: false,
|
||||
published: Utc::now(),
|
||||
send_inboxes: vec![],
|
||||
send_community_followers_of: Some(community_id),
|
||||
send_all_instances: false,
|
||||
actor_type: ActorType::Person,
|
||||
actor_apub_id: None,
|
||||
};
|
||||
|
||||
let result = collector.get_inbox_urls(&activity).await.unwrap();
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.contains(&Url::parse(url1).unwrap()));
|
||||
assert!(result.contains(&Url::parse(url2).unwrap()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_inbox_urls_send_inboxes() {
|
||||
let mut collector = setup_collector();
|
||||
collector.domain = "example.com".to_string();
|
||||
let inbox_user_1 = Url::parse("https://example.com/user1/inbox").unwrap();
|
||||
let inbox_user_2 = Url::parse("https://example.com/user2/inbox").unwrap();
|
||||
let other_domain_inbox = Url::parse("https://other-domain.com/user3/inbox").unwrap();
|
||||
let activity = SentActivity {
|
||||
id: ActivityId(1),
|
||||
ap_id: Url::parse("https://example.com/activities/1")
|
||||
.unwrap()
|
||||
.into(),
|
||||
data: json!({}),
|
||||
sensitive: false,
|
||||
published: Utc::now(),
|
||||
send_inboxes: vec![
|
||||
Some(inbox_user_1.clone().into()),
|
||||
Some(inbox_user_2.clone().into()),
|
||||
Some(other_domain_inbox.clone().into()),
|
||||
],
|
||||
send_community_followers_of: None,
|
||||
send_all_instances: false,
|
||||
actor_type: ActorType::Person,
|
||||
actor_apub_id: None,
|
||||
};
|
||||
|
||||
let result = collector.get_inbox_urls(&activity).await.unwrap();
|
||||
assert_eq!(result.len(), 2);
|
||||
assert!(result.contains(&inbox_user_1));
|
||||
assert!(result.contains(&inbox_user_2));
|
||||
assert!(!result.contains(&other_domain_inbox));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_inbox_urls_combined() {
|
||||
let mut collector = setup_collector();
|
||||
collector.domain = "example.com".to_string();
|
||||
let community_id = CommunityId(1);
|
||||
|
||||
let site_inbox = Url::parse("https://example.com/site_inbox").unwrap();
|
||||
let site = Site {
|
||||
id: SiteId(1),
|
||||
name: "Test Site".to_string(),
|
||||
sidebar: None,
|
||||
published: Utc::now(),
|
||||
updated: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
description: None,
|
||||
actor_id: Url::parse("https://example.com/site").unwrap().into(),
|
||||
last_refreshed_at: Utc::now(),
|
||||
inbox_url: site_inbox.clone().into(),
|
||||
private_key: None,
|
||||
public_key: "test_key".to_string(),
|
||||
instance_id: InstanceId(1),
|
||||
content_warning: None,
|
||||
};
|
||||
|
||||
collector
|
||||
.data_source
|
||||
.expect_read_site_from_instance_id()
|
||||
.return_once(move |_| Ok(Some(site)));
|
||||
|
||||
let subdomain_inbox = "https://follower.example.com/inbox";
|
||||
collector
|
||||
.data_source
|
||||
.expect_get_instance_followed_community_inboxes()
|
||||
.return_once(move |_, _| {
|
||||
Ok(vec![(
|
||||
community_id,
|
||||
Url::parse(subdomain_inbox).unwrap().into(),
|
||||
)])
|
||||
});
|
||||
|
||||
collector.update_communities().await.unwrap();
|
||||
let user1_inbox = Url::parse("https://example.com/user1/inbox").unwrap();
|
||||
let user2_inbox = Url::parse("https://other-domain.com/user2/inbox").unwrap();
|
||||
let activity = SentActivity {
|
||||
id: ActivityId(1),
|
||||
ap_id: Url::parse("https://example.com/activities/1")
|
||||
.unwrap()
|
||||
.into(),
|
||||
data: json!({}),
|
||||
sensitive: false,
|
||||
published: Utc::now(),
|
||||
send_inboxes: vec![
|
||||
Some(user1_inbox.clone().into()),
|
||||
Some(user2_inbox.clone().into()),
|
||||
],
|
||||
send_community_followers_of: Some(community_id),
|
||||
send_all_instances: true,
|
||||
actor_type: ActorType::Person,
|
||||
actor_apub_id: None,
|
||||
};
|
||||
|
||||
let result = collector.get_inbox_urls(&activity).await.unwrap();
|
||||
assert_eq!(result.len(), 3);
|
||||
assert!(result.contains(&site_inbox));
|
||||
assert!(result.contains(&Url::parse(subdomain_inbox).unwrap()));
|
||||
assert!(result.contains(&user1_inbox));
|
||||
assert!(!result.contains(&user2_inbox));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_communities() {
|
||||
let mut collector = setup_collector();
|
||||
let community_id1 = CommunityId(1);
|
||||
let community_id2 = CommunityId(2);
|
||||
let community_id3 = CommunityId(3);
|
||||
|
||||
let user1_inbox_str = "https://follower1.example.com/inbox";
|
||||
let user1_inbox = Url::parse(user1_inbox_str).unwrap();
|
||||
let user2_inbox_str = "https://follower2.example.com/inbox";
|
||||
let user2_inbox = Url::parse(user2_inbox_str).unwrap();
|
||||
let user3_inbox_str = "https://follower3.example.com/inbox";
|
||||
let user3_inbox = Url::parse(user3_inbox_str).unwrap();
|
||||
|
||||
collector
|
||||
.data_source
|
||||
.expect_get_instance_followed_community_inboxes()
|
||||
.times(2)
|
||||
.returning(move |_, last_fetch| {
|
||||
if last_fetch == Utc.timestamp_nanos(0) {
|
||||
Ok(vec![
|
||||
(community_id1, Url::parse(user1_inbox_str).unwrap().into()),
|
||||
(community_id2, Url::parse(user2_inbox_str).unwrap().into()),
|
||||
])
|
||||
} else {
|
||||
Ok(vec![(
|
||||
community_id3,
|
||||
Url::parse(user3_inbox_str).unwrap().into(),
|
||||
)])
|
||||
}
|
||||
});
|
||||
|
||||
// First update
|
||||
collector.update_communities().await.unwrap();
|
||||
assert_eq!(collector.followed_communities.len(), 2);
|
||||
assert!(collector.followed_communities[&community_id1].contains(&user1_inbox));
|
||||
assert!(collector.followed_communities[&community_id2].contains(&user2_inbox));
|
||||
|
||||
// Simulate time passing
|
||||
collector.last_full_communities_fetch = Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap();
|
||||
collector.last_incremental_communities_fetch =
|
||||
Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap();
|
||||
|
||||
// Second update (incremental)
|
||||
collector.update_communities().await.unwrap();
|
||||
assert_eq!(collector.followed_communities.len(), 3);
|
||||
assert!(collector.followed_communities[&community_id1].contains(&user1_inbox));
|
||||
assert!(collector.followed_communities[&community_id3].contains(&user3_inbox));
|
||||
assert!(collector.followed_communities[&community_id2].contains(&user2_inbox));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_inbox_urls_no_duplicates() {
|
||||
let mut collector = setup_collector();
|
||||
collector.domain = "example.com".to_string();
|
||||
let community_id = CommunityId(1);
|
||||
let site_inbox = Url::parse("https://example.com/site_inbox").unwrap();
|
||||
let site_inbox_clone = site_inbox.clone();
|
||||
let site = Site {
|
||||
id: SiteId(1),
|
||||
name: "Test Site".to_string(),
|
||||
sidebar: None,
|
||||
published: Utc::now(),
|
||||
updated: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
description: None,
|
||||
actor_id: Url::parse("https://example.com/site").unwrap().into(),
|
||||
last_refreshed_at: Utc::now(),
|
||||
inbox_url: site_inbox.clone().into(),
|
||||
private_key: None,
|
||||
public_key: "test_key".to_string(),
|
||||
instance_id: InstanceId(1),
|
||||
content_warning: None,
|
||||
};
|
||||
|
||||
collector
|
||||
.data_source
|
||||
.expect_read_site_from_instance_id()
|
||||
.return_once(move |_| Ok(Some(site)));
|
||||
|
||||
collector
|
||||
.data_source
|
||||
.expect_get_instance_followed_community_inboxes()
|
||||
.return_once(move |_, _| Ok(vec![(community_id, site_inbox_clone.into())]));
|
||||
|
||||
collector.update_communities().await.unwrap();
|
||||
|
||||
let activity = SentActivity {
|
||||
id: ActivityId(1),
|
||||
ap_id: Url::parse("https://example.com/activities/1")
|
||||
.unwrap()
|
||||
.into(),
|
||||
data: json!({}),
|
||||
sensitive: false,
|
||||
published: Utc::now(),
|
||||
send_inboxes: vec![Some(site_inbox.into())],
|
||||
send_community_followers_of: Some(community_id),
|
||||
send_all_instances: true,
|
||||
actor_type: ActorType::Person,
|
||||
actor_apub_id: None,
|
||||
};
|
||||
|
||||
let result = collector.get_inbox_urls(&activity).await.unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.contains(&Url::parse("https://example.com/site_inbox").unwrap()));
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
use crate::{util::CancellableTask, worker::InstanceWorker};
|
||||
use activitypub_federation::config::FederationConfig;
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
lemmy_utils::settings::structs::FederationWorkerConfig,
|
||||
};
|
||||
use lemmy_db_schema::{newtypes::InstanceId, source::instance::Instance};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
use stats::receive_print_stats;
|
||||
|
@ -14,6 +17,8 @@ use tokio_util::sync::CancellationToken;
|
|||
use tracing::info;
|
||||
use util::FederationQueueStateWithDomain;
|
||||
|
||||
mod inboxes;
|
||||
mod send;
|
||||
mod stats;
|
||||
mod util;
|
||||
mod worker;
|
||||
|
@ -38,10 +43,15 @@ pub struct SendManager {
|
|||
context: FederationConfig<LemmyContext>,
|
||||
stats_sender: UnboundedSender<FederationQueueStateWithDomain>,
|
||||
exit_print: JoinHandle<()>,
|
||||
federation_worker_config: FederationWorkerConfig,
|
||||
}
|
||||
|
||||
impl SendManager {
|
||||
fn new(opts: Opts, context: FederationConfig<LemmyContext>) -> Self {
|
||||
fn new(
|
||||
opts: Opts,
|
||||
context: FederationConfig<LemmyContext>,
|
||||
federation_worker_config: FederationWorkerConfig,
|
||||
) -> Self {
|
||||
assert!(opts.process_count > 0);
|
||||
assert!(opts.process_index > 0);
|
||||
assert!(opts.process_index <= opts.process_count);
|
||||
|
@ -56,14 +66,20 @@ impl SendManager {
|
|||
stats_receiver,
|
||||
)),
|
||||
context,
|
||||
federation_worker_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(opts: Opts, context: FederationConfig<LemmyContext>) -> CancellableTask {
|
||||
pub fn run(
|
||||
opts: Opts,
|
||||
context: FederationConfig<LemmyContext>,
|
||||
config: FederationWorkerConfig,
|
||||
) -> CancellableTask {
|
||||
CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |cancel| {
|
||||
let opts = opts.clone();
|
||||
let config = config.clone();
|
||||
let context = context.clone();
|
||||
let mut manager = Self::new(opts, context);
|
||||
let mut manager = Self::new(opts, context, config);
|
||||
async move {
|
||||
let result = manager.do_loop(cancel).await;
|
||||
// the loop function will only return if there is (a) an internal error (e.g. db connection
|
||||
|
@ -120,22 +136,21 @@ impl SendManager {
|
|||
// create new worker
|
||||
let context = self.context.clone();
|
||||
let stats_sender = self.stats_sender.clone();
|
||||
let federation_worker_config = self.federation_worker_config.clone();
|
||||
|
||||
self.workers.insert(
|
||||
instance.id,
|
||||
CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| {
|
||||
// if the instance worker ends unexpectedly due to internal/db errors, this lambda is rerun by cancellabletask.
|
||||
// if the instance worker ends unexpectedly due to internal/db errors, this lambda is
|
||||
// rerun by cancellabletask.
|
||||
let instance = instance.clone();
|
||||
let req_data = context.to_request_data();
|
||||
let stats_sender = stats_sender.clone();
|
||||
async move {
|
||||
InstanceWorker::init_and_loop(
|
||||
instance,
|
||||
req_data,
|
||||
stop,
|
||||
stats_sender,
|
||||
)
|
||||
.await
|
||||
}
|
||||
InstanceWorker::init_and_loop(
|
||||
instance,
|
||||
context.clone(),
|
||||
federation_worker_config.clone(),
|
||||
stop,
|
||||
stats_sender.clone(),
|
||||
)
|
||||
}),
|
||||
);
|
||||
} else if !should_federate {
|
||||
|
@ -214,7 +229,14 @@ mod test {
|
|||
.app_data(context.clone())
|
||||
.build()
|
||||
.await?;
|
||||
let concurrent_sends_per_instance = std::env::var("LEMMY_TEST_FEDERATION_CONCURRENT_SENDS")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(1);
|
||||
|
||||
let federation_worker_config = FederationWorkerConfig {
|
||||
concurrent_sends_per_instance,
|
||||
};
|
||||
let pool = &mut context.pool();
|
||||
let instances = vec![
|
||||
Instance::read_or_create(pool, "alpha.com".to_string()).await?,
|
||||
|
@ -222,7 +244,7 @@ mod test {
|
|||
Instance::read_or_create(pool, "gamma.com".to_string()).await?,
|
||||
];
|
||||
|
||||
let send_manager = SendManager::new(opts, federation_config);
|
||||
let send_manager = SendManager::new(opts, federation_config, federation_worker_config);
|
||||
Ok(Self {
|
||||
send_manager,
|
||||
context,
|
||||
|
|
148
crates/federate/src/send.rs
Normal file
148
crates/federate/src/send.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use crate::util::get_actor_cached;
|
||||
use activitypub_federation::{
|
||||
activity_sending::SendActivityTask,
|
||||
config::Data,
|
||||
protocol::context::WithContext,
|
||||
};
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use lemmy_api_common::{context::LemmyContext, federate_retry_sleep_duration};
|
||||
use lemmy_apub::{activity_lists::SharedInboxActivities, FEDERATION_CONTEXT};
|
||||
use lemmy_db_schema::{newtypes::ActivityId, source::activity::SentActivity};
|
||||
use reqwest::Url;
|
||||
use std::ops::Deref;
|
||||
use tokio::{sync::mpsc::UnboundedSender, time::sleep};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(Debug, Eq)]
|
||||
pub(crate) struct SendSuccessInfo {
|
||||
pub activity_id: ActivityId,
|
||||
pub published: Option<DateTime<Utc>>,
|
||||
// true if the activity was skipped because the target instance is not interested in this
|
||||
// activity
|
||||
pub was_skipped: bool,
|
||||
}
|
||||
impl PartialEq for SendSuccessInfo {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.activity_id == other.activity_id
|
||||
}
|
||||
}
|
||||
/// order backwards because the binary heap is a max heap, and we need the smallest element to be on
|
||||
/// top
|
||||
impl PartialOrd for SendSuccessInfo {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
impl Ord for SendSuccessInfo {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
other.activity_id.cmp(&self.activity_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the result of sending an activity.
|
||||
///
|
||||
/// This enum is used to communicate the outcome of a send operation from a send task
|
||||
/// to the main instance worker. It's designed to maintain a clean separation between
|
||||
/// the send task and the main thread, allowing the send.rs file to be self-contained
|
||||
/// and easier to understand.
|
||||
///
|
||||
/// The use of a channel for communication (rather than shared atomic variables) was chosen
|
||||
/// because:
|
||||
/// 1. It keeps the send task cleanly separated with no direct interaction with the main thread.
|
||||
/// 2. The failure event needs to be transferred to the main task for database updates anyway.
|
||||
/// 3. The main fail_count should only be updated under certain conditions, which are best handled
|
||||
/// in the main task.
|
||||
/// 4. It maintains consistency in how data is communicated (all via channels rather than a mix of
|
||||
/// channels and atomics).
|
||||
/// 5. It simplifies concurrency management and makes the flow of data more predictable.
|
||||
pub(crate) enum SendActivityResult {
|
||||
Success(SendSuccessInfo),
|
||||
Failure { fail_count: i32 },
|
||||
}
|
||||
/// Represents a task for retrying to send an activity.
|
||||
///
|
||||
/// This struct encapsulates all the necessary information and resources for attempting
|
||||
/// to send an activity to multiple inbox URLs, with built-in retry logic.
|
||||
pub(crate) struct SendRetryTask<'a> {
|
||||
pub activity: &'a SentActivity,
|
||||
pub object: &'a SharedInboxActivities,
|
||||
/// Must not be empty at this point
|
||||
pub inbox_urls: Vec<Url>,
|
||||
/// Channel to report results back to the main instance worker
|
||||
pub report: &'a mut UnboundedSender<SendActivityResult>,
|
||||
/// The first request will be sent immediately, but subsequent requests will be delayed
|
||||
/// according to the number of previous fails + 1
|
||||
///
|
||||
/// This is a read-only immutable variable that is passed only one way, from the main
|
||||
/// thread to each send task. It allows the task to determine how long to sleep initially
|
||||
/// if the request fails.
|
||||
pub initial_fail_count: i32,
|
||||
/// For logging purposes
|
||||
pub domain: String,
|
||||
pub context: Data<LemmyContext>,
|
||||
pub stop: CancellationToken,
|
||||
}
|
||||
|
||||
impl<'a> SendRetryTask<'a> {
|
||||
// this function will return successfully when (a) send succeeded or (b) worker cancelled
|
||||
// and will return an error if an internal error occurred (send errors cause an infinite loop)
|
||||
pub async fn send_retry_loop(self) -> Result<()> {
|
||||
let SendRetryTask {
|
||||
activity,
|
||||
object,
|
||||
inbox_urls,
|
||||
report,
|
||||
initial_fail_count,
|
||||
domain,
|
||||
context,
|
||||
stop,
|
||||
} = self;
|
||||
debug_assert!(!inbox_urls.is_empty());
|
||||
|
||||
let pool = &mut context.pool();
|
||||
let Some(actor_apub_id) = &activity.actor_apub_id else {
|
||||
return Err(anyhow::anyhow!("activity is from before lemmy 0.19"));
|
||||
};
|
||||
let actor = get_actor_cached(pool, activity.actor_type, actor_apub_id)
|
||||
.await
|
||||
.context("failed getting actor instance (was it marked deleted / removed?)")?;
|
||||
|
||||
let object = WithContext::new(object.clone(), FEDERATION_CONTEXT.deref().clone());
|
||||
let requests = SendActivityTask::prepare(&object, actor.as_ref(), inbox_urls, &context).await?;
|
||||
for task in requests {
|
||||
// usually only one due to shared inbox
|
||||
tracing::debug!("sending out {}", task);
|
||||
let mut fail_count = initial_fail_count;
|
||||
while let Err(e) = task.sign_and_send(&context).await {
|
||||
fail_count += 1;
|
||||
report.send(SendActivityResult::Failure {
|
||||
fail_count,
|
||||
// activity_id: activity.id,
|
||||
})?;
|
||||
let retry_delay = federate_retry_sleep_duration(fail_count);
|
||||
tracing::info!(
|
||||
"{}: retrying {:?} attempt {} with delay {retry_delay:.2?}. ({e})",
|
||||
domain,
|
||||
activity.id,
|
||||
fail_count
|
||||
);
|
||||
tokio::select! {
|
||||
() = sleep(retry_delay) => {},
|
||||
() = stop.cancelled() => {
|
||||
// cancel sending without reporting any result.
|
||||
// the InstanceWorker needs to be careful to not hang on receive of that
|
||||
// channel when cancelled (see handle_send_results)
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
report.send(SendActivityResult::Success(SendSuccessInfo {
|
||||
activity_id: activity.id,
|
||||
published: Some(activity.published),
|
||||
was_skipped: false,
|
||||
}))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
use anyhow::{anyhow, Context, Result};
|
||||
use diesel::prelude::*;
|
||||
use diesel_async::RunQueryDsl;
|
||||
use lemmy_api_common::lemmy_utils::CACHE_DURATION_FEDERATION;
|
||||
use lemmy_apub::{
|
||||
activity_lists::SharedInboxActivities,
|
||||
fetcher::{site_or_community_or_user::SiteOrCommunityOrUser, user_or_community::UserOrCommunity},
|
||||
|
@ -28,19 +27,28 @@ use tokio_util::sync::CancellationToken;
|
|||
|
||||
/// Decrease the delays of the federation queue.
|
||||
/// Should only be used for federation tests since it significantly increases CPU and DB load of the
|
||||
/// federation queue.
|
||||
/// federation queue. This is intentionally a separate flag from other flags like debug_assertions,
|
||||
/// since this is a invasive change we only need rarely.
|
||||
pub(crate) static LEMMY_TEST_FAST_FEDERATION: Lazy<bool> = Lazy::new(|| {
|
||||
std::env::var("LEMMY_TEST_FAST_FEDERATION")
|
||||
.map(|s| !s.is_empty())
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
/// Recheck for new federation work every n seconds.
|
||||
/// Recheck for new federation work every n seconds within each InstanceWorker.
|
||||
///
|
||||
/// When the queue is processed faster than new activities are added and it reaches the current time
|
||||
/// with an empty batch, this is the delay the queue waits before it checks if new activities have
|
||||
/// been added to the sent_activities table. This delay is only applied if no federated activity
|
||||
/// happens during sending activities of the last batch.
|
||||
/// happens during sending activities of the last batch, which means on high-activity instances it
|
||||
/// may never be used. This means that it does not affect the maximum throughput of the queue.
|
||||
///
|
||||
///
|
||||
/// This is thus the interval with which tokio wakes up each of the
|
||||
/// InstanceWorkers to check for new work, if the queue previously was empty.
|
||||
/// If the delay is too short, the workers (one per federated instance) will wake up too
|
||||
/// often and consume a lot of CPU. If the delay is long, then activities on low-traffic instances
|
||||
/// will on average take delay/2 seconds to federate.
|
||||
pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy<Duration> = Lazy::new(|| {
|
||||
if *LEMMY_TEST_FAST_FEDERATION {
|
||||
Duration::from_millis(100)
|
||||
|
@ -49,6 +57,21 @@ pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy<Duration> = Lazy::new(|| {
|
|||
}
|
||||
});
|
||||
|
||||
/// Cache the latest activity id for a certain duration.
|
||||
///
|
||||
/// This cache is common to all the instance workers and prevents there from being more than one
|
||||
/// call per N seconds between each DB query to find max(activity_id).
|
||||
pub(crate) static CACHE_DURATION_LATEST_ID: Lazy<Duration> = Lazy::new(|| {
|
||||
if *LEMMY_TEST_FAST_FEDERATION {
|
||||
// in test mode, we use the same cache duration as the recheck delay so when recheck happens
|
||||
// data is fresh, accelerating the time the tests take.
|
||||
*WORK_FINISHED_RECHECK_DELAY
|
||||
} else {
|
||||
// in normal mode, we limit the query to one per second
|
||||
Duration::from_secs(1)
|
||||
}
|
||||
});
|
||||
|
||||
/// A task that will be run in an infinite loop, unless it is cancelled.
|
||||
/// If the task exits without being cancelled, an error will be logged and the task will be
|
||||
/// restarted.
|
||||
|
@ -174,7 +197,7 @@ pub(crate) async fn get_activity_cached(
|
|||
pub(crate) async fn get_latest_activity_id(pool: &mut DbPool<'_>) -> Result<ActivityId> {
|
||||
static CACHE: Lazy<Cache<(), ActivityId>> = Lazy::new(|| {
|
||||
Cache::builder()
|
||||
.time_to_live(CACHE_DURATION_FEDERATION)
|
||||
.time_to_live(*CACHE_DURATION_LATEST_ID)
|
||||
.build()
|
||||
});
|
||||
CACHE
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -41,12 +41,66 @@ pub fn config(
|
|||
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
trait ProcessUrl {
|
||||
/// If thumbnail or format is given, this uses the pictrs process endpoint.
|
||||
/// Otherwise, it uses the normal pictrs url (IE image/original).
|
||||
fn process_url(&self, image_url: &str, pictrs_url: &Url) -> String;
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
struct PictrsGetParams {
|
||||
format: Option<String>,
|
||||
thumbnail: Option<i32>,
|
||||
}
|
||||
|
||||
impl ProcessUrl for PictrsGetParams {
|
||||
fn process_url(&self, src: &str, pictrs_url: &Url) -> String {
|
||||
if self.format.is_none() && self.thumbnail.is_none() {
|
||||
format!("{}image/original/{}", pictrs_url, src)
|
||||
} else {
|
||||
// Take file type from name, or jpg if nothing is given
|
||||
let format = self
|
||||
.clone()
|
||||
.format
|
||||
.unwrap_or_else(|| src.split('.').last().unwrap_or("jpg").to_string());
|
||||
|
||||
let mut url = format!("{}image/process.{}?src={}", pictrs_url, format, src);
|
||||
|
||||
if let Some(size) = self.thumbnail {
|
||||
url = format!("{url}&thumbnail={size}",);
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct ImageProxyParams {
|
||||
url: String,
|
||||
format: Option<String>,
|
||||
thumbnail: Option<i32>,
|
||||
}
|
||||
|
||||
impl ProcessUrl for ImageProxyParams {
|
||||
fn process_url(&self, proxy_url: &str, pictrs_url: &Url) -> String {
|
||||
if self.format.is_none() && self.thumbnail.is_none() {
|
||||
format!("{}image/original?proxy={}", pictrs_url, proxy_url)
|
||||
} else {
|
||||
// Take file type from name, or jpg if nothing is given
|
||||
let format = self
|
||||
.clone()
|
||||
.format
|
||||
.unwrap_or_else(|| proxy_url.split('.').last().unwrap_or("jpg").to_string());
|
||||
|
||||
let mut url = format!("{}image/process.{}?proxy={}", pictrs_url, format, proxy_url);
|
||||
|
||||
if let Some(size) = self.thumbnail {
|
||||
url = format!("{url}&thumbnail={size}",);
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
fn adapt_request(
|
||||
request: &HttpRequest,
|
||||
client: &ClientWithMiddleware,
|
||||
|
@ -133,23 +187,10 @@ async fn full_res(
|
|||
|
||||
// If there are no query params, the URL is original
|
||||
let pictrs_config = context.settings().pictrs_config()?;
|
||||
let url = if params.format.is_none() && params.thumbnail.is_none() {
|
||||
format!("{}image/original/{}", pictrs_config.url, name,)
|
||||
} else {
|
||||
// Take file type from name, or jpg if nothing is given
|
||||
let format = params
|
||||
.format
|
||||
.unwrap_or_else(|| name.split('.').last().unwrap_or("jpg").to_string());
|
||||
|
||||
let mut url = format!("{}image/process.{}?src={}", pictrs_config.url, format, name,);
|
||||
let processed_url = params.process_url(name, &pictrs_config.url);
|
||||
|
||||
if let Some(size) = params.thumbnail {
|
||||
url = format!("{url}&thumbnail={size}",);
|
||||
}
|
||||
url
|
||||
};
|
||||
|
||||
image(url, req, &client).await
|
||||
image(processed_url, req, &client).await
|
||||
}
|
||||
|
||||
async fn image(
|
||||
|
@ -208,11 +249,6 @@ async fn delete(
|
|||
Ok(HttpResponse::build(res.status()).body(BodyStream::new(res.bytes_stream())))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ImageProxyParams {
|
||||
url: String,
|
||||
}
|
||||
|
||||
pub async fn image_proxy(
|
||||
Query(params): Query<ImageProxyParams>,
|
||||
req: HttpRequest,
|
||||
|
@ -226,9 +262,10 @@ pub async fn image_proxy(
|
|||
RemoteImage::validate(&mut context.pool(), url.clone().into()).await?;
|
||||
|
||||
let pictrs_config = context.settings().pictrs_config()?;
|
||||
let url = format!("{}image/original?proxy={}", pictrs_config.url, ¶ms.url);
|
||||
|
||||
image(url, req, &client).await
|
||||
let processed_url = params.process_url(¶ms.url, &pictrs_config.url);
|
||||
|
||||
image(processed_url, req, &client).await
|
||||
}
|
||||
|
||||
fn make_send<S>(mut stream: S) -> impl Stream<Item = S::Item> + Send + Unpin + 'static
|
||||
|
|
|
@ -44,7 +44,6 @@ full = [
|
|||
"dep:enum-map",
|
||||
"dep:futures",
|
||||
"dep:tokio",
|
||||
"dep:openssl",
|
||||
"dep:html2text",
|
||||
"dep:lettre",
|
||||
"dep:uuid",
|
||||
|
@ -65,7 +64,6 @@ actix-web = { workspace = true, optional = true }
|
|||
anyhow = { workspace = true, optional = true }
|
||||
reqwest-middleware = { workspace = true, optional = true }
|
||||
strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
futures = { workspace = true, optional = true }
|
||||
diesel = { workspace = true, features = ["chrono"], optional = true }
|
||||
http = { workspace = true, optional = true }
|
||||
|
@ -74,13 +72,14 @@ uuid = { workspace = true, features = ["serde", "v4"], optional = true }
|
|||
rosetta-i18n = { workspace = true, optional = true }
|
||||
tokio = { workspace = true, optional = true }
|
||||
urlencoding = { workspace = true, optional = true }
|
||||
openssl = { version = "0.10.64", optional = true }
|
||||
html2text = { version = "0.12.5", optional = true }
|
||||
deser-hjson = { version = "2.2.4", optional = true }
|
||||
smart-default = { version = "0.7.1", optional = true }
|
||||
lettre = { version = "0.11.7", features = [
|
||||
lettre = { version = "0.11.7", default-features = false, features = [
|
||||
"builder",
|
||||
"tokio1",
|
||||
"tokio1-native-tls",
|
||||
"tokio1-rustls-tls",
|
||||
"smtp-transport",
|
||||
], optional = true }
|
||||
markdown-it = { version = "0.6.0", optional = true }
|
||||
ts-rs = { workspace = true, optional = true }
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
use openssl::{pkey::PKey, rsa::Rsa};
|
||||
use std::io::{Error, ErrorKind};
|
||||
|
||||
pub struct Keypair {
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
}
|
||||
|
||||
/// Generate the asymmetric keypair for ActivityPub HTTP signatures.
|
||||
pub fn generate_actor_keypair() -> Result<Keypair, Error> {
|
||||
let rsa = Rsa::generate(2048)?;
|
||||
let pkey = PKey::from_rsa(rsa)?;
|
||||
let public_key = pkey.public_key_to_pem()?;
|
||||
let private_key = pkey.private_key_to_pem_pkcs8()?;
|
||||
let key_to_string = |key| match String::from_utf8(key) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Failed converting key to string: {e}"),
|
||||
)),
|
||||
};
|
||||
Ok(Keypair {
|
||||
private_key: key_to_string(private_key)?,
|
||||
public_key: key_to_string(public_key)?,
|
||||
})
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
use cfg_if::cfg_if;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
use strum_macros::{Display, EnumIter};
|
||||
use strum::{Display, EnumIter};
|
||||
|
||||
#[derive(Display, Debug, Serialize, Deserialize, Clone, PartialEq, Eq, EnumIter, Hash)]
|
||||
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
|
||||
|
@ -38,6 +38,8 @@ pub enum LemmyErrorType {
|
|||
NotTopAdmin,
|
||||
NotTopMod,
|
||||
NotLoggedIn,
|
||||
NotHigherMod,
|
||||
NotHigherAdmin,
|
||||
SiteBan,
|
||||
Deleted,
|
||||
BannedFromCommunity,
|
||||
|
|
|
@ -2,7 +2,6 @@ use cfg_if::cfg_if;
|
|||
|
||||
cfg_if! {
|
||||
if #[cfg(feature = "full")] {
|
||||
pub mod apub;
|
||||
pub mod cache_header;
|
||||
pub mod email;
|
||||
pub mod rate_limit;
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
time::Instant,
|
||||
};
|
||||
use strum_macros::AsRefStr;
|
||||
use strum::{AsRefStr, Display};
|
||||
use tracing::debug;
|
||||
|
||||
static START_TIME: Lazy<Instant> = Lazy::new(Instant::now);
|
||||
|
@ -66,7 +66,7 @@ impl Bucket {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, enum_map::Enum, Copy, Clone, AsRefStr)]
|
||||
#[derive(Debug, enum_map::Enum, Copy, Clone, Display, AsRefStr)]
|
||||
pub enum ActionType {
|
||||
Message,
|
||||
Register,
|
||||
|
|
|
@ -43,12 +43,8 @@ pub struct Settings {
|
|||
#[default(None)]
|
||||
#[doku(skip)]
|
||||
pub opentelemetry_url: Option<Url>,
|
||||
/// The number of activitypub federation workers that can be in-flight concurrently
|
||||
#[default(0)]
|
||||
pub worker_count: usize,
|
||||
/// The number of activitypub federation retry workers that can be in-flight concurrently
|
||||
#[default(0)]
|
||||
pub retry_count: usize,
|
||||
#[default(Default::default())]
|
||||
pub federation: FederationWorkerConfig,
|
||||
// Prometheus configuration.
|
||||
#[default(None)]
|
||||
#[doku(example = "Some(Default::default())")]
|
||||
|
@ -237,3 +233,14 @@ pub struct PrometheusConfig {
|
|||
#[doku(example = "10002")]
|
||||
pub port: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document)]
|
||||
#[serde(default)]
|
||||
// named federation"worker"config to disambiguate from the activitypub library configuration
|
||||
pub struct FederationWorkerConfig {
|
||||
/// Limit to the number of concurrent outgoing federation requests per target instance.
|
||||
/// Set this to a higher value than 1 (e.g. 6) only if you have a huge instance (>10 activities
|
||||
/// per second) and if a receiving instance is not keeping up.
|
||||
#[default(1)]
|
||||
pub concurrent_sends_per_instance: i8,
|
||||
}
|
||||
|
|
|
@ -97,7 +97,7 @@ services:
|
|||
logging: *default-logging
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
image: pgautoupgrade/pgautoupgrade: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:16-alpine
|
||||
image: pgautoupgrade/pgautoupgrade:16-alpine
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=password
|
||||
|
|
|
@ -1,48 +1,8 @@
|
|||
#!/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
|
||||
sleep 20s
|
||||
|
||||
echo "Exporting the Database to 12_15.dump.sql ..."
|
||||
sudo docker-compose exec -T postgres pg_dumpall -c -U lemmy > 12_15_dump.sql
|
||||
echo "Done."
|
||||
|
||||
echo "Stopping postgres..."
|
||||
sudo docker-compose stop postgres
|
||||
sleep 20s
|
||||
|
||||
echo "Removing the old postgres folder"
|
||||
sudo rm -rf volumes/postgres
|
||||
|
||||
echo "Updating docker-compose to use postgres version 15."
|
||||
sed -i "s/image: postgres:.*/image: postgres:15-alpine/" ./docker-compose.yml
|
||||
|
||||
echo "Starting up new postgres..."
|
||||
sudo docker-compose up -d postgres
|
||||
sleep 20s
|
||||
|
||||
echo "Importing the database...."
|
||||
cat 12_15_dump.sql | sudo docker-compose exec -T postgres psql -U lemmy
|
||||
echo "Done."
|
||||
|
||||
POSTGRES_PASSWORD=$(grep "POSTGRES_PASSWORD" ./docker-compose.yml | cut -d"=" -f2)
|
||||
|
||||
echo "Fixing a weird password issue with postgres 15"
|
||||
sudo docker-compose exec -T postgres psql -U lemmy -c "alter user lemmy with password '$POSTGRES_PASSWORD'"
|
||||
sudo docker-compose restart postgres
|
||||
|
||||
echo "Setting correct perms for pictrs folder"
|
||||
sudo chown -R 991:991 volumes/pictrs
|
||||
echo "Updating docker-compose to use postgres version 16."
|
||||
sudo sed -i "s/image: .*postgres:.*/image: pgautoupgrade\/pgautoupgrade:16-alpine/" ./docker-compose.yml
|
||||
|
||||
echo "Starting up lemmy..."
|
||||
sudo docker-compose up -d
|
||||
|
||||
echo "A copy of your old database is at 12_15.dump.sql . You can delete this file if the upgrade went smoothly."
|
||||
echo "Upgrade complete."
|
||||
|
|
|
@ -1,42 +1,8 @@
|
|||
#!/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 | sudo tee 15_16_dump.sql > /dev/null
|
||||
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."
|
||||
sudo sed -i "s/image: .*postgres:.*/image: docker.io\/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...."
|
||||
sudo cat 15_16_dump.sql | sudo docker compose exec -T postgres psql -U lemmy
|
||||
echo "Done."
|
||||
echo "Updating docker-compose to use postgres version 16."
|
||||
sudo sed -i "s/image: .*postgres:.*/image: pgautoupgrade\/pgautoupgrade:16-alpine/" ./docker-compose.yml
|
||||
|
||||
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."
|
||||
sudo docker-compose up -d
|
||||
|
|
|
@ -14,6 +14,7 @@ source scripts/start_dev_db.sh
|
|||
# so to load the config we need to traverse to the repo root
|
||||
export LEMMY_CONFIG_LOCATION=../../config/config.hjson
|
||||
export RUST_BACKTRACE=1
|
||||
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
|
||||
|
||||
if [ -n "$PACKAGE" ];
|
||||
then
|
||||
|
|
|
@ -82,6 +82,7 @@ use lemmy_api::{
|
|||
},
|
||||
registration_applications::{
|
||||
approve::approve_registration_application,
|
||||
get::get_registration_application,
|
||||
list::list_registration_applications,
|
||||
unread_count::get_unread_registration_application_count,
|
||||
},
|
||||
|
@ -361,6 +362,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
|
|||
"/registration_application/approve",
|
||||
web::put().to(approve_registration_application),
|
||||
)
|
||||
.route(
|
||||
"/registration_application",
|
||||
web::get().to(get_registration_application),
|
||||
)
|
||||
.route("/list_all_media", web::get().to(list_all_media))
|
||||
.service(
|
||||
web::scope("/purge")
|
||||
|
|
|
@ -468,12 +468,11 @@ async fn initialize_local_site_2022_10_10(
|
|||
};
|
||||
let person_inserted = Person::create(pool, &person_form).await?;
|
||||
|
||||
let local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(person_inserted.id)
|
||||
.password_encrypted(setup.admin_password.clone())
|
||||
.email(setup.admin_email.clone())
|
||||
.admin(Some(true))
|
||||
.build();
|
||||
let local_user_form = LocalUserInsertForm {
|
||||
email: setup.admin_email.clone(),
|
||||
admin: Some(true),
|
||||
..LocalUserInsertForm::new(person_inserted.id, setup.admin_password.clone())
|
||||
};
|
||||
LocalUser::create(pool, &local_user_form, vec![]).await?;
|
||||
};
|
||||
|
||||
|
|
|
@ -276,6 +276,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> LemmyResult<()> {
|
|||
process_count: args.federate_process_count,
|
||||
},
|
||||
cfg,
|
||||
SETTINGS.federation.clone(),
|
||||
)
|
||||
});
|
||||
let mut interrupt = tokio::signal::unix::signal(SignalKind::interrupt())?;
|
||||
|
|
|
@ -2,11 +2,17 @@ use clap::Parser;
|
|||
use lemmy_server::{init_logging, start_lemmy_server, CmdArgs};
|
||||
use lemmy_utils::{error::LemmyResult, settings::SETTINGS};
|
||||
|
||||
pub extern crate rustls;
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() -> LemmyResult<()> {
|
||||
init_logging(&SETTINGS.opentelemetry_url)?;
|
||||
let args = CmdArgs::parse();
|
||||
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.expect("Failed to install rustls crypto provider");
|
||||
|
||||
#[cfg(not(feature = "embed-pictrs"))]
|
||||
start_lemmy_server(args).await?;
|
||||
#[cfg(feature = "embed-pictrs")]
|
||||
|
|
|
@ -559,9 +559,9 @@ mod tests {
|
|||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_nodeinfo_voyager_lemmy_ml() -> LemmyResult<()> {
|
||||
async fn test_nodeinfo_lemmy_ml() -> LemmyResult<()> {
|
||||
let client = ClientBuilder::new(client_builder(&Settings::default()).build()?).build();
|
||||
let form = build_update_instance_form("voyager.lemmy.ml", &client)
|
||||
let form = build_update_instance_form("lemmy.ml", &client)
|
||||
.await
|
||||
.ok_or(LemmyErrorType::CouldntFindObject)?;
|
||||
assert_eq!(
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue