Merge branch 'main' into cache-local-site-read

pull/4585/head
Felix Ableitner 2 months ago
commit b6c72fcc27

@ -5,7 +5,8 @@ variables:
- &rust_image "rust:1.76"
- &install_pnpm "corepack enable pnpm"
- &slow_check_paths
- path:
- event: pull_request
path:
include: [
# rust source code
"crates/**",
@ -40,21 +41,29 @@ steps:
- apk add git
- git submodule init
- git submodule update
when:
- event: pull_request
prettier_check:
image: tmknom/prettier:3.0.0
commands:
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml'
when:
- event: pull_request
toml_fmt:
image: tamasfe/taplo:0.8.1
commands:
- taplo format --check
when:
- event: pull_request
sql_fmt:
image: backplane/pgformatter:latest
commands:
- ./scripts/sql_format_check.sh
when:
- event: pull_request
cargo_fmt:
image: rustlang/rust:nightly
@ -64,6 +73,8 @@ steps:
commands:
- rustup component add rustfmt
- cargo +nightly fmt -- --check
when:
- event: pull_request
cargo_machete:
image: rustlang/rust:nightly
@ -73,6 +84,8 @@ steps:
- cp cargo-binstall /usr/local/cargo/bin
- cargo binstall -y cargo-machete
- cargo machete
when:
- event: pull_request
ignored_files:
image: alpine:3
@ -80,6 +93,8 @@ steps:
- apk add git
- IGNORED=$(git ls-files --cached -i --exclude-standard)
- if [[ "$IGNORED" ]]; then echo "Ignored files present:\n$IGNORED\n"; exit 1; fi
when:
- event: pull_request
# make sure api builds with default features (used by other crates relying on lemmy api)
check_api_common_default_features:
@ -200,7 +215,8 @@ steps:
- cat target/log/lemmy_*.out || true
- "# If you can't see all output, then use the download button"
when:
- status: [failure]
- event: pull_request
status: failure
publish_release_docker:
image: woodpeckerci/plugin-docker-buildx
@ -247,7 +263,8 @@ steps:
- apk add curl
- "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
when:
- status: [failure]
- event: pull_request
status: failure
notify_on_tag_deploy:
image: alpine:3

@ -17,13 +17,16 @@ import {
deleteAllImages,
delta,
epsilon,
followCommunity,
gamma,
getSite,
imageFetchLimit,
registerUser,
resolveBetaCommunity,
resolveCommunity,
resolvePost,
setupLogins,
waitForPost,
unfollows,
} from "./shared";
const downloadFileSync = require("download-file-sync");
@ -209,6 +212,11 @@ test("Images in remote post are proxied if setting enabled", async () => {
test("No image proxying if setting is disabled", async () => {
let user = await registerUser(beta, betaUrl);
let community = await createCommunity(alpha);
let betaCommunity = await resolveCommunity(
beta,
community.community_view.community.actor_id,
);
await followCommunity(beta, true, betaCommunity.community!.community.id);
const upload_form: UploadImage = {
image: Buffer.from("test"),
@ -228,15 +236,19 @@ test("No image proxying if setting is disabled", async () => {
).toBeTruthy();
expect(post.post_view.post.body).toBe("![](http://example.com/image2.png)");
let gammaPost = await resolvePost(delta, post.post_view.post);
expect(gammaPost.post).toBeDefined();
let betaPost = await waitForPost(
beta,
post.post_view.post,
res => res?.post.alt_text != null,
);
expect(betaPost.post).toBeDefined();
// remote image doesnt get proxied after federation
expect(
gammaPost.post!.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy();
expect(gammaPost.post!.post.body).toBe("![](http://example.com/image2.png)");
expect(betaPost.post.body).toBe("![](http://example.com/image2.png)");
// Make sure the alt text got federated
expect(post.post_view.post.alt_text).toBe(gammaPost.post!.post.alt_text);
expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text);
});

@ -55,7 +55,18 @@ afterAll(() => {
unfollows();
});
function assertPostFederation(postOne?: PostView, postTwo?: PostView) {
async function assertPostFederation(postOne: PostView, postTwo: PostView) {
// 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,
);
expect(postOne?.post.ap_id).toBe(postTwo?.post.ap_id);
expect(postOne?.post.name).toBe(postTwo?.post.name);
expect(postOne?.post.body).toBe(postTwo?.post.body);
@ -109,7 +120,7 @@ test("Create a post", async () => {
expect(betaPost?.community.local).toBe(true);
expect(betaPost?.creator.local).toBe(false);
expect(betaPost?.counts.score).toBe(1);
assertPostFederation(betaPost, postRes.post_view);
await assertPostFederation(betaPost, postRes.post_view);
// Delta only follows beta, so it should not see an alpha ap_id
await expect(
@ -157,7 +168,7 @@ test("Unlike a post", async () => {
expect(betaPost?.community.local).toBe(true);
expect(betaPost?.creator.local).toBe(false);
expect(betaPost?.counts.score).toBe(0);
assertPostFederation(betaPost, postRes.post_view);
await assertPostFederation(betaPost, postRes.post_view);
});
test("Update a post", async () => {
@ -178,7 +189,7 @@ test("Update a post", async () => {
expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false);
expect(betaPost.post.name).toBe(updatedName);
assertPostFederation(betaPost, updatedPost.post_view);
await assertPostFederation(betaPost, updatedPost.post_view);
// Make sure lemmy beta cannot update the post
await expect(editPost(beta, betaPost.post)).rejects.toStrictEqual(
@ -329,7 +340,7 @@ test("Delete a post", async () => {
throw "Missing beta post 2";
}
expect(betaPost2.post.deleted).toBe(false);
assertPostFederation(betaPost2, undeletedPost.post_view);
await assertPostFederation(betaPost2, undeletedPost.post_view);
// Make sure lemmy beta cannot delete the post
await expect(deletePost(beta, true, betaPost2.post)).rejects.toStrictEqual(
@ -372,7 +383,7 @@ test("Remove a post from admin and community on different instance", async () =>
// Make sure lemmy beta sees post is undeleted
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post;
expect(betaPost2?.post.removed).toBe(false);
assertPostFederation(betaPost2, undeletedPost.post_view);
await assertPostFederation(betaPost2!, undeletedPost.post_view);
});
test("Remove a post from admin and community on same instance", async () => {
@ -403,7 +414,7 @@ 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);
assertPostFederation(alphaPost.post_view, removePostRes.post_view);
await assertPostFederation(alphaPost.post_view, removePostRes.post_view);
// Undelete
let undeletedPost = await removePost(beta, false, betaPost.post);
@ -416,7 +427,7 @@ test("Remove a post from admin and community on same instance", async () => {
p => !!p && !p.post.removed,
);
expect(alphaPost2.post.removed).toBe(false);
assertPostFederation(alphaPost2, undeletedPost.post_view);
await assertPostFederation(alphaPost2, undeletedPost.post_view);
await unfollowRemotes(alpha);
});

@ -19,8 +19,9 @@ import {
getPost,
getComments,
fetchFunction,
alphaImage,
} from "./shared";
import { LemmyHttp, SaveUserSettings } from "lemmy-js-client";
import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client";
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
beforeAll(setupLogins);
@ -159,3 +160,34 @@ test("Create user with accept-language", async () => {
// which is automatically enabled by backend
expect(langs).toStrictEqual(["und", "de", "en", "fr"]);
});
test("Set a new avatar, old avatar is deleted", async () => {
const listMediaRes = await alphaImage.listMedia();
expect(listMediaRes.images.length).toBe(0);
const upload_form1: UploadImage = {
image: Buffer.from("test1"),
};
const upload1 = await alphaImage.uploadImage(upload_form1);
expect(upload1.url).toBeDefined();
let form1 = {
avatar: upload1.url,
};
await saveUserSettings(alpha, form1);
const listMediaRes1 = await alphaImage.listMedia();
expect(listMediaRes1.images.length).toBe(1);
const upload_form2: UploadImage = {
image: Buffer.from("test2"),
};
const upload2 = await alphaImage.uploadImage(upload_form2);
expect(upload2.url).toBeDefined();
let form2 = {
avatar: upload1.url,
};
await saveUserSettings(alpha, form2);
// make sure only the new avatar is kept
const listMediaRes2 = await alphaImage.listMedia();
expect(listMediaRes2.images.length).toBe(1);
});

@ -1,7 +1,9 @@
use actix_web::web::{Data, Json};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
person::SaveUserSettings,
request::replace_image,
utils::{
get_url_blocklist,
local_site_to_slur_regex,
@ -40,6 +42,8 @@ pub async fn save_user_settings(
let bio = diesel_option_overwrite(
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context).await?,
);
replace_image(&data.avatar, &local_user_view.person.avatar, &context).await?;
replace_image(&data.banner, &local_user_view.person.banner, &context).await?;
let avatar = proxy_image_link_opt_api(&data.avatar, &context).await?;
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;

@ -15,7 +15,7 @@ use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView};
use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorType},
version,
VERSION,
};
#[tracing::instrument(skip(context))]
@ -68,7 +68,7 @@ pub async fn leave_admin(
Ok(Json(GetSiteResponse {
site_view,
admins,
version: version::VERSION.to_string(),
version: VERSION.to_string(),
my_user: None,
all_languages,
discussion_languages,

@ -3,15 +3,13 @@ use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
request::delete_image_from_pictrs,
send_activity::{ActivityChannel, SendActivityData},
site::PurgePerson,
utils::is_admin,
utils::{is_admin, purge_user_account},
SuccessResponse,
};
use lemmy_db_schema::{
source::{
images::LocalImage,
moderator::{AdminPurgePerson, AdminPurgePersonForm},
person::{Person, PersonUpdateForm},
},
@ -29,18 +27,6 @@ pub async fn purge_person(
// Only let admin purge an item
is_admin(&local_user_view)?;
// Read the local user to get their images, and delete them
if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), data.person_id).await {
let pictrs_uploads =
LocalImage::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id).await?;
for upload in pictrs_uploads {
delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, &context)
.await
.ok();
}
}
let person = Person::read(&mut context.pool(), data.person_id).await?;
ban_nonlocal_user_from_local_communities(
&local_user_view,
@ -54,7 +40,8 @@ pub async fn purge_person(
.await?;
// Clear profile data.
Person::delete_account(&mut context.pool(), data.person_id).await?;
purge_user_account(data.person_id, &context).await?;
// Keep person record, but mark as banned to prevent login or refetching from home instance.
let person = Person::update(
&mut context.pool(),

@ -112,7 +112,7 @@ pub async fn send_local_notifs(
if let Ok(mention_user_view) = user_view {
// TODO
// At some point, make it so you can't tag the parent creator either
// This can cause two notifications, one for reply and the other for mention
// Potential duplication of notifications, one for reply and the other for mention, is handled below by checking recipient ids
recipient_ids.push(mention_user_view.local_user.id);
let user_mention_form = PersonMentionInsertForm {
@ -163,30 +163,33 @@ pub async fn send_local_notifs(
if parent_comment.creator_id != person.id && !check_blocks {
let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await;
if let Ok(parent_user_view) = user_view {
recipient_ids.push(parent_user_view.local_user.id);
let comment_reply_form = CommentReplyInsertForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
CommentReply::create(&mut context.pool(), &comment_reply_form)
.await
.ok();
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_comment_reply_subject(&person.name),
&lang.notification_comment_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
// Don't duplicate notif if already mentioned by checking recipient ids
if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id);
let comment_reply_form = CommentReplyInsertForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
CommentReply::create(&mut context.pool(), &comment_reply_form)
.await
.ok();
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_comment_reply_subject(&person.name),
&lang.notification_comment_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
}
}
}
}
@ -205,30 +208,32 @@ pub async fn send_local_notifs(
let creator_id = post.creator_id;
let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await;
if let Ok(parent_user_view) = parent_user {
recipient_ids.push(parent_user_view.local_user.id);
let comment_reply_form = CommentReplyInsertForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
CommentReply::create(&mut context.pool(), &comment_reply_form)
.await
.ok();
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_post_reply_subject(&person.name),
&lang.notification_post_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id);
let comment_reply_form = CommentReplyInsertForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
CommentReply::create(&mut context.pool(), &comment_reply_form)
.await
.ok();
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_post_reply_subject(&person.name),
&lang.notification_post_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
}
}
}
}

@ -1,18 +1,26 @@
use crate::{
context::LemmyContext,
lemmy_db_schema::traits::Crud,
post::{LinkMetadata, OpenGraphData},
utils::proxy_image_link,
send_activity::{ActivityChannel, SendActivityData},
utils::{local_site_opt_to_sensitive, proxy_image_link, proxy_image_link_opt_apub},
};
use activitypub_federation::config::Data;
use encoding::{all::encodings, DecoderTrap};
use lemmy_db_schema::{
newtypes::DbUrl,
source::images::{LocalImage, LocalImageForm},
source::{
images::{LocalImage, LocalImageForm},
local_site::LocalSite,
post::{Post, PostUpdateForm},
},
};
use lemmy_utils::{
error::{LemmyError, LemmyErrorType},
settings::structs::{PictrsImageMode, Settings},
version::VERSION,
spawn_try_task,
REQWEST_TIMEOUT,
VERSION,
};
use mime::Mime;
use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder};
@ -24,11 +32,7 @@ use urlencoding::encode;
use webpage::HTML;
pub fn client_builder(settings: &Settings) -> ClientBuilder {
let user_agent = format!(
"Lemmy/{}; +{}",
VERSION,
settings.get_protocol_and_hostname()
);
let user_agent = format!("Lemmy/{VERSION}; +{}", settings.get_protocol_and_hostname());
Client::builder()
.user_agent(user_agent.clone())
@ -82,6 +86,50 @@ pub async fn fetch_link_metadata_opt(
_ => Default::default(),
}
}
/// Generate post thumbnail in background task, because some sites can be very slow to respond.
///
/// Takes a callback to generate a send activity task, so that post can be federated with metadata.
pub fn generate_post_link_metadata(
post: Post,
custom_thumbnail: Option<Url>,
send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static,
local_site: Option<LocalSite>,
context: Data<LemmyContext>,
) {
spawn_try_task(async move {
let allow_sensitive = local_site_opt_to_sensitive(&local_site);
let page_is_sensitive = post.nsfw;
let allow_generate_thumbnail = allow_sensitive || !page_is_sensitive;
let mut thumbnail_url = custom_thumbnail.or_else(|| post.thumbnail_url.map(Into::into));
let do_generate_thumbnail = thumbnail_url.is_none() && allow_generate_thumbnail;
// Generate local thumbnail only if no thumbnail was federated and 'sensitive' attributes allow it.
let metadata = fetch_link_metadata_opt(
post.url.map(Into::into).as_ref(),
do_generate_thumbnail,
&context,
)
.await;
if let Some(thumbnail_url_) = metadata.thumbnail {
thumbnail_url = Some(thumbnail_url_.into());
}
let thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, &context).await?;
let form = PostUpdateForm {
embed_title: Some(metadata.opengraph_data.title),
embed_description: Some(metadata.opengraph_data.description),
embed_video_url: Some(metadata.opengraph_data.embed_video_url),
thumbnail_url: Some(thumbnail_url),
url_content_type: Some(metadata.content_type),
..Default::default()
};
let updated_post = Post::update(&mut context.pool(), post.id, &form).await?;
if let Some(send_activity) = send_activity(updated_post) {
ActivityChannel::submit_activity(send_activity, &context).await?;
}
Ok(())
});
}
/// Extract site metadata from HTML Opengraph attributes.
fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> Result<OpenGraphData, LemmyError> {
@ -312,6 +360,26 @@ async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> Resu
}
}
/// When adding a new avatar or similar image, delete the old one.
pub async fn replace_image(
new_image: &Option<String>,
old_image: &Option<DbUrl>,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
if new_image.is_some() {
// Ignore errors because image may be stored externally.
if let Some(avatar) = &old_image {
let image = LocalImage::delete_by_url(&mut context.pool(), avatar)
.await
.ok();
if let Some(image) = image {
delete_image_from_pictrs(&image.pictrs_alias, &image.pictrs_delete_token, context).await?;
}
}
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]

@ -1,6 +1,6 @@
use crate::{
context::LemmyContext,
request::purge_image_from_pictrs,
request::{delete_image_from_pictrs, purge_image_from_pictrs},
site::{FederatedInstances, InstanceWithFederationState},
};
use chrono::{DateTime, Days, Local, TimeZone, Utc};
@ -12,7 +12,7 @@ use lemmy_db_schema::{
community::{Community, CommunityModerator, CommunityUpdateForm},
community_block::CommunityBlock,
email_verification::{EmailVerification, EmailVerificationForm},
images::RemoteImage,
images::{LocalImage, RemoteImage},
instance::Instance,
instance_block::InstanceBlock,
local_site::LocalSite,
@ -660,6 +660,25 @@ pub async fn purge_image_posts_for_person(
Ok(())
}
/// Delete a local_user's images
async fn delete_local_user_images(
person_id: PersonId,
context: &LemmyContext,
) -> Result<(), LemmyError> {
if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), person_id).await {
let pictrs_uploads =
LocalImage::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id).await?;
// Delete their images
for upload in pictrs_uploads {
delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, context)
.await
.ok();
}
}
Ok(())
}
pub async fn purge_image_posts_for_community(
banned_community_id: CommunityId,
context: &LemmyContext,
@ -801,15 +820,22 @@ pub async fn purge_user_account(
context: &LemmyContext,
) -> Result<(), LemmyError> {
let pool = &mut context.pool();
// Delete their images
let person = Person::read(pool, person_id).await?;
// Delete their local images, if they're a local user
delete_local_user_images(person_id, context).await.ok();
// No need to update avatar and banner, those are handled in Person::delete_account
if let Some(avatar) = person.avatar {
purge_image_from_pictrs(&avatar, context).await.ok();
}
if let Some(banner) = person.banner {
purge_image_from_pictrs(&banner, context).await.ok();
}
// No need to update avatar and banner, those are handled in Person::delete_account
// Purge image posts
purge_image_posts_for_person(person_id, context).await.ok();
// Comments
Comment::permadelete_for_creator(pool, person_id)
@ -821,9 +847,6 @@ pub async fn purge_user_account(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
// Purge image posts
purge_image_posts_for_person(person_id, context).await?;
// Leave communities they mod
CommunityModerator::leave_all_communities(pool, person_id).await?;

@ -4,6 +4,7 @@ use lemmy_api_common::{
build_response::build_community_response,
community::{CommunityResponse, EditCommunity},
context::LemmyContext,
request::replace_image,
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_mod_action,
@ -42,6 +43,9 @@ pub async fn update_community(
let description =
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&data.description, false)?;
let old_community = Community::read(&mut context.pool(), data.community_id).await?;
replace_image(&data.icon, &old_community.icon, &context).await?;
replace_image(&data.banner, &old_community.banner, &context).await?;
let description = diesel_option_overwrite(description);
let icon = proxy_image_link_opt_api(&data.icon, &context).await?;

@ -4,8 +4,8 @@ use lemmy_api_common::{
build_response::build_post_response,
context::LemmyContext,
post::{CreatePost, PostResponse},
request::fetch_link_metadata_opt,
send_activity::{ActivityChannel, SendActivityData},
request::generate_post_link_metadata,
send_activity::SendActivityData,
utils::{
check_community_user_action,
generate_local_apub_endpoint,
@ -75,6 +75,7 @@ pub async fn create_post(
is_url_blocked(&url, &url_blocklist)?;
check_url_scheme(&url)?;
check_url_scheme(&custom_thumbnail)?;
let url = proxy_image_link_opt_apub(url, &context).await?;
check_community_user_action(
&local_user_view.person,
@ -98,18 +99,6 @@ pub async fn create_post(
}
}
// Only generate the thumbnail if there's no custom thumbnail provided,
// otherwise it will save it in pictrs
let generate_thumbnail = custom_thumbnail.is_none();
// Fetch post links and pictrs cached image
let metadata = fetch_link_metadata_opt(url.as_ref(), generate_thumbnail, &context).await;
let url = proxy_image_link_opt_apub(url, &context).await?;
let thumbnail_url = proxy_image_link_opt_apub(custom_thumbnail, &context)
.await?
.map(Into::into)
.or(metadata.thumbnail);
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
CommunityLanguage::is_allowed_community_language(
@ -134,18 +123,13 @@ pub async fn create_post(
let post_form = PostInsertForm::builder()
.name(data.name.trim().to_string())
.url_content_type(metadata.content_type)
.url(url)
.body(body)
.alt_text(data.alt_text.clone())
.community_id(data.community_id)
.creator_id(local_user_view.person.id)
.nsfw(data.nsfw)
.embed_title(metadata.opengraph_data.title)
.embed_description(metadata.opengraph_data.description)
.embed_video_url(metadata.opengraph_data.embed_video_url)
.language_id(language_id)
.thumbnail_url(thumbnail_url)
.build();
let inserted_post = Post::create(&mut context.pool(), &post_form)
@ -170,6 +154,14 @@ pub async fn create_post(
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
generate_post_link_metadata(
updated_post.clone(),
custom_thumbnail,
|post| Some(SendActivityData::CreatePost(post)),
Some(local_site),
context.reset_request_count(),
);
// They like their own post by default
let person_id = local_user_view.person.id;
let post_id = inserted_post.id;
@ -183,9 +175,6 @@ pub async fn create_post(
.await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
ActivityChannel::submit_activity(SendActivityData::CreatePost(updated_post.clone()), &context)
.await?;
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;

@ -4,8 +4,8 @@ use lemmy_api_common::{
build_response::build_post_response,
context::LemmyContext,
post::{EditPost, PostResponse},
request::fetch_link_metadata,
send_activity::{ActivityChannel, SendActivityData},
request::generate_post_link_metadata,
send_activity::SendActivityData,
utils::{
check_community_user_action,
get_url_blocklist,
@ -84,40 +84,11 @@ pub async fn update_post(
Err(LemmyErrorType::NoPostEditAllowed)?
}
// Fetch post links and thumbnail if url was updated
let (embed_title, embed_description, embed_video_url, metadata_thumbnail, metadata_content_type) =
match &url {
Some(url) => {
// Only generate the thumbnail if there's no custom thumbnail provided,
// otherwise it will save it in pictrs
let generate_thumbnail = custom_thumbnail.is_none() || orig_post.thumbnail_url.is_none();
let metadata = fetch_link_metadata(url, generate_thumbnail, &context).await?;
(
Some(metadata.opengraph_data.title),
Some(metadata.opengraph_data.description),
Some(metadata.opengraph_data.embed_video_url),
Some(metadata.thumbnail),
Some(metadata.content_type),
)
}
_ => Default::default(),
};
let url = match url {
Some(url) => Some(proxy_image_link_opt_apub(Some(url), &context).await?),
_ => Default::default(),
};
let custom_thumbnail = match custom_thumbnail {
Some(custom_thumbnail) => {
Some(proxy_image_link_opt_apub(Some(custom_thumbnail), &context).await?)
}
_ => Default::default(),
};
let thumbnail_url = custom_thumbnail.or(metadata_thumbnail);
let language_id = data.language_id;
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
@ -129,15 +100,10 @@ pub async fn update_post(
let post_form = PostUpdateForm {
name: data.name.clone(),
url,
url_content_type: metadata_content_type,
body: diesel_option_overwrite(body),
alt_text: diesel_option_overwrite(data.alt_text.clone()),
nsfw: data.nsfw,
embed_title,
embed_description,
embed_video_url,
language_id: data.language_id,
thumbnail_url,
updated: Some(Some(naive_now())),
..Default::default()
};
@ -147,7 +113,13 @@ pub async fn update_post(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
ActivityChannel::submit_activity(SendActivityData::UpdatePost(updated_post), &context).await?;
generate_post_link_metadata(
updated_post.clone(),
custom_thumbnail,
|post| Some(SendActivityData::UpdatePost(post)),
Some(local_site),
context.reset_request_count(),
);
build_post_response(
context.deref(),

@ -20,8 +20,8 @@ use lemmy_db_views_actor::structs::{
};
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
version,
CACHE_DURATION_SHORT,
VERSION,
};
use moka::future::Cache;
use once_cell::sync::Lazy;
@ -52,7 +52,7 @@ pub async fn get_site(
Ok(GetSiteResponse {
site_view,
admins,
version: version::VERSION.to_string(),
version: VERSION.to_string(),
my_user: None,
all_languages,
discussion_languages,

@ -1,7 +1,9 @@
use crate::site::{application_question_check, site_default_post_listing_type_check};
use actix_web::web::{Data, Json};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
request::replace_image,
site::{EditSite, SiteResponse},
utils::{
get_url_blocklist,
@ -63,6 +65,9 @@ pub async fn update_site(
SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
}
replace_image(&data.icon, &site.icon, &context).await?;
replace_image(&data.banner, &site.banner, &context).await?;
let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?;
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?;

@ -24,10 +24,9 @@ use chrono::{DateTime, Utc};
use html2text::{from_read_with_decorator, render::text_renderer::TrivialDecorator};
use lemmy_api_common::{
context::LemmyContext,
request::fetch_link_metadata_opt,
request::generate_post_link_metadata,
utils::{
get_url_blocklist,
local_site_opt_to_sensitive,
local_site_opt_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_apub,
@ -218,6 +217,7 @@ impl Object for ApubPost {
let old_post = page.id.dereference_local(context).await;
let first_attachment = page.attachment.first();
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let form = if !page.is_mod_action(context).await? {
let url = if let Some(attachment) = first_attachment.cloned() {
@ -231,20 +231,8 @@ impl Object for ApubPost {
check_url_scheme(&url)?;
let alt_text = first_attachment.cloned().and_then(Attachment::alt_text);
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let allow_sensitive = local_site_opt_to_sensitive(&local_site);
let page_is_sensitive = page.sensitive.unwrap_or(false);
let allow_generate_thumbnail = allow_sensitive || !page_is_sensitive;
let mut thumbnail_url = page.image.map(|i| i.url);
let do_generate_thumbnail = thumbnail_url.is_none() && allow_generate_thumbnail;
// Generate local thumbnail only if no thumbnail was federated and 'sensitive' attributes allow it.
let metadata = fetch_link_metadata_opt(url.as_ref(), do_generate_thumbnail, context).await;
if let Some(thumbnail_url_) = metadata.thumbnail {
thumbnail_url = Some(thumbnail_url_.into());
}
let url = proxy_image_link_opt_apub(url, context).await?;
let thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, context).await?;
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(context).await?;
@ -254,30 +242,22 @@ impl Object for ApubPost {
let language_id =
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?;
PostInsertForm {
name,
url: url.map(Into::into),
body,
alt_text,
creator_id: creator.id,
community_id: community.id,
removed: None,
locked: page.comments_enabled.map(|e| !e),
published: page.published.map(Into::into),
updated: page.updated.map(Into::into),
deleted: Some(false),
nsfw: page.sensitive,
embed_title: metadata.opengraph_data.title,
embed_description: metadata.opengraph_data.description,
embed_video_url: metadata.opengraph_data.embed_video_url,
thumbnail_url,
ap_id: Some(page.id.clone().into()),
local: Some(false),
language_id,
featured_community: None,
featured_local: None,
url_content_type: metadata.content_type,
}
PostInsertForm::builder()
.name(name)
.url(url.map(Into::into))
.body(body)
.alt_text(alt_text)
.creator_id(creator.id)
.community_id(community.id)
.locked(page.comments_enabled.map(|e| !e))
.published(page.published.map(Into::into))
.updated(page.updated.map(Into::into))
.deleted(Some(false))
.nsfw(page.sensitive)
.ap_id(Some(page.id.clone().into()))
.local(Some(false))
.language_id(language_id)
.build()
} else {
// if is mod action, only update locked/stickied fields, nothing else
PostInsertForm::builder()
@ -292,6 +272,14 @@ impl Object for ApubPost {
let post = Post::create(&mut context.pool(), &form).await?;
generate_post_link_metadata(
post.clone(),
page.image.map(|i| i.url),
|_| None,
local_site,
context.reset_request_count(),
);
// write mod log entry for lock
if Page::is_locked_changed(&old_post, &page.comments_enabled) {
let form = ModLockPostForm {

@ -29,6 +29,7 @@ use diesel::{
select,
sql_types,
update,
BoolExpressionMethods,
ExpressionMethods,
NullableExpressionMethods,
QueryDsl,
@ -150,30 +151,16 @@ impl Community {
for p in &posts {
debug_assert!(p.community_id == community_id);
}
conn
.build_transaction()
.run(|conn| {
Box::pin(async move {
update(
// first remove all existing featured posts
post::table,
)
.filter(post::dsl::community_id.eq(community_id))
.set(post::dsl::featured_community.eq(false))
.execute(conn)
.await?;
// then mark the given posts as featured
let post_ids: Vec<_> = posts.iter().map(|p| p.id).collect();
update(post::table)
.filter(post::dsl::id.eq_any(post_ids))
.set(post::dsl::featured_community.eq(true))
.execute(conn)
.await?;
Ok(())
}) as _
})
.await
// 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))
// 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)))
.execute(conn)
.await?;
Ok(())
}
}

@ -74,12 +74,17 @@ impl LocalImage {
query.load::<LocalImage>(conn).await
}
pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<usize, Error> {
pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::delete(local_image::table.filter(local_image::pictrs_alias.eq(alias)))
.execute(conn)
.get_result(conn)
.await
}
pub async fn delete_by_url(pool: &mut DbPool<'_>, url: &DbUrl) -> Result<Self, Error> {
let alias = url.as_str().split('/').last().ok_or(NotFound)?;
Self::delete_by_alias(pool, alias).await
}
}
impl RemoteImage {

@ -49,14 +49,18 @@ fn queries<'a>() -> Queries<
let list = move |mut conn: DbConn<'a>, options: PrivateMessageReportQuery| async move {
let mut query = all_joins(private_message_report::table.into_boxed());
// If viewing all reports, order by newest, but if viewing unresolved only, show the oldest first (FIFO)
if options.unresolved_only {
query = query.filter(private_message_report::resolved.eq(false));
query = query
.filter(private_message_report::resolved.eq(false))
.order_by(private_message_report::published.asc());
} else {
query = query.order_by(private_message_report::published.desc());
}
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
query
.order_by(private_message::published.asc())
.limit(limit)
.offset(offset)
.load::<PrivateMessageReportView>(&mut conn)

@ -6,7 +6,7 @@ use lemmy_db_views::structs::SiteView;
use lemmy_utils::{
cache_header::{cache_1hour, cache_3days},
error::LemmyError,
version,
VERSION,
};
use serde::{Deserialize, Serialize};
use url::Url;
@ -56,7 +56,7 @@ async fn node_info(context: web::Data<LemmyContext>) -> Result<HttpResponse, Err
version: Some("2.0".to_string()),
software: Some(NodeInfoSoftware {
name: Some("lemmy".to_string()),
version: Some(version::VERSION.to_string()),
version: Some(VERSION.to_string()),
}),
protocols,
usage: Some(NodeInfoUsage {

@ -10,7 +10,6 @@ cfg_if! {
pub mod response;
pub mod settings;
pub mod utils;
pub mod version;
}
}
@ -20,6 +19,8 @@ use std::time::Duration;
pub type ConnectionId = usize;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const REQWEST_TIMEOUT: Duration = Duration::from_secs(10);
#[cfg(debug_assertions)]

@ -1 +0,0 @@
pub const VERSION: &str = "unknown version";

@ -28,7 +28,6 @@ COPY . ./
# Debug build
RUN --mount=type=cache,target=/lemmy/target set -ex; \
if [ "${RUST_RELEASE_MODE}" = "debug" ]; then \
echo "pub const VERSION: &str = \"$(git describe --tag)\";" > crates/utils/src/version.rs; \
cargo build --features "${CARGO_BUILD_FEATURES}"; \
mv target/"${RUST_RELEASE_MODE}"/lemmy_server ./lemmy_server; \
fi
@ -36,7 +35,6 @@ RUN --mount=type=cache,target=/lemmy/target set -ex; \
# Release build
RUN --mount=type=cache,target=/lemmy/target set -ex; \
if [ "${RUST_RELEASE_MODE}" = "release" ]; then \
echo "pub const VERSION: &str = \"$(git describe --tag)\";" > crates/utils/src/version.rs; \
[ -z "$USE_RELEASE_CACHE" ] && cargo clean --release; \
cargo build --features "${CARGO_BUILD_FEATURES}" --release; \
mv target/"${RUST_RELEASE_MODE}"/lemmy_server ./lemmy_server; \
@ -63,7 +61,6 @@ ENV RUST_RELEASE_MODE=${RUST_RELEASE_MODE} \
# Debug build
RUN --mount=type=cache,target=./target,uid=10001,gid=10001 set -ex; \
if [ "${RUST_RELEASE_MODE}" = "debug" ]; then \
echo "pub const VERSION: &str = \"$(git describe --tag)\";" > crates/utils/src/version.rs; \
cargo build --features "${CARGO_BUILD_FEATURES}"; \
mv "./target/$CARGO_BUILD_TARGET/$RUST_RELEASE_MODE/lemmy_server" /home/lemmy/lemmy_server; \
fi
@ -71,7 +68,6 @@ RUN --mount=type=cache,target=./target,uid=10001,gid=10001 set -ex; \
# Release build
RUN --mount=type=cache,target=./target,uid=10001,gid=10001 set -ex; \
if [ "${RUST_RELEASE_MODE}" = "release" ]; then \
echo "pub const VERSION: &str = \"$(git describe --tag)\";" > crates/utils/src/version.rs; \
[ -z "$USE_RELEASE_CACHE" ] && cargo clean --release; \
cargo build --features "${CARGO_BUILD_FEATURES}" --release; \
mv "./target/$CARGO_BUILD_TARGET/$RUST_RELEASE_MODE/lemmy_server" /home/lemmy/lemmy_server; \

@ -49,7 +49,7 @@ use lemmy_utils::{
rate_limit::RateLimitCell,
response::jsonify_plain_text_errors,
settings::{structs::Settings, SETTINGS},
version,
VERSION,
};
use prometheus::default_registry;
use prometheus_metrics::serve_prometheus;
@ -109,7 +109,7 @@ pub struct CmdArgs {
/// Placing the main function in lib.rs allows other crates to import it and embed Lemmy
pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
// Print version number to log
println!("Lemmy v{}", version::VERSION);
println!("Lemmy v{VERSION}");
// return error 503 while running db migrations and startup tasks
let mut startup_server_handle = None;

Loading…
Cancel
Save