Merge branch 'main' into reduce-pool-size

reduce-pool-size
Nutomic 5 months ago committed by GitHub
commit 4a0d3a63a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -262,6 +262,19 @@ steps:
when:
event: cron
# using https://github.com/pksunkara/cargo-workspaces
publish_to_crates_io:
image: *rust_image
commands:
- 'echo "pub const VERSION: &str = \"$(git describe --tag)\";" > "crates/utils/src/version.rs"'
- cargo install cargo-workspaces
- cp -r migrations crates/db_schema/
- cargo login "$CARGO_API_TOKEN"
- cargo workspaces publish --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
secrets: [cargo_api_token]
when:
event: tag
notify_on_failure:
image: alpine:3
commands:

24
Cargo.lock generated

@ -2523,7 +2523,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lemmy_api"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2551,7 +2551,7 @@ dependencies = [
[[package]]
name = "lemmy_api_common"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2585,7 +2585,7 @@ dependencies = [
[[package]]
name = "lemmy_api_crud"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2607,7 +2607,7 @@ dependencies = [
[[package]]
name = "lemmy_apub"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2646,7 +2646,7 @@ dependencies = [
[[package]]
name = "lemmy_db_schema"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"activitypub_federation",
"async-trait",
@ -2682,7 +2682,7 @@ dependencies = [
[[package]]
name = "lemmy_db_views"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"actix-web",
"chrono",
@ -2701,7 +2701,7 @@ dependencies = [
[[package]]
name = "lemmy_db_views_actor"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"chrono",
"diesel",
@ -2718,7 +2718,7 @@ dependencies = [
[[package]]
name = "lemmy_db_views_moderator"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"diesel",
"diesel-async",
@ -2730,7 +2730,7 @@ dependencies = [
[[package]]
name = "lemmy_federate"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"activitypub_federation",
"anyhow",
@ -2753,7 +2753,7 @@ dependencies = [
[[package]]
name = "lemmy_routes"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2777,7 +2777,7 @@ dependencies = [
[[package]]
name = "lemmy_server"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"activitypub_federation",
"actix-cors",
@ -2819,7 +2819,7 @@ dependencies = [
[[package]]
name = "lemmy_utils"
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
dependencies = [
"actix-web",
"anyhow",

@ -1,5 +1,6 @@
[workspace.package]
version = "0.19.1-rc.1"
version = "0.19.1-rc.2"
publish = false
edition = "2021"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
@ -84,16 +85,16 @@ unused_self = "deny"
unwrap_used = "deny"
[workspace.dependencies]
lemmy_api = { version = "=0.19.1-rc.1", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.1-rc.1", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.1-rc.1", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.1-rc.1", path = "./crates/utils" }
lemmy_db_schema = { version = "=0.19.1-rc.1", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.1-rc.1", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.1-rc.1", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.1-rc.1", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.1-rc.1", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.1-rc.1", path = "./crates/db_views_moderator" }
lemmy_api = { version = "=0.19.1-rc.2", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.1-rc.2", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.1-rc.2", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.1-rc.2", path = "./crates/utils" }
lemmy_db_schema = { version = "=0.19.1-rc.2", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.1-rc.2", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.1-rc.2", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.1-rc.2", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.1-rc.2", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.1-rc.2", path = "./crates/db_views_moderator" }
activitypub_federation = { version = "0.5.0-beta.6", default-features = false, features = [
"actix-web",
] }
@ -164,7 +165,7 @@ lemmy_utils = { workspace = true }
lemmy_db_schema = { workspace = true }
lemmy_api_common = { workspace = true }
lemmy_routes = { workspace = true }
lemmy_federate = { version = "0.19.1-rc.1", path = "crates/federate" }
lemmy_federate = { version = "0.19.1-rc.2", path = "crates/federate" }
activitypub_federation = { workspace = true }
diesel = { workspace = true }
diesel-async = { workspace = true }

@ -7,7 +7,7 @@
[![Translation status](http://weblate.join-lemmy.org/widgets/lemmy/-/lemmy/svg-badge.svg)](http://weblate.join-lemmy.org/engage/lemmy/)
[![License](https://img.shields.io/github/license/LemmyNet/lemmy.svg)](LICENSE)
![GitHub stars](https://img.shields.io/github/stars/LemmyNet/lemmy?style=social)
[![Delightful Humane Tech](https://codeberg.org/teaserbot-labs/delightful-humane-design/raw/branch/main/humane-tech-badge.svg)](https://codeberg.org/teaserbot-labs/delightful-humane-design)
<a href="https://endsoftwarepatents.org/innovating-without-patents"><img style="height: 20px;" src="https://static.fsf.org/nosvn/esp/logos/patent-free.svg"></a>
</div>

@ -112,18 +112,19 @@ test("Delete user", async () => {
).toBe(true);
});
test("Requests with invalid auth should be treated as unauthenticated", async () => {
test("Requests with invalid auth should throw error", async () => {
let invalid_auth = new LemmyHttp(alphaUrl, {
headers: { Authorization: "Bearer foobar" },
fetchFunction,
});
let site = await getSite(invalid_auth);
expect(site.my_user).toBeUndefined();
expect(site.site_view).toBeDefined();
await expect(getSite(invalid_auth)).rejects.toStrictEqual(
Error("incorrect_login"),
);
let form: GetPosts = {};
let posts = invalid_auth.getPosts(form);
expect((await posts).posts).toBeDefined();
await expect(invalid_auth.getPosts(form)).rejects.toStrictEqual(
Error("incorrect_login"),
);
});
test("Create user with Arabic name", async () => {

@ -1,5 +1,6 @@
[package]
name = "lemmy_api"
publish = false
version.workspace = true
edition.workspace = true
description.workspace = true

@ -0,0 +1,24 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
comment::{ListCommentLikes, ListCommentLikesResponse},
context::LemmyContext,
utils::is_admin,
};
use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::error::LemmyError;
/// Lists likes for a comment
#[tracing::instrument(skip(context))]
pub async fn list_comment_likes(
data: Query<ListCommentLikes>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<ListCommentLikesResponse>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;
let comment_likes =
VoteView::list_for_comment(&mut context.pool(), data.comment_id, data.page, data.limit).await?;
Ok(Json(ListCommentLikesResponse { comment_likes }))
}

@ -1,3 +1,4 @@
pub mod distinguish;
pub mod like;
pub mod list_comment_likes;
pub mod save;

@ -23,7 +23,7 @@ pub async fn resolve_comment_report(
check_community_mod_action(
&local_user_view.person,
report.community.id,
false,
true,
&mut context.pool(),
)
.await?;

@ -2,15 +2,11 @@ use actix_web::{http::header::Header, HttpRequest};
use actix_web_httpauth::headers::authorization::{Authorization, Bearer};
use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine};
use captcha::Captcha;
use lemmy_api_common::{
claims::Claims,
context::LemmyContext,
utils::{check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME},
};
use lemmy_api_common::utils::{local_site_to_slur_regex, AUTH_COOKIE_NAME};
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::slurs::check_slurs,
};
use std::io::Cursor;
@ -141,20 +137,6 @@ pub(crate) fn build_totp_2fa(
.with_lemmy_type(LemmyErrorType::CouldntGenerateTotp)
}
#[tracing::instrument(skip_all)]
pub async fn local_user_view_from_jwt(
jwt: &str,
context: &LemmyContext,
) -> Result<LocalUserView, LemmyError> {
let local_user_id = Claims::validate(jwt, context)
.await
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?;
check_user_valid(&local_user_view.person)?;
Ok(local_user_view)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]

@ -1,10 +1,10 @@
use crate::{local_user_view_from_jwt, read_auth_token};
use crate::read_auth_token;
use actix_web::{
web::{Data, Json},
HttpRequest,
};
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use lemmy_api_common::{claims::Claims, context::LemmyContext, SuccessResponse};
use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
/// Returns an error message if the auth token is invalid for any reason. Necessary because other
/// endpoints silently treat any call with invalid auth as unauthenticated.
@ -15,7 +15,9 @@ pub async fn validate_auth(
) -> Result<Json<SuccessResponse>, LemmyError> {
let jwt = read_auth_token(&req)?;
if let Some(jwt) = jwt {
local_user_view_from_jwt(&jwt, &context).await?;
Claims::validate(&jwt, &context)
.await
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
} else {
Err(LemmyErrorType::NotLoggedIn)?;
}

@ -0,0 +1,24 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{ListPostLikes, ListPostLikesResponse},
utils::is_admin,
};
use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::error::LemmyError;
/// Lists likes for a post
#[tracing::instrument(skip(context))]
pub async fn list_post_likes(
data: Query<ListPostLikes>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<ListPostLikesResponse>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;
let post_likes =
VoteView::list_for_post(&mut context.pool(), data.post_id, data.page, data.limit).await?;
Ok(Json(ListPostLikesResponse { post_likes }))
}

@ -1,6 +1,7 @@
pub mod feature;
pub mod get_link_metadata;
pub mod like;
pub mod list_post_likes;
pub mod lock;
pub mod mark_read;
pub mod save;

@ -23,7 +23,7 @@ pub async fn resolve_post_report(
check_community_mod_action(
&local_user_view.person,
report.community.id,
false,
true,
&mut context.pool(),
)
.await?;

@ -6,7 +6,7 @@ use lemmy_db_schema::{
newtypes::LocalUserId,
source::login_token::{LoginToken, LoginTokenCreateForm},
};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
@ -25,8 +25,7 @@ impl Claims {
validation.required_spec_claims.remove("exp");
let jwt_secret = &context.secret().jwt_secret;
let key = DecodingKey::from_secret(jwt_secret.as_ref());
let claims =
decode::<Claims>(jwt, &key, &validation).with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let claims = decode::<Claims>(jwt, &key, &validation)?;
let user_id = LocalUserId(claims.claims.sub.parse()?);
let is_valid = LoginToken::validate(&mut context.pool(), user_id, jwt).await?;
if !is_valid {

@ -3,7 +3,7 @@ use lemmy_db_schema::{
CommentSortType,
ListingType,
};
use lemmy_db_views::structs::{CommentReportView, CommentView};
use lemmy_db_views::structs::{CommentReportView, CommentView, VoteView};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
@ -176,3 +176,22 @@ pub struct ListCommentReports {
pub struct ListCommentReportsResponse {
pub comment_reports: Vec<CommentReportView>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List comment likes. Admins-only.
pub struct ListCommentLikes {
pub comment_id: CommentId,
pub page: Option<i64>,
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The comment likes response
pub struct ListCommentLikesResponse {
pub comment_likes: Vec<VoteView>,
}

@ -43,5 +43,5 @@ impl Default for SuccessResponse {
/// how long to sleep based on how many retries have already happened
pub fn federate_retry_sleep_duration(retry_count: i32) -> Duration {
Duration::from_secs_f64(10.0 * 2.0_f64.powf(f64::from(retry_count)))
Duration::from_secs_f64(2.0_f64.powf(f64::from(retry_count)))
}

@ -4,7 +4,7 @@ use lemmy_db_schema::{
PostFeatureType,
SortType,
};
use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView};
use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView};
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
@ -252,3 +252,22 @@ pub struct SiteMetadata {
pub(crate) image: Option<DbUrl>,
pub embed_video_url: Option<DbUrl>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List post likes. Admins-only.
pub struct ListPostLikes {
pub post_id: PostId,
pub page: Option<i64>,
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The post likes response
pub struct ListPostLikesResponse {
pub post_likes: Vec<VoteView>,
}

@ -1,5 +1,6 @@
[package]
name = "lemmy_api_crud"
publish = false
version.workspace = true
edition.workspace = true
description.workspace = true

@ -1,5 +1,6 @@
[package]
name = "lemmy_apub"
publish = false
version.workspace = true
edition.workspace = true
description.workspace = true

@ -1,7 +1,7 @@
use crate::{
activities::{generate_activity_id, send_lemmy_activity, verify_person_in_community},
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson},
protocol::{activities::community::report::Report, InCommunity},
PostOrComment,
};
@ -19,8 +19,9 @@ use lemmy_db_schema::{
community::Community,
person::Person,
post_report::{PostReport, PostReportForm},
site::Site,
},
traits::Reportable,
traits::{Crud, Reportable},
};
use lemmy_utils::error::LemmyError;
use url::Url;
@ -44,18 +45,31 @@ impl Report {
let report = Report {
actor: actor.id().into(),
to: [community.id().into()],
object: object_id,
object: object_id.clone(),
summary: reason,
kind,
id: id.clone(),
audience: Some(community.id().into()),
};
let inbox = if community.local {
ActivitySendTargets::empty()
} else {
ActivitySendTargets::to_inbox(community.shared_inbox_or_inbox())
// send report to the community where object was posted
let mut inboxes = ActivitySendTargets::to_inbox(community.shared_inbox_or_inbox());
// also send report to user's home instance if possible
let object_creator_id = match object_id.dereference_local(&context).await? {
PostOrComment::Post(p) => p.creator_id,
PostOrComment::Comment(c) => c.creator_id,
};
send_lemmy_activity(&context, report, &actor, inbox, false).await
let object_creator = Person::read(&mut context.pool(), object_creator_id).await?;
let object_creator_site: Option<ApubSite> =
Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id)
.await?
.map(Into::into);
if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) {
inboxes.add_inbox(inbox);
}
send_lemmy_activity(&context, report, &actor, inboxes, false).await
}
}

@ -22,3 +22,5 @@ pub mod registration_application_view;
#[cfg(feature = "full")]
pub mod site_view;
pub mod structs;
#[cfg(feature = "full")]
pub mod vote_view;

@ -210,7 +210,7 @@ mod tests {
.recipient_id(timmy.id)
.content(message_content.clone())
.build();
let _inserted_sara_timmy_message_form = PrivateMessage::create(pool, &sara_timmy_message_form)
PrivateMessage::create(pool, &sara_timmy_message_form)
.await
.unwrap();
@ -219,7 +219,7 @@ mod tests {
.recipient_id(jess.id)
.content(message_content.clone())
.build();
let _inserted_sara_jess_message_form = PrivateMessage::create(pool, &sara_jess_message_form)
PrivateMessage::create(pool, &sara_jess_message_form)
.await
.unwrap();
@ -228,7 +228,7 @@ mod tests {
.recipient_id(sara.id)
.content(message_content.clone())
.build();
let _inserted_timmy_sara_message_form = PrivateMessage::create(pool, &timmy_sara_message_form)
PrivateMessage::create(pool, &timmy_sara_message_form)
.await
.unwrap();
@ -237,13 +237,13 @@ mod tests {
.recipient_id(timmy.id)
.content(message_content.clone())
.build();
let _inserted_jess_timmy_message_form = PrivateMessage::create(pool, &jess_timmy_message_form)
PrivateMessage::create(pool, &jess_timmy_message_form)
.await
.unwrap();
let timmy_messages = PrivateMessageQuery {
unread_only: false,
creator_id: Option::None,
creator_id: None,
..Default::default()
}
.list(pool, timmy.id)
@ -260,7 +260,7 @@ mod tests {
let timmy_unread_messages = PrivateMessageQuery {
unread_only: true,
creator_id: Option::None,
creator_id: None,
..Default::default()
}
.list(pool, timmy.id)
@ -320,7 +320,7 @@ mod tests {
let timmy_messages = PrivateMessageQuery {
unread_only: true,
creator_id: Option::None,
creator_id: None,
..Default::default()
}
.list(pool, timmy.id)
@ -333,5 +333,8 @@ mod tests {
.await
.unwrap();
assert_eq!(timmy_unread_messages, 1);
// This also deletes all persons and private messages thanks to sql `on delete cascade`
Instance::delete(pool, instance.id).await.unwrap();
}
}

@ -184,3 +184,14 @@ pub struct CustomEmojiView {
pub custom_emoji: CustomEmoji,
pub keywords: Vec<CustomEmojiKeyword>,
}
#[skip_serializing_none]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// A vote view for checking a post or comments votes.
pub struct VoteView {
pub creator: Person,
pub score: i16,
}

@ -0,0 +1,195 @@
use crate::structs::VoteView;
use diesel::{result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::{CommentId, PostId},
schema::{comment_like, person, post_like},
utils::{get_conn, limit_and_offset, DbPool},
};
impl VoteView {
pub async fn list_for_post(
pool: &mut DbPool<'_>,
post_id: PostId,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
let (limit, offset) = limit_and_offset(page, limit)?;
post_like::table
.inner_join(person::table)
.filter(post_like::post_id.eq(post_id))
.select((person::all_columns, post_like::score))
.order_by(post_like::score)
.limit(limit)
.offset(offset)
.load::<Self>(conn)
.await
}
pub async fn list_for_comment(
pool: &mut DbPool<'_>,
comment_id: CommentId,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
let (limit, offset) = limit_and_offset(page, limit)?;
comment_like::table
.inner_join(person::table)
.filter(comment_like::comment_id.eq(comment_id))
.select((person::all_columns, comment_like::score))
.order_by(comment_like::score)
.limit(limit)
.offset(offset)
.load::<Self>(conn)
.await
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::structs::VoteView;
use lemmy_db_schema::{
source::{
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm},
community::{Community, CommunityInsertForm},
instance::Instance,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm},
},
traits::{Crud, Likeable},
utils::build_db_pool_for_tests,
};
use serial_test::serial;
#[tokio::test]
#[serial]
async fn post_and_comment_vote_views() {
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 new_person = PersonInsertForm::builder()
.name("timmy_vv".into())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let inserted_timmy = Person::create(pool, &new_person).await.unwrap();
let new_person_2 = PersonInsertForm::builder()
.name("sara_vv".into())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let inserted_sara = Person::create(pool, &new_person_2).await.unwrap();
let new_community = CommunityInsertForm::builder()
.name("test community vv".to_string())
.title("nada".to_owned())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let new_post = PostInsertForm::builder()
.name("A test post vv".into())
.creator_id(inserted_timmy.id)
.community_id(inserted_community.id)
.build();
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let comment_form = CommentInsertForm::builder()
.content("A test comment vv".into())
.creator_id(inserted_timmy.id)
.post_id(inserted_post.id)
.build();
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
// Timmy upvotes his own post
let timmy_post_vote_form = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_timmy.id,
score: 1,
};
PostLike::like(pool, &timmy_post_vote_form).await.unwrap();
// Sara downvotes timmy's post
let sara_post_vote_form = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_sara.id,
score: -1,
};
PostLike::like(pool, &sara_post_vote_form).await.unwrap();
let expected_post_vote_views = [
VoteView {
creator: inserted_sara.clone(),
score: -1,
},
VoteView {
creator: inserted_timmy.clone(),
score: 1,
},
];
let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None)
.await
.unwrap();
assert_eq!(read_post_vote_views, expected_post_vote_views);
// Timothy votes down his own comment
let timmy_comment_vote_form = CommentLikeForm {
post_id: inserted_post.id,
comment_id: inserted_comment.id,
person_id: inserted_timmy.id,
score: -1,
};
CommentLike::like(pool, &timmy_comment_vote_form)
.await
.unwrap();
// Sara upvotes timmy's comment
let sara_comment_vote_form = CommentLikeForm {
post_id: inserted_post.id,
comment_id: inserted_comment.id,
person_id: inserted_sara.id,
score: 1,
};
CommentLike::like(pool, &sara_comment_vote_form)
.await
.unwrap();
let expected_comment_vote_views = [
VoteView {
creator: inserted_timmy.clone(),
score: -1,
},
VoteView {
creator: inserted_sara.clone(),
score: 1,
},
];
let read_comment_vote_views = VoteView::list_for_comment(pool, inserted_comment.id, None, None)
.await
.unwrap();
assert_eq!(read_comment_vote_views, expected_comment_vote_views);
// Cleanup
Instance::delete(pool, inserted_instance.id).await.unwrap();
}
}

@ -230,7 +230,6 @@ mod tests {
#[tokio::test]
#[serial]
#[allow(clippy::dbg_macro)]
async fn exclude_deleted() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
@ -257,7 +256,6 @@ mod tests {
.list(pool)
.await
.unwrap();
dbg!(&list);
assert_eq!(list.len(), 1);
assert_eq!(list[0].person.id, data.bob.id);

@ -1,5 +1,6 @@
[package]
name = "lemmy_federate"
publish = false
version.workspace = true
edition.workspace = true
description.workspace = true

@ -23,6 +23,7 @@ static INSTANCES_RECHECK_DELAY: Duration = Duration::from_secs(5);
#[cfg(not(debug_assertions))]
static INSTANCES_RECHECK_DELAY: Duration = Duration::from_secs(60);
#[derive(Clone)]
pub struct Opts {
/// how many processes you are starting in total
pub process_count: i32,
@ -36,7 +37,7 @@ async fn start_stop_federation_workers(
federation_config: FederationConfig<LemmyContext>,
cancel: CancellationToken,
) -> anyhow::Result<()> {
let mut workers = HashMap::<InstanceId, CancellableTask<_>>::new();
let mut workers = HashMap::<InstanceId, CancellableTask>::new();
let (stats_sender, stats_receiver) = unbounded_channel();
let exit_print = tokio::spawn(receive_print_stats(pool.clone(), stats_receiver));
@ -66,40 +67,30 @@ async fn start_stop_federation_workers(
let should_federate = allowed && !is_dead;
if should_federate {
if workers.contains_key(&instance.id) {
if workers
.get(&instance.id)
.map(util::CancellableTask::has_ended)
.unwrap_or(false)
{
// task must have errored out, remove and recreated it
let worker = workers
.remove(&instance.id)
.expect("just checked contains_key");
tracing::error!(
"worker for {} has stopped, recreating: {:?}",
instance.domain,
worker.cancel().await
);
} else {
continue;
}
// worker already running
continue;
}
// create new worker
let config = federation_config.clone();
let stats_sender = stats_sender.clone();
let context = federation_config.to_request_data();
let pool = pool.clone();
workers.insert(
instance.id,
CancellableTask::spawn(WORKER_EXIT_TIMEOUT, |stop| async move {
InstanceWorker::init_and_loop(
instance,
context,
&mut DbPool::Pool(&pool),
stop,
stats_sender,
)
.await?;
Ok(())
CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| {
let instance = instance.clone();
let req_data = config.clone().to_request_data();
let stats_sender = stats_sender.clone();
let pool = pool.clone();
async move {
InstanceWorker::init_and_loop(
instance,
req_data,
&mut DbPool::Pool(&pool),
stop,
stats_sender,
)
.await
}
}),
);
} else if !should_federate {
@ -135,9 +126,12 @@ pub fn start_stop_federation_workers_cancellable(
opts: Opts,
pool: ActualDbPool,
config: FederationConfig<LemmyContext>,
) -> CancellableTask<()> {
CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |c| {
start_stop_federation_workers(opts, pool, config, c)
) -> CancellableTask {
CancellableTask::spawn(WORKER_EXIT_TIMEOUT, move |stop| {
let opts = opts.clone();
let pool = pool.clone();
let config = config.clone();
async move { start_stop_federation_workers(opts, pool, config, stop).await }
})
}

@ -20,12 +20,7 @@ use moka::future::Cache;
use once_cell::sync::Lazy;
use reqwest::Url;
use serde_json::Value;
use std::{
future::Future,
pin::Pin,
sync::{Arc, RwLock},
time::Duration,
};
use std::{fmt::Debug, future::Future, pin::Pin, sync::Arc, time::Duration};
use tokio::{task::JoinHandle, time::sleep};
use tokio_util::sync::CancellationToken;
@ -49,41 +44,41 @@ pub(crate) static WORK_FINISHED_RECHECK_DELAY: Lazy<Duration> = Lazy::new(|| {
}
});
pub struct CancellableTask<R: Send + 'static> {
f: Pin<Box<dyn Future<Output = Result<R, anyhow::Error>> + Send + 'static>>,
ended: Arc<RwLock<bool>>,
/// 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.
pub struct CancellableTask {
f: Pin<Box<dyn Future<Output = Result<(), anyhow::Error>> + Send + 'static>>,
}
impl<R: Send + 'static> CancellableTask<R> {
impl CancellableTask {
/// spawn a task but with graceful shutdown
pub fn spawn<F>(
pub fn spawn<F, R: Debug>(
timeout: Duration,
task: impl FnOnce(CancellationToken) -> F,
) -> CancellableTask<R>
task: impl Fn(CancellationToken) -> F + Send + 'static,
) -> CancellableTask
where
F: Future<Output = Result<R>> + Send + 'static,
F: Future<Output = R> + Send + 'static,
{
let stop = CancellationToken::new();
let task = task(stop.clone());
let ended = Arc::new(RwLock::new(false));
let ended_write = ended.clone();
let task: JoinHandle<Result<R>> = tokio::spawn(async move {
match task.await {
Ok(o) => Ok(o),
Err(e) => {
*ended_write.write().expect("poisoned") = true;
Err(e)
let stop2 = stop.clone();
let task: JoinHandle<()> = tokio::spawn(async move {
loop {
let res = task(stop2.clone()).await;
if stop2.is_cancelled() {
return;
} else {
tracing::warn!("task exited, restarting: {res:?}");
}
}
});
let abort = task.abort_handle();
CancellableTask {
ended,
f: Box::pin(async move {
stop.cancel();
tokio::select! {
r = task => {
Ok(r.context("could not join")??)
r.context("could not join")?;
Ok(())
},
_ = sleep(timeout) => {
abort.abort();
@ -96,12 +91,9 @@ impl<R: Send + 'static> CancellableTask<R> {
}
/// cancel the cancel signal, wait for timeout for the task to stop gracefully, otherwise abort it
pub async fn cancel(self) -> Result<R, anyhow::Error> {
pub async fn cancel(self) -> Result<(), anyhow::Error> {
self.f.await
}
pub fn has_ended(&self) -> bool {
*self.ended.read().expect("poisoned")
}
}
/// assuming apub priv key and ids are immutable, then we don't need to have TTL

@ -206,6 +206,7 @@ impl InstanceWorker {
.await
.context("failed figuring out inbox urls")?;
if inbox_urls.is_empty() {
tracing::debug!("{}: {:?} no inboxes", self.instance.domain, activity.id);
self.state.last_successful_id = Some(activity.id);
self.state.last_successful_published_time = Some(activity.published);
return Ok(());

@ -1,5 +1,6 @@
[package]
name = "lemmy_routes"
publish = false
version.workspace = true
edition.workspace = true
description.workspace = true

@ -1,6 +1,6 @@
use lemmy_api_common::{claims::Claims, context::LemmyContext, utils::check_user_valid};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
pub mod feeds;
pub mod images;
@ -12,7 +12,9 @@ async fn local_user_view_from_jwt(
jwt: &str,
context: &LemmyContext,
) -> Result<LocalUserView, LemmyError> {
let local_user_id = Claims::validate(jwt, context).await?;
let local_user_id = Claims::validate(jwt, context)
.await
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?;
check_user_valid(&local_user_view.person)?;

@ -6,7 +6,7 @@ use url::Url;
// From here: https://github.com/vector-im/element-android/blob/develop/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt#L35
static VALID_MATRIX_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^@[A-Za-z0-9\\x21-\\x39\\x3B-\\x7F]+:[A-Za-z0-9.-]+(:[0-9]{2,5})?$")
Regex::new(r"^@[A-Za-z0-9\x21-\x39\x3B-\x7F]+:[A-Za-z0-9.-]+(:[0-9]{2,5})?$")
.expect("compile regex")
});
// taken from https://en.wikipedia.org/wiki/UTM_parameters
@ -386,6 +386,7 @@ mod tests {
#[test]
fn test_valid_matrix_id() {
assert!(is_valid_matrix_id("@dess:matrix.org").is_ok());
assert!(is_valid_matrix_id("@dess_:matrix.org").is_ok());
assert!(is_valid_matrix_id("@dess:matrix.org:443").is_ok());
assert!(is_valid_matrix_id("dess:matrix.org").is_err());
assert!(is_valid_matrix_id(" @dess:matrix.org").is_err());

@ -77,7 +77,7 @@ services:
init: true
pictrs:
image: asonix/pictrs:0.4.0-beta.19
image: asonix/pictrs:0.5.0-rc.2
# this needs to match the pictrs url in lemmy.hjson
hostname: pictrs
# we can set options to pictrs like this, here we set max. image size and forced format for conversion

@ -49,7 +49,7 @@ services:
pictrs:
restart: always
image: asonix/pictrs:0.4.0-beta.19
image: asonix/pictrs:0.5.0-rc.2
user: 991:991
volumes:
- ./volumes/pictrs_alpha:/mnt:Z

@ -0,0 +1,3 @@
ALTER TABLE site
ADD CONSTRAINT site_name_key UNIQUE (name);

@ -0,0 +1,3 @@
ALTER TABLE site
DROP CONSTRAINT site_name_key;

@ -45,9 +45,9 @@
## Sobre El Proyecto
| Escritorio | Móvil |
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_img.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
| Escritorio | Móvil |
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_screen_2.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
[Lemmy](https://github.com/LemmyNet/lemmy) es similar a sitios como [Menéame](https://www.meneame.net/), [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), [Raddle](https://raddle.me), o [Hacker News](https://news.ycombinator.com/): te subscribes a los foros que te interesan, publicas enlaces y debates, luego votas y comentas en ellos. Entre bastidores, es muy diferente; cualquiera puede gestionar fácilmente un servidor, y todos estos servidores son federados (piensa en el correo electrónico), y conectados al mismo universo, llamado [Fediverso](https://es.wikipedia.org/wiki/Fediverso).

@ -47,9 +47,9 @@
## プロジェクトについて
| デスクトップ | モバイル |
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_img.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
| デスクトップ | モバイル |
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_screen_2.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
[Lemmy](https://github.com/LemmyNet/lemmy) は、[Reddit](https://reddit.com)、[Lobste.rs](https://lobste.rs)、[Hacker News](https://news.ycombinator.com/) といったサイトに似ています。興味のあるフォーラムを購読してリンクや議論を掲載し、投票したり、コメントしたりしています。誰でも簡単にサーバーを運営することができ、これらのサーバーはすべて連合しており(電子メールを考えてください)、[Fediverse](https://en.wikipedia.org/wiki/Fediverse) と呼ばれる同じ宇宙に接続されています。

@ -45,9 +45,9 @@
## О проекте
| Десктоп | Мобильный |
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_img.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
| Десктоп | Мобильный |
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_screen_2.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
[Lemmy](https://github.com/LemmyNet/lemmy) это аналог таких сайтов как [Reddit](https://reddit.com), [Lobste.rs](https://lobste.rs), или [Hacker News](https://news.ycombinator.com/): вы подписываетесь на форумы, которые вас интересуют , размещаете ссылки и дискутируете, затем голосуете и комментируете их. Однако за кулисами всё совсем по-другому; любой может легко запустить сервер, и все эти серверы объединены (например электронная почта) и подключены к одной вселенной, именуемой [Федиверс](https://ru.wikipedia.org/wiki/Fediverse).

@ -47,9 +47,9 @@
## 关于项目
| 桌面应用 | 移动应用 |
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_img.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
| 桌面应用 | 移动应用 |
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_screen_2.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
[Lemmy](https://github.com/LemmyNet/lemmy) 与 [Reddit](https://reddit.com)、[Lobste.rs](https://lobste.rs) 或 [Hacker News](https://news.ycombinator.com/) 等网站类似你可以订阅你感兴趣的论坛发布链接和讨论然后进行投票或评论。但在幕后Lemmy 和他们不同——任何人都可以很容易地运行一个服务器,所有服务器都是联邦式的(想想电子邮件),并连接到 [联邦宇宙](https://zh.wikipedia.org/wiki/%E8%81%94%E9%82%A6%E5%AE%87%E5%AE%99)。

@ -48,9 +48,9 @@
## 關於專案
| 桌面設備 | 行動裝置 |
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_img.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
| 桌面設備 | 行動裝置 |
| --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| ![desktop](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/main_screen_2.webp) | ![mobile](https://raw.githubusercontent.com/LemmyNet/joinlemmy-site/main/src/assets/images/mobile_pic.webp) |
[Lemmy](https://github.com/LemmyNet/lemmy) 與 [Reddit](https://reddit.com)、[Lobste.rs](https://lobste.rs) 或 [Hacker News](https://news.ycombinator.com/) 等網站類似你可以訂閱你感興趣的論壇釋出連結和討論然後進行投票或評論。但在幕後Lemmy 和他們不同——任何人都可以很容易地架設一個伺服器,所有伺服器都是聯邦式的(想想電子郵件),並與 [聯邦宇宙](https://zh.wikipedia.org/wiki/%E8%81%94%E9%82%A6%E5%AE%87%E5%AE%99) 互聯。

@ -1,6 +1,11 @@
use actix_web::{guard, web};
use lemmy_api::{
comment::{distinguish::distinguish_comment, like::like_comment, save::save_comment},
comment::{
distinguish::distinguish_comment,
like::like_comment,
list_comment_likes::list_comment_likes,
save::save_comment,
},
comment_report::{
create::create_comment_report,
list::list_comment_reports,
@ -45,6 +50,7 @@ use lemmy_api::{
feature::feature_post,
get_link_metadata::get_link_metadata,
like::like_post,
list_post_likes::list_post_likes,
lock::lock_post,
mark_read::mark_post_as_read,
save::save_post,
@ -202,6 +208,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
.route("/feature", web::post().to(feature_post))
.route("/list", web::get().to(list_posts))
.route("/like", web::post().to(like_post))
.route("/like/list", web::get().to(list_post_likes))
.route("/save", web::put().to(save_post))
.route("/report", web::post().to(create_post_report))
.route("/report/resolve", web::put().to(resolve_post_report))
@ -226,6 +233,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
.route("/mark_as_read", web::post().to(mark_reply_as_read))
.route("/distinguish", web::post().to(distinguish_comment))
.route("/like", web::post().to(like_comment))
.route("/like/list", web::get().to(list_comment_likes))
.route("/save", web::put().to(save_comment))
.route("/list", web::get().to(list_comments))
.route("/report", web::post().to(create_comment_report))

@ -7,8 +7,14 @@ use actix_web::{
};
use core::future::Ready;
use futures_util::future::LocalBoxFuture;
use lemmy_api::{local_user_view_from_jwt, read_auth_token};
use lemmy_api_common::context::LemmyContext;
use lemmy_api::read_auth_token;
use lemmy_api_common::{
claims::Claims,
context::LemmyContext,
lemmy_db_views::structs::LocalUserView,
utils::check_user_valid,
};
use lemmy_utils::error::{LemmyError, LemmyErrorExt2, LemmyErrorType};
use reqwest::header::HeaderValue;
use std::{future::ready, rc::Rc};
@ -67,14 +73,14 @@ where
let jwt = read_auth_token(req.request())?;
if let Some(jwt) = &jwt {
// Ignore any invalid auth so the site can still be used
// TODO: this means it will be impossible to get any error message for invalid jwt. Need
// to add a separate endpoint for that.
// https://github.com/LemmyNet/lemmy/issues/3702
let local_user_view = local_user_view_from_jwt(jwt, &context).await.ok();
if let Some(local_user_view) = local_user_view {
req.extensions_mut().insert(local_user_view);
}
let local_user_id = Claims::validate(jwt, &context)
.await
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id)
.await
.map_err(LemmyError::from)?;
check_user_valid(&local_user_view.person)?;
req.extensions_mut().insert(local_user_view);
}
let mut res = svc.call(req).await?;

Loading…
Cancel
Save