Merge branch 'main' into markdown-link-rule

markdown-link-rule-dess
Felix Ableitner 6 months ago
commit 3d698dde7c

@ -1,2 +0,0 @@
[build]
rustflags = ["--cfg", "tokio_unstable"]

@ -5,4 +5,4 @@ api_tests
ansible
tests
*.sh
pictrs
pictrs

1
.gitignore vendored

@ -20,7 +20,6 @@ query_testing/**/reports/*.json
api_tests/node_modules
api_tests/.yalc
api_tests/yalc.lock
api_tests/test.png
api_tests/pict-rs
# pictrs data

@ -6,7 +6,8 @@ variables:
- &slow_check_paths
- path:
# rust source code
- "**/*.rs"
- "crates/**"
- "src/**"
- "**/Cargo.toml"
- "Cargo.lock"
# database migrations
@ -60,7 +61,7 @@ steps:
image: rustlang/rust:nightly
environment:
# store cargo data in repo folder so that it gets cached between steps
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
commands:
# need make existing toolchain available
- cargo +nightly fmt -- --check
@ -75,6 +76,14 @@ steps:
- cargo binstall -y cargo-machete
- cargo machete
ignored_files:
group: format
image: alpine:3
commands:
- apk add git
- IGNORED=$(git ls-files --cached -i --exclude-standard)
- if [[ "$IGNORED" ]]; then echo "Ignored files present:\n$IGNORED\n"; exit 1; fi
restore-cache:
image: meltwater/drone-cache:v1
pull: true
@ -92,7 +101,7 @@ steps:
cache_key: "rust-cache"
path-style: true
mount:
- ".cargo"
- ".cargo_home"
- "target"
- "api_tests/node_modules"
secrets:
@ -103,7 +112,7 @@ steps:
check_api_common_default_features:
image: *rust_image
environment:
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
commands:
- cargo check --package lemmy_api_common
when: *slow_check_paths
@ -111,7 +120,7 @@ steps:
lemmy_api_common_doesnt_depend_on_diesel:
image: *rust_image
environment:
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
commands:
- "! cargo tree -p lemmy_api_common --no-default-features -i diesel"
when: *slow_check_paths
@ -119,7 +128,7 @@ steps:
lemmy_api_common_works_with_wasm:
image: *rust_image
environment:
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
commands:
- "rustup target add wasm32-unknown-unknown"
- "cargo check --target wasm32-unknown-unknown -p lemmy_api_common"
@ -128,7 +137,7 @@ steps:
check_defaults_hjson_updated:
image: *rust_image
environment:
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
commands:
- export LEMMY_CONFIG_LOCATION=./config/config.hjson
- ./scripts/update_config_defaults.sh config/defaults_current.hjson
@ -138,7 +147,7 @@ steps:
check_diesel_schema:
image: willsquire/diesel-cli
environment:
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands:
- diesel migration run
@ -149,7 +158,7 @@ steps:
check_diesel_migration_revertable:
image: willsquire/diesel-cli
environment:
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands:
- diesel migration run
@ -159,7 +168,7 @@ steps:
cargo_clippy:
image: *rust_image
environment:
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
commands:
# when adding new clippy lints, make sure to also add them in scripts/lint.sh
- rustup component add clippy
@ -169,7 +178,7 @@ steps:
cargo_build:
image: *rust_image
environment:
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
commands:
- cargo build
- mv target/debug/lemmy_server target/lemmy_server
@ -181,7 +190,7 @@ steps:
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo
CARGO_HOME: .cargo_home
commands:
- export LEMMY_CONFIG_LOCATION=../../config/config.hjson
- cargo test --workspace --no-fail-fast
@ -218,7 +227,7 @@ steps:
region: us-east-1
path-style: true
mount:
- ".cargo"
- ".cargo_home"
- "target"
- "api_tests/node_modules"
secrets:
@ -233,9 +242,7 @@ steps:
settings:
repo: dessalines/lemmy
dockerfile: docker/Dockerfile
# TODO fix arm build: see: https://woodpecker.join-lemmy.org/repos/129/pipeline/2888/20
# platforms: linux/amd64,linux/arm64
platforms: linux/amd64
platforms: linux/amd64, linux/arm64
build_args:
- RUST_RELEASE_MODE=release
tag: ${CI_COMMIT_TAG}
@ -255,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:

1325
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,5 +1,6 @@
[workspace.package]
version = "0.19.0-rc.6"
version = "0.19.1-rc.2"
publish = false
edition = "2021"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
@ -84,23 +85,23 @@ unused_self = "deny"
unwrap_used = "deny"
[workspace.dependencies]
lemmy_api = { version = "=0.19.0-rc.6", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.0-rc.6", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.0-rc.6", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.0-rc.6", path = "./crates/utils" }
lemmy_db_schema = { version = "=0.19.0-rc.6", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.0-rc.6", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.0-rc.6", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.0-rc.6", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.0-rc.6", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.0-rc.6", path = "./crates/db_views_moderator" }
activitypub_federation = { version = "0.5.0-beta.5", default-features = false, features = [
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",
] }
diesel = "2.1.3"
diesel = "2.1.4"
diesel_migrations = "2.1.0"
diesel-async = "0.3.2"
serde = { version = "1.0.189", features = ["derive"] }
diesel-async = "0.4.1"
serde = { version = "1.0.193", features = ["derive"] }
serde_with = "3.4.0"
actix-web = { version = "4.4.0", default-features = false, features = [
"macros",
@ -111,11 +112,11 @@ actix-web = { version = "4.4.0", default-features = false, features = [
"cookies",
] }
tracing = "0.1.40"
tracing-actix-web = { version = "0.7.8", default-features = false }
tracing-actix-web = { version = "0.7.9", default-features = false }
tracing-error = "0.2.0"
tracing-log = "0.1.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = { version = "2.4.1", features = ["serde"] }
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.22", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.4"
reqwest-tracing = "0.4.6"
@ -123,9 +124,9 @@ clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.0"
chrono = { version = "0.4.31", features = ["serde"], default-features = false }
serde_json = { version = "1.0.107", features = ["preserve_order"] }
serde_json = { version = "1.0.108", features = ["preserve_order"] }
base64 = "0.21.5"
uuid = { version = "1.5.0", features = ["serde", "v4"] }
uuid = { version = "1.6.1", features = ["serde", "v4"] }
async-trait = "0.1.74"
captcha = "0.0.9"
anyhow = { version = "1.0.75", features = [
@ -133,26 +134,28 @@ anyhow = { version = "1.0.75", features = [
] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.0"
serial_test = "2.0.0"
tokio = { version = "1.33.0", features = ["full"] }
tokio = { version = "1.35.0", features = ["full"] }
regex = "1.10.2"
once_cell = "1.18.0"
once_cell = "1.19.0"
diesel-derive-newtype = "2.1.0"
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = "0.25.0"
strum_macros = "0.25.3"
itertools = "0.11.0"
futures = "0.3.28"
http = "0.2.9"
itertools = "0.12.0"
futures = "0.3.29"
http = "0.2.11"
percent-encoding = "2.3.1"
rosetta-i18n = "0.1.3"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.19.0" }
ts-rs = { version = "7.0.0", features = ["serde-compat", "chrono-impl"] }
rustls = { version = "0.21.8", features = ["dangerous_configuration"] }
futures-util = "0.3.28"
rustls = { version = "0.21.10", features = ["dangerous_configuration"] }
futures-util = "0.3.29"
tokio-postgres = "0.7.10"
tokio-postgres-rustls = "0.10.0"
urlencoding = "2.1.3"
enum-map = "2.7"
moka = { version = "0.12.1", features = ["future"] }
[dependencies]
lemmy_api = { workspace = true }
@ -162,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.0-rc.6", 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 }
@ -182,11 +185,12 @@ tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry = { workspace = true, optional = true }
console-subscriber = { version = "0.1.10", optional = true }
opentelemetry-otlp = { version = "0.12.0", optional = true }
pict-rs = { version = "0.4.5", optional = true }
pict-rs = { version = "0.5.0-rc.2", optional = true }
tokio.workspace = true
actix-cors = "0.6.4"
actix-cors = "0.6.5"
futures-util = { workspace = true }
chrono = { workspace = true }
prometheus = { version = "0.13.3", features = ["process"] }
serial_test = { workspace = true }
clap = { version = "4.4.7", features = ["derive"] }
clap = { version = "4.4.11", features = ["derive"] }
actix-web-prom = "0.7.0"

@ -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>

@ -14,6 +14,7 @@
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-var-requires": 0,
"arrow-body-style": 0,
"curly": 0,
"eol-last": 0,

@ -19,17 +19,17 @@
"api-test-image": "jest -i image.spec.ts"
},
"devDependencies": {
"@types/jest": "^29.5.8",
"@types/node": "^20.9.0",
"@typescript-eslint/eslint-plugin": "^6.10.0",
"@typescript-eslint/parser": "^6.10.0",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"download-file-sync": "^1.0.4",
"eslint": "^8.53.0",
"eslint": "^8.55.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.5.0",
"lemmy-js-client": "0.19.0-alpha.18",
"prettier": "^3.0.0",
"lemmy-js-client": "0.19.0",
"prettier": "^3.1.1",
"ts-jest": "^29.1.0",
"typescript": "^5.0.4"
"typescript": "^5.3.3"
}
}

Binary file not shown.

@ -9,7 +9,7 @@ export RUST_LOG="warn,lemmy_server=debug,lemmy_federate=debug,lemmy_api=debug,le
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
# pictrs setup
if ! [ -f "pict-rs" ]; then
if [ ! -f "pict-rs" ]; then
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs
chmod +x api_tests/pict-rs
fi

@ -39,7 +39,6 @@ import {
delay,
} from "./shared";
import { CommentView, CommunityView } from "lemmy-js-client";
import { LemmyHttp } from "lemmy-js-client";
let betaCommunity: CommunityView | undefined;
let postOnAlphaRes: PostResponse;
@ -345,17 +344,26 @@ test("Federated comment like", async () => {
test("Reply to a comment from another instance, get notification", async () => {
await alpha.markAllAsRead();
let betaCommunity = (await resolveBetaCommunity(alpha)).community;
let betaCommunity = (
await waitUntil(
() => resolveBetaCommunity(alpha),
c => !!c.community?.community.instance_id,
)
).community;
if (!betaCommunity) {
throw "Missing beta community";
}
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
// Create a root-level trunk-branch comment on alpha
let commentRes = await createComment(alpha, postOnAlphaRes.post_view.post.id);
// find that comment id on beta
let betaComment = (
await resolveComment(beta, commentRes.comment_view.comment)
await waitUntil(
() => resolveComment(beta, commentRes.comment_view.comment),
c => c.comment?.counts.score === 1,
)
).comment;
if (!betaComment) {
@ -406,7 +414,10 @@ test("Reply to a comment from another instance, get notification", async () => {
expect(alphaUnreadCountRes.replies).toBeGreaterThanOrEqual(1);
// check inbox of replies on alpha, fetching read/unread both
let alphaRepliesRes = await getReplies(alpha);
let alphaRepliesRes = await waitUntil(
() => getReplies(alpha),
r => r.replies.length > 0,
);
const alphaReply = alphaRepliesRes.replies.find(
r => r.comment.id === alphaComment.comment.id,
);

@ -32,7 +32,7 @@ import {
resolveBetaCommunity,
longDelay,
} from "./shared";
import { EditSite, LemmyHttp } from "lemmy-js-client";
import { EditSite } from "lemmy-js-client";
beforeAll(setupLogins);

@ -7,7 +7,7 @@ import {
PurgePost,
} from "lemmy-js-client";
import {
alpha,
alphaImage,
alphaUrl,
beta,
betaUrl,
@ -18,22 +18,22 @@ import {
setupLogins,
unfollowRemotes,
} from "./shared";
import fs = require("fs");
const downloadFileSync = require("download-file-sync");
beforeAll(setupLogins);
afterAll(() => {
unfollowRemotes(alpha);
unfollowRemotes(alphaImage);
});
test("Upload image and delete it", async () => {
// upload test image
const upload_image = fs.readFileSync("test.png");
// Upload test image. We use a simple string buffer as pictrs doesnt require an actual image
// in testing mode.
const upload_image = Buffer.from("test");
const upload_form: UploadImage = {
image: upload_image,
};
const upload = await alpha.uploadImage(upload_form);
const upload = await alphaImage.uploadImage(upload_form);
expect(upload.files![0].file).toBeDefined();
expect(upload.files![0].delete_token).toBeDefined();
expect(upload.url).toBeDefined();
@ -48,7 +48,7 @@ test("Upload image and delete it", async () => {
token: upload.files![0].delete_token,
filename: upload.files![0].file,
};
const delete_ = await alpha.deleteImage(delete_form);
const delete_ = await alphaImage.deleteImage(delete_form);
expect(delete_).toBe(true);
// ensure that image is deleted
@ -57,10 +57,10 @@ test("Upload image and delete it", async () => {
});
test("Purge user, uploaded image removed", async () => {
let user = await registerUser(alpha, alphaUrl);
let user = await registerUser(alphaImage, alphaUrl);
// upload test image
const upload_image = fs.readFileSync("test.png");
const upload_image = Buffer.from("test");
const upload_form: UploadImage = {
image: upload_image,
};
@ -79,7 +79,7 @@ test("Purge user, uploaded image removed", async () => {
const purge_form: PurgePerson = {
person_id: site.my_user!.local_user_view.person.id,
};
const delete_ = await alpha.purgePerson(purge_form);
const delete_ = await alphaImage.purgePerson(purge_form);
expect(delete_.success).toBe(true);
// ensure that image is deleted
@ -91,7 +91,7 @@ test("Purge post, linked image removed", async () => {
let user = await registerUser(beta, betaUrl);
// upload test image
const upload_image = fs.readFileSync("test.png");
const upload_image = Buffer.from("test");
const upload_form: UploadImage = {
image: upload_image,
};

@ -39,7 +39,7 @@ import {
loginUser,
} from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView";
import { LemmyHttp, ResolveObject } from "lemmy-js-client";
import { ResolveObject } from "lemmy-js-client";
let betaCommunity: CommunityView | undefined;

@ -10,6 +10,7 @@ import {
deletePrivateMessage,
unfollowRemotes,
waitUntil,
reportPrivateMessage,
} from "./shared";
let recipient_id: number;
@ -109,3 +110,42 @@ test("Delete a private message", async () => {
betaPms1.private_messages.length,
);
});
test("Create a private message report", async () => {
let pmRes = await createPrivateMessage(alpha, recipient_id);
let betaPms1 = await waitUntil(
() => listPrivateMessages(beta),
m =>
!!m.private_messages.find(
e =>
e.private_message.ap_id ===
pmRes.private_message_view.private_message.ap_id,
),
);
let betaPm = betaPms1.private_messages[0];
expect(betaPm).toBeDefined();
// Make sure that only the recipient can report it, so this should fail
await expect(
reportPrivateMessage(
alpha,
pmRes.private_message_view.private_message.id,
"a reason",
),
).rejects.toStrictEqual(Error("couldnt_create_report"));
// This one should pass
let reason = "another reason";
let report = await reportPrivateMessage(
beta,
betaPm.private_message.id,
reason,
);
expect(report.private_message_report_view.private_message.id).toBe(
betaPm.private_message.id,
);
expect(report.private_message_report_view.private_message_report.reason).toBe(
reason,
);
});

@ -4,12 +4,14 @@ import {
BlockInstance,
BlockInstanceResponse,
CommunityId,
CreatePrivateMessageReport,
GetReplies,
GetRepliesResponse,
GetUnreadCountResponse,
InstanceId,
LemmyHttp,
PostView,
PrivateMessageReportResponse,
SuccessResponse,
} from "lemmy-js-client";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
@ -75,17 +77,20 @@ import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDe
import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails";
import { ListingType } from "lemmy-js-client/dist/types/ListingType";
export const fetchFunction = fetch;
export let alphaUrl = "http://127.0.0.1:8541";
export let betaUrl = "http://127.0.0.1:8551";
export let gammaUrl = "http://127.0.0.1:8561";
export let deltaUrl = "http://127.0.0.1:8571";
export let epsilonUrl = "http://127.0.0.1:8581";
export let alpha = new LemmyHttp(alphaUrl);
export let beta = new LemmyHttp(betaUrl);
export let gamma = new LemmyHttp(gammaUrl);
export let delta = new LemmyHttp(deltaUrl);
export let epsilon = new LemmyHttp(epsilonUrl);
export let alpha = new LemmyHttp(alphaUrl, { fetchFunction });
export let alphaImage = new LemmyHttp(alphaUrl);
export let beta = new LemmyHttp(betaUrl, { fetchFunction });
export let gamma = new LemmyHttp(gammaUrl, { fetchFunction });
export let delta = new LemmyHttp(deltaUrl, { fetchFunction });
export let epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
export let betaAllowedInstances = [
"lemmy-alpha",
@ -135,6 +140,7 @@ export async function setupLogins() {
resEpsilon,
]);
alpha.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` });
alphaImage.setHeaders({ Authorization: `Bearer ${res[0].jwt ?? ""}` });
beta.setHeaders({ Authorization: `Bearer ${res[1].jwt ?? ""}` });
gamma.setHeaders({ Authorization: `Bearer ${res[2].jwt ?? ""}` });
delta.setHeaders({ Authorization: `Bearer ${res[3].jwt ?? ""}` });
@ -325,6 +331,7 @@ export async function getComments(
post_id: post_id,
type_: listingType,
sort: "New",
limit: 50,
};
return api.getComments(form);
}
@ -776,6 +783,18 @@ export async function reportComment(
return api.createCommentReport(form);
}
export async function reportPrivateMessage(
api: LemmyHttp,
private_message_id: number,
reason: string,
): Promise<PrivateMessageReportResponse> {
let form: CreatePrivateMessageReport = {
private_message_id,
reason,
};
return api.createPrivateMessageReport(form);
}
export async function listCommentReports(
api: LemmyHttp,
): Promise<ListCommentReportsResponse> {
@ -789,6 +808,7 @@ export function getPosts(
): Promise<GetPostsResponse> {
let form: GetPosts = {
type_: listingType,
limit: 50,
};
return api.getPosts(form);
}
@ -857,6 +877,7 @@ export function getCommentParentId(comment: Comment): number | undefined {
if (split.length > 1) {
return Number(split[split.length - 2]);
} else {
console.log(`Failed to extract comment parent id from ${comment.path}`);
return undefined;
}
}

@ -18,6 +18,7 @@ import {
saveUserSettings,
getPost,
getComments,
fetchFunction,
} from "./shared";
import { LemmyHttp, SaveUserSettings } from "lemmy-js-client";
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
@ -114,6 +115,7 @@ test("Delete user", async () => {
test("Requests with invalid auth should be treated as unauthenticated", async () => {
let invalid_auth = new LemmyHttp(alphaUrl, {
headers: { Authorization: "Bearer foobar" },
fetchFunction,
});
let site = await getSite(invalid_auth);
expect(site.my_user).toBeUndefined();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

@ -314,10 +314,10 @@
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.9.1.tgz#449dfa81a57a1d755b09aa58d826c1262e4283b4"
integrity sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==
"@eslint/eslintrc@^2.1.3":
version "2.1.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.3.tgz#797470a75fe0fbd5a53350ee715e85e87baff22d"
integrity sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==
"@eslint/eslintrc@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
@ -329,10 +329,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.53.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.53.0.tgz#bea56f2ed2b5baea164348ff4d5a879f6f81f20d"
integrity sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==
"@eslint/js@8.55.0":
version "8.55.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.55.0.tgz#b721d52060f369aa259cf97392403cb9ce892ec6"
integrity sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==
"@humanwhocodes/config-array@^0.11.13":
version "0.11.13"
@ -704,10 +704,10 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@^29.5.8":
version "29.5.8"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.8.tgz#ed5c256fe2bc7c38b1915ee5ef1ff24a3427e120"
integrity sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==
"@types/jest@^29.5.11":
version "29.5.11"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.11.tgz#0c13aa0da7d0929f078ab080ae5d4ced80fa2f2c"
integrity sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==
dependencies:
expect "^29.0.0"
pretty-format "^29.0.0"
@ -722,10 +722,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.0.tgz#10ddf0119cf20028781c06d7115562934e53f745"
integrity sha512-LzcWltT83s1bthcvjBmiBvGJiiUe84NWRHkw+ZV6Fr41z2FbIzvc815dk2nQ3RAKMuN2fkenM/z3Xv2QzEpYxQ==
"@types/node@^20.9.0":
version "20.9.0"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.9.0.tgz#bfcdc230583aeb891cf51e73cfdaacdd8deae298"
integrity sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==
"@types/node@^20.10.4":
version "20.10.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.4.tgz#b246fd84d55d5b1b71bf51f964bd514409347198"
integrity sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==
dependencies:
undici-types "~5.26.4"
@ -751,16 +751,16 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz#cfe2bd34e26d2289212946b96ab19dcad64b661a"
integrity sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==
"@typescript-eslint/eslint-plugin@^6.14.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.14.0.tgz#fc1ab5f23618ba590c87e8226ff07a760be3dd7b"
integrity sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==
dependencies:
"@eslint-community/regexpp" "^4.5.1"
"@typescript-eslint/scope-manager" "6.10.0"
"@typescript-eslint/type-utils" "6.10.0"
"@typescript-eslint/utils" "6.10.0"
"@typescript-eslint/visitor-keys" "6.10.0"
"@typescript-eslint/scope-manager" "6.14.0"
"@typescript-eslint/type-utils" "6.14.0"
"@typescript-eslint/utils" "6.14.0"
"@typescript-eslint/visitor-keys" "6.14.0"
debug "^4.3.4"
graphemer "^1.4.0"
ignore "^5.2.4"
@ -768,72 +768,72 @@
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/parser@^6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.10.0.tgz#578af79ae7273193b0b6b61a742a2bc8e02f875a"
integrity sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==
"@typescript-eslint/parser@^6.14.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.14.0.tgz#a2d6a732e0d2b95c73f6a26ae7362877cc1b4212"
integrity sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==
dependencies:
"@typescript-eslint/scope-manager" "6.10.0"
"@typescript-eslint/types" "6.10.0"
"@typescript-eslint/typescript-estree" "6.10.0"
"@typescript-eslint/visitor-keys" "6.10.0"
"@typescript-eslint/scope-manager" "6.14.0"
"@typescript-eslint/types" "6.14.0"
"@typescript-eslint/typescript-estree" "6.14.0"
"@typescript-eslint/visitor-keys" "6.14.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz#b0276118b13d16f72809e3cecc86a72c93708540"
integrity sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==
"@typescript-eslint/scope-manager@6.14.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz#53d24363fdb5ee0d1d8cda4ed5e5321272ab3d48"
integrity sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==
dependencies:
"@typescript-eslint/types" "6.10.0"
"@typescript-eslint/visitor-keys" "6.10.0"
"@typescript-eslint/types" "6.14.0"
"@typescript-eslint/visitor-keys" "6.14.0"
"@typescript-eslint/type-utils@6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz#1007faede067c78bdbcef2e8abb31437e163e2e1"
integrity sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==
"@typescript-eslint/type-utils@6.14.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz#ac9cb5ba0615c837f1a6b172feeb273d36e4f8af"
integrity sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==
dependencies:
"@typescript-eslint/typescript-estree" "6.10.0"
"@typescript-eslint/utils" "6.10.0"
"@typescript-eslint/typescript-estree" "6.14.0"
"@typescript-eslint/utils" "6.14.0"
debug "^4.3.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.10.0.tgz#f4f0a84aeb2ac546f21a66c6e0da92420e921367"
integrity sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==
"@typescript-eslint/types@6.14.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.14.0.tgz#935307f7a931016b7a5eb25d494ea3e1f613e929"
integrity sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==
"@typescript-eslint/typescript-estree@6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz#667381eed6f723a1a8ad7590a31f312e31e07697"
integrity sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==
"@typescript-eslint/typescript-estree@6.14.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz#90c7ddd45cd22139adf3d4577580d04c9189ac13"
integrity sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==
dependencies:
"@typescript-eslint/types" "6.10.0"
"@typescript-eslint/visitor-keys" "6.10.0"
"@typescript-eslint/types" "6.14.0"
"@typescript-eslint/visitor-keys" "6.14.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/utils@6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.10.0.tgz#4d76062d94413c30e402c9b0df8c14aef8d77336"
integrity sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==
"@typescript-eslint/utils@6.14.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.14.0.tgz#856a9e274367d99ffbd39c48128b93a86c4261e3"
integrity sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.10.0"
"@typescript-eslint/types" "6.10.0"
"@typescript-eslint/typescript-estree" "6.10.0"
"@typescript-eslint/scope-manager" "6.14.0"
"@typescript-eslint/types" "6.14.0"
"@typescript-eslint/typescript-estree" "6.14.0"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@6.10.0":
version "6.10.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz#b9eaf855a1ac7e95633ae1073af43d451e8f84e3"
integrity sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==
"@typescript-eslint/visitor-keys@6.14.0":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz#1d1d486581819287de824a56c22f32543561138e"
integrity sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==
dependencies:
"@typescript-eslint/types" "6.10.0"
"@typescript-eslint/types" "6.14.0"
eslint-visitor-keys "^3.4.1"
"@ungap/structured-clone@^1.2.0":
@ -1338,15 +1338,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint@^8.53.0:
version "8.53.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.53.0.tgz#14f2c8244298fcae1f46945459577413ba2697ce"
integrity sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==
eslint@^8.55.0:
version "8.55.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.55.0.tgz#078cb7b847d66f2c254ea1794fa395bf8e7e03f8"
integrity sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.3"
"@eslint/js" "8.53.0"
"@eslint/eslintrc" "^2.1.4"
"@eslint/js" "8.55.0"
"@humanwhocodes/config-array" "^0.11.13"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
@ -2286,10 +2286,10 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
lemmy-js-client@0.19.0-alpha.18:
version "0.19.0-alpha.18"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0-alpha.18.tgz#f94841681cabdf9d5c4ce7048eacb57557f68724"
integrity sha512-cKJfKKnjK+ijk0Yd6ydtne3Y4FILp2RbQg05pCru9n6PCyPAa85eQL4QxPB1PPed20ckSZRcHLcnr/bYFDgpaw==
lemmy-js-client@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0.tgz#50098183264fa176784857f45665b06994b31e18"
integrity sha512-h+E8wC9RKjlToWw9+kuGFAzk4Fiaf61KqAwzvoCDAfj2L1r+YNt5EDMOggGCoRx5PlqLuIVr7BNEU46KxJfmHA==
dependencies:
cross-fetch "^3.1.5"
form-data "^4.0.0"
@ -2619,10 +2619,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
prettier@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.1.tgz#6ba9f23165d690b6cbdaa88cb0807278f7019848"
integrity sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==
pretty-format@^29.0.0, pretty-format@^29.7.0:
version "29.7.0"
@ -2952,10 +2952,10 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typescript@^5.0.4:
version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
typescript@^5.3.3:
version "5.3.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
undici-types@~5.26.4:
version "5.26.5"

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

@ -26,6 +26,11 @@ pub async fn distinguish_comment(
)
.await?;
// Verify that only the creator can distinguish
if local_user_view.person.id != orig_comment.creator.id {
Err(LemmyErrorType::NoCommentEditAllowed)?
}
// Verify that only a mod or admin can distinguish a comment
check_community_mod_action(
&local_user_view.person,

@ -82,16 +82,7 @@ pub fn read_auth_token(req: &HttpRequest) -> Result<Option<String>, LemmyError>
}
// If that fails, try to read from cookie
else if let Some(cookie) = &req.cookie(AUTH_COOKIE_NAME) {
// ensure that its marked as httponly and secure
let secure = cookie.secure().unwrap_or_default();
let http_only = cookie.http_only().unwrap_or_default();
let is_debug_mode = cfg!(debug_assertions);
if !is_debug_mode && (!secure || !http_only) {
Err(LemmyError::from(LemmyErrorType::AuthCookieInsecure))
} else {
Ok(Some(cookie.value().to_string()))
}
Ok(Some(cookie.value().to_string()))
}
// Otherwise, there's no auth
else {

@ -1,4 +1,4 @@
use actix_web::web::{Data, Json};
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{GetSiteMetadata, GetSiteMetadataResponse},
@ -8,7 +8,7 @@ use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))]
pub async fn get_link_metadata(
data: Json<GetSiteMetadata>,
data: Query<GetSiteMetadata>,
context: Data<LemmyContext>,
) -> Result<Json<GetSiteMetadataResponse>, LemmyError> {
let metadata = fetch_link_metadata(&data.url, false, &context).await?;

@ -31,6 +31,11 @@ pub async fn create_pm_report(
let private_message_id = data.private_message_id;
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
// Make sure that only the recipient of the private message can create a report
if person_id != private_message.recipient_id {
Err(LemmyErrorType::CouldntCreateReport)?
}
let report_form = PrivateMessageReportForm {
creator_id: person_id,
private_message_id,

@ -19,6 +19,10 @@ pub async fn block_instance(
) -> Result<Json<BlockInstanceResponse>, LemmyError> {
let instance_id = data.instance_id;
let person_id = local_user_view.person.id;
if local_user_view.person.instance_id == instance_id {
return Err(LemmyErrorType::CantBlockLocalInstance)?;
}
let instance_block_form = InstanceBlockForm {
person_id,
instance_id,

@ -26,7 +26,7 @@ async fn generate_urlset(
}
pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpResponse> {
info!("Generating sitemap with posts from last {} hours...", 24);
info!("Generating sitemap...",);
let posts = Post::list_for_sitemap(&mut context.pool()).await?;
info!("Loaded latest {} posts", posts.len());
@ -36,7 +36,7 @@ pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
Ok(
HttpResponse::Ok()
.content_type("application/xml")
.insert_header(header::CacheControl(vec![CacheDirective::MaxAge(86_400)])) // 24 h
.insert_header(header::CacheControl(vec![CacheDirective::MaxAge(3_600)])) // 1 h
.body(buf),
)
}

@ -72,7 +72,7 @@ webpage = { version = "1.6", default-features = false, features = [
encoding = { version = "0.2.33", optional = true }
jsonwebtoken = { version = "8.3.0", optional = true }
# necessary for wasmt compilation
getrandom = { version = "0.2.10", features = ["js"] }
getrandom = { version = "0.2.11", features = ["js"] }
task-local-extensions = "0.1.4"
[package.metadata.cargo-machete]

@ -98,9 +98,9 @@ impl ActivityChannel {
Ok(())
}
pub async fn close(outgoing_activities_task: JoinHandle<LemmyResult<()>>) -> LemmyResult<()> {
pub async fn close(outgoing_activities_task: JoinHandle<()>) -> LemmyResult<()> {
ACTIVITY_CHANNEL.keepalive_sender.lock().await.take();
outgoing_activities_task.await??;
outgoing_activities_task.await?;
Ok(())
}
}

@ -48,7 +48,7 @@ use tracing::warn;
use url::{ParseError, Url};
use urlencoding::encode;
pub static AUTH_COOKIE_NAME: &str = "auth";
pub static AUTH_COOKIE_NAME: &str = "jwt";
#[tracing::instrument(skip_all)]
pub async fn is_mod_or_admin(

@ -1,5 +1,6 @@
[package]
name = "lemmy_api_crud"
publish = false
version.workspace = true
edition.workspace = true
description.workspace = true
@ -22,5 +23,12 @@ bcrypt = { workspace = true }
actix-web = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
webmention = "0.5.0"
futures.workspace = true
uuid = { workspace = true }
moka.workspace = true
once_cell.workspace = true
anyhow.workspace = true
webmention = "0.5.0"
[package.metadata.cargo-machete]
ignored = ["futures"]

@ -21,46 +21,68 @@ use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
version,
};
use moka::future::Cache;
use once_cell::sync::Lazy;
use std::time::Duration;
#[tracing::instrument(skip(context))]
pub async fn get_site(
local_user_view: Option<LocalUserView>,
context: Data<LemmyContext>,
) -> Result<Json<GetSiteResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
static CACHE: Lazy<Cache<(), GetSiteResponse>> = Lazy::new(|| {
Cache::builder()
.max_capacity(1)
.time_to_live(Duration::from_secs(1))
.build()
});
let admins = PersonView::admins(&mut context.pool()).await?;
// This data is independent from the user account so we can cache it across requests
let mut site_response = CACHE
.try_get_with::<_, LemmyError>((), async {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
Ok(GetSiteResponse {
site_view,
admins,
version: version::VERSION.to_string(),
my_user: None,
all_languages,
discussion_languages,
taglines,
custom_emojis,
})
})
.await
.map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?;
// Build the local user
let my_user = if let Some(local_user_view) = local_user_view {
// Build the local user with parallel queries and add it to site response
site_response.my_user = if let Some(local_user_view) = local_user_view {
let person_id = local_user_view.person.id;
let local_user_id = local_user_view.local_user.id;
let pool = &mut context.pool();
let follows = CommunityFollowerView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let person_id = local_user_view.person.id;
let community_blocks = CommunityBlockView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let instance_blocks = InstanceBlockView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let person_id = local_user_view.person.id;
let person_blocks = PersonBlockView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let moderates = CommunityModeratorView::for_person(&mut context.pool(), person_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let discussion_languages = LocalUserLanguage::read(&mut context.pool(), local_user_id)
.await
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
let (
follows,
community_blocks,
instance_blocks,
person_blocks,
moderates,
discussion_languages,
) = lemmy_db_schema::try_join_with_pool!(pool => (
|pool| CommunityFollowerView::for_person(pool, person_id),
|pool| CommunityBlockView::for_person(pool, person_id),
|pool| InstanceBlockView::for_person(pool, person_id),
|pool| PersonBlockView::for_person(pool, person_id),
|pool| CommunityModeratorView::for_person(pool, person_id),
|pool| LocalUserLanguage::read(pool, local_user_id)
))
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
Some(MyUserInfo {
local_user_view,
@ -75,20 +97,5 @@ pub async fn get_site(
None
};
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
Ok(Json(GetSiteResponse {
site_view,
admins,
version: version::VERSION.to_string(),
my_user,
all_languages,
discussion_languages,
taglines,
custom_emojis,
}))
Ok(Json(site_response))
}

@ -1,5 +1,6 @@
[package]
name = "lemmy_apub"
publish = false
version.workspace = true
edition.workspace = true
description.workspace = true
@ -40,12 +41,12 @@ async-trait = { workspace = true }
anyhow = { workspace = true }
reqwest = { workspace = true }
once_cell = { workspace = true }
serde_with = { workspace = true }
moka.workspace = true
serde_with.workspace = true
html2md = "0.2.14"
html2text = "0.6.0"
stringreader = "0.1.1"
enum_delegate = "0.2.0"
moka = { version = "0.11", features = ["future"] }
[dev-dependencies]
serial_test = { workspace = true }

@ -1,30 +1,4 @@
{
"type": "Group",
"id": "https://framatube.org/video-channels/joinpeertube",
"following": "https://framatube.org/video-channels/joinpeertube/following",
"followers": "https://framatube.org/video-channels/joinpeertube/followers",
"playlists": "https://framatube.org/video-channels/joinpeertube/playlists",
"inbox": "https://framatube.org/video-channels/joinpeertube/inbox",
"outbox": "https://framatube.org/video-channels/joinpeertube/outbox",
"preferredUsername": "joinpeertube",
"url": "https://framatube.org/video-channels/joinpeertube",
"name": "A propos de PeerTube",
"endpoints": {
"sharedInbox": "https://framatube.org/inbox"
},
"publicKey": {
"id": "https://framatube.org/video-channels/joinpeertube#main-key",
"owner": "https://framatube.org/video-channels/joinpeertube",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsJCIZJga+4Kumb9Wrmpy\ntyV7kWdINImoXBiFkGG+6OHreHN2C3UPwTu9IkX/e20NaX6Ly6c0busieW7yh//q\nomHl2U8zz2Z5xQHUN/2ljQjUNO+89OV6cFIGyEvcwc6QhuqGvrcxonjrEkux7xSv\nxQM4kZ3YW1Sii4piFpGGIm1pcUkOxFab8PWVB5Hzpg/df2/XOmH8UECT5vaMRPE6\ns6hNiQNE34z9QmPiG6nUlaWb/WDcMYbma3sUVWW3DI008ukLlwLaLIm30ax8CEYt\nHEv2jOQb1E1sXtBPe1FI+dXRgTIk40KF50KLqcgwJH1y5ck7c8IEeooj+tYGVqPr\npQIDAQAB\n-----END PUBLIC KEY-----"
},
"published": "2021-08-09T14:26:09.514Z",
"icon": {
"type": "Image",
"mediaType": "image/png",
"height": 120,
"width": 120,
"url": "https://framatube.org/lazy-static/avatars/a2c2ff10-9da6-4c6c-9b25-2e557fa74b66.png"
},
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
@ -33,99 +7,66 @@
},
{
"pt": "https://joinpeertube.org/ns#",
"sc": "http://schema.org#",
"Hashtag": "as:Hashtag",
"uuid": "sc:identifier",
"category": "sc:category",
"licence": "sc:license",
"subtitleLanguage": "sc:subtitleLanguage",
"sensitive": "as:sensitive",
"language": "sc:inLanguage",
"isLiveBroadcast": "sc:isLiveBroadcast",
"liveSaveReplay": {
"@type": "sc:Boolean",
"@id": "pt:liveSaveReplay"
},
"permanentLive": {
"@type": "sc:Boolean",
"@id": "pt:permanentLive"
},
"Infohash": "pt:Infohash",
"Playlist": "pt:Playlist",
"PlaylistElement": "pt:PlaylistElement",
"originallyPublishedAt": "sc:datePublished",
"views": {
"@type": "sc:Number",
"@id": "pt:views"
},
"state": {
"@type": "sc:Number",
"@id": "pt:state"
},
"size": {
"@type": "sc:Number",
"@id": "pt:size"
},
"fps": {
"@type": "sc:Number",
"@id": "pt:fps"
},
"startTimestamp": {
"@type": "sc:Number",
"@id": "pt:startTimestamp"
},
"stopTimestamp": {
"@type": "sc:Number",
"@id": "pt:stopTimestamp"
},
"position": {
"@type": "sc:Number",
"@id": "pt:position"
},
"commentsEnabled": {
"@type": "sc:Boolean",
"@id": "pt:commentsEnabled"
},
"downloadEnabled": {
"@type": "sc:Boolean",
"@id": "pt:downloadEnabled"
},
"waitTranscoding": {
"@type": "sc:Boolean",
"@id": "pt:waitTranscoding"
},
"support": {
"@type": "sc:Text",
"@id": "pt:support"
},
"likes": {
"@id": "as:likes",
"@type": "@id"
},
"dislikes": {
"@id": "as:dislikes",
"@type": "@id"
},
"sc": "http://schema.org/",
"playlists": {
"@id": "pt:playlists",
"@type": "@id"
},
"shares": {
"@id": "as:shares",
"@type": "@id"
"support": {
"@type": "sc:Text",
"@id": "pt:support"
},
"comments": {
"@id": "as:comments",
"@type": "@id"
}
"icons": "as:icon"
}
],
"type": "Group",
"id": "https://peertube.stream/video-channels/vu",
"following": "https://peertube.stream/video-channels/vu/following",
"followers": "https://peertube.stream/video-channels/vu/followers",
"playlists": "https://peertube.stream/video-channels/vu/playlists",
"inbox": "https://peertube.stream/video-channels/vu/inbox",
"outbox": "https://peertube.stream/video-channels/vu/outbox",
"preferredUsername": "vu",
"url": "https://peertube.stream/video-channels/vu",
"name": "VU",
"endpoints": {
"sharedInbox": "https://peertube.stream/inbox"
},
"publicKey": {
"id": "https://peertube.stream/video-channels/vu#main-key",
"owner": "https://peertube.stream/video-channels/vu",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtcWpN7efQx5C7ecWkw3r\nX4ViPy/bl3d3iyVLyP6z/3+WAUKJxqR+QKlNzxM7NglzB0B48NYu2cg4iuwKkSK9\ntrfMC/Ze0H10Wo/5kUH5YQKzLo4syHOuuM+1rbZFBbzVFwk4k0qqLFTXQ+Y6WNSS\nG9OlFYZNpRaUkgF8Q/KCsngn68qsZ0gLly9FJb+6+j3IppLJNXrBpFB5qulWibL+\neN+3XMnaTm6ge6X+rFti5r6dh10grL0KU/eZKmGyadgdwYdvR/LLtBWwFIwSJShk\nuIPhcz2zbkwrV3AixLe76TLGXX5M9qczfsVYLupyU7TwPlFM2ENDtDdfp41sWaZa\nxQIDAQAB\n-----END PUBLIC KEY-----"
},
"published": "2020-12-10T16:07:08.406Z",
"icon": [
{
"type": "Image",
"mediaType": "image/jpeg",
"height": 48,
"width": 48,
"url": "https://peertube.stream/lazy-static/avatars/45ec87d5-c8ec-4fcf-948f-d5a928b56496.jpg"
},
{
"type": "Image",
"mediaType": "image/jpeg",
"height": 120,
"width": 120,
"url": "https://peertube.stream/lazy-static/avatars/3296c098-abbb-4fda-a67a-ab88e447ca19.jpg"
}
],
"summary": "Un logiciel libre pour reprendre le contrôle de vos vidéos",
"support": null,
"image": {
"type": "Image",
"mediaType": "image/jpeg",
"height": 317,
"width": 1920,
"url": "https://peertube.stream/lazy-static/banners/550c0541-3021-4d4b-8654-54d0c4cda96d.jpg"
},
"summary": "VU c'est du lundi au samedi sur France 5 à 20h00 \nRetrouvez les meilleurs moments de la télévision, en 6 minutes.\n\nChaîne PeerTube non-officielle.",
"support": "Suivre VU :\n- Twitter : https://twitter.com/vufrancetv\n- Facebook :https://www.facebook.com/vufrancetv/\n- Site : https://www.france.tv/france-5/vu/",
"attributedTo": [
{
"type": "Person",
"id": "https://framatube.org/accounts/framasoft"
"id": "https://peertube.stream/accounts/createurs"
}
]
}

@ -1,30 +1,4 @@
{
"type": "Person",
"id": "https://framatube.org/accounts/framasoft",
"following": "https://framatube.org/accounts/framasoft/following",
"followers": "https://framatube.org/accounts/framasoft/followers",
"playlists": "https://framatube.org/accounts/framasoft/playlists",
"inbox": "https://framatube.org/accounts/framasoft/inbox",
"outbox": "https://framatube.org/accounts/framasoft/outbox",
"preferredUsername": "framasoft",
"url": "https://framatube.org/accounts/framasoft",
"name": "Framasoft",
"endpoints": {
"sharedInbox": "https://framatube.org/inbox"
},
"publicKey": {
"id": "https://framatube.org/accounts/framasoft#main-key",
"owner": "https://framatube.org/accounts/framasoft",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuRh3frgIg866D0y0FThp\nSUkJImMcHGkUvpYQYv2iUgarZZtEbwT8PfQf0bJazy+cP8KqQmMDf5PBhT7dfdny\nf/GKGMw9Olc+QISeKDj3sqZ3Csrm4KV4avMGCfth6eSU7LozojeSGCXdUFz/8UgE\nfhV4mJjEX/FbwRYoKlagv5rY9mkX5XomzZU+z9j6ZVXyofwOwJvmI1hq0SYDv2bc\neB/RgIh/H0nyMtF8o+0CT42FNEET9j9m1BKOBtPzwZHmitKRkEmui5cK256s1laB\nT61KHpcD9gQKkQ+I3sFEzCBUJYfVo6fUe+GehBZuAfq4qDhd15SfE4K9veDscDFI\nTwIDAQAB\n-----END PUBLIC KEY-----"
},
"published": "2018-03-01T15:16:17.118Z",
"icon": {
"type": "Image",
"mediaType": "image/png",
"height": null,
"width": null,
"url": "https://framatube.org/lazy-static/avatars/f73876f5-1d45-4f8a-942a-d3d5d5ac5dc1.png"
},
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
@ -33,92 +7,52 @@
},
{
"pt": "https://joinpeertube.org/ns#",
"sc": "http://schema.org#",
"Hashtag": "as:Hashtag",
"uuid": "sc:identifier",
"category": "sc:category",
"licence": "sc:license",
"subtitleLanguage": "sc:subtitleLanguage",
"sensitive": "as:sensitive",
"language": "sc:inLanguage",
"isLiveBroadcast": "sc:isLiveBroadcast",
"liveSaveReplay": {
"@type": "sc:Boolean",
"@id": "pt:liveSaveReplay"
},
"permanentLive": {
"@type": "sc:Boolean",
"@id": "pt:permanentLive"
},
"Infohash": "pt:Infohash",
"Playlist": "pt:Playlist",
"PlaylistElement": "pt:PlaylistElement",
"originallyPublishedAt": "sc:datePublished",
"views": {
"@type": "sc:Number",
"@id": "pt:views"
},
"state": {
"@type": "sc:Number",
"@id": "pt:state"
},
"size": {
"@type": "sc:Number",
"@id": "pt:size"
},
"fps": {
"@type": "sc:Number",
"@id": "pt:fps"
},
"startTimestamp": {
"@type": "sc:Number",
"@id": "pt:startTimestamp"
},
"stopTimestamp": {
"@type": "sc:Number",
"@id": "pt:stopTimestamp"
},
"position": {
"@type": "sc:Number",
"@id": "pt:position"
},
"commentsEnabled": {
"@type": "sc:Boolean",
"@id": "pt:commentsEnabled"
},
"downloadEnabled": {
"@type": "sc:Boolean",
"@id": "pt:downloadEnabled"
},
"waitTranscoding": {
"@type": "sc:Boolean",
"@id": "pt:waitTranscoding"
},
"support": {
"@type": "sc:Text",
"@id": "pt:support"
},
"likes": {
"@id": "as:likes",
"@type": "@id"
},
"dislikes": {
"@id": "as:dislikes",
"@type": "@id"
},
"sc": "http://schema.org/",
"playlists": {
"@id": "pt:playlists",
"@type": "@id"
},
"shares": {
"@id": "as:shares",
"@type": "@id"
"support": {
"@type": "sc:Text",
"@id": "pt:support"
},
"comments": {
"@id": "as:comments",
"@type": "@id"
}
"icons": "as:icon"
}
],
"type": "Person",
"id": "https://peertube.stream/accounts/createurs",
"following": "https://peertube.stream/accounts/createurs/following",
"followers": "https://peertube.stream/accounts/createurs/followers",
"playlists": "https://peertube.stream/accounts/createurs/playlists",
"inbox": "https://peertube.stream/accounts/createurs/inbox",
"outbox": "https://peertube.stream/accounts/createurs/outbox",
"preferredUsername": "createurs",
"url": "https://peertube.stream/accounts/createurs",
"name": "Créateurs",
"endpoints": {
"sharedInbox": "https://peertube.stream/inbox"
},
"publicKey": {
"id": "https://peertube.stream/accounts/createurs#main-key",
"owner": "https://peertube.stream/accounts/createurs",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxqkQhbRYbA81+WTYjorR\n2lEMad3kYCnzDjGTLr4I92eanzFHxyELGnjzP6TpEvjOiB9NrCRrqU/iFPLdgrq2\nwIFcXPWdCq6Gcg7QLlaeMM0JoJmr0KTEhzg0XKCo96UsyTzaF4DISxqi8RyoyWeU\nEkgiOzlkdYTlouq3MlQH+p1PBAsNUQfIEUsU+l6k1vzbm8JRwlT+D1bNde4I/Lqs\n4uB5ru3zzInwZ2hz9+heiriNoGEBv74rZHYn966tZVX8iMGx2+m6okozEdEQbqCl\n0ekqDcd8P6CoFqqeeu8coh82OUtuFI/XsbetdWA55YQmSHyMiTsIwVbeoogIETbI\n4QIDAQAB\n-----END PUBLIC KEY-----"
},
"published": "2020-11-11T17:12:37.243Z",
"icon": [
{
"type": "Image",
"mediaType": "image/png",
"height": 48,
"width": 48,
"url": "https://peertube.stream/lazy-static/avatars/1760df9a-3c96-45fc-9342-c313a3bf2210.png"
},
{
"type": "Image",
"mediaType": "image/png",
"height": 120,
"width": 120,
"url": "https://peertube.stream/lazy-static/avatars/c27b672d-ad8f-498a-adbe-553af8da56f9.png"
}
],
"summary": null
"summary": "Centralisation de miroirs de chaînes. La grande majorité a été contactée ou diffuse sous licence avec paternité.\n\nCompte maintenu par [Raph](https://tooter.social/@raph)."
}

@ -1,339 +1,406 @@
{
"type": "Video",
"id": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277",
"name": "What is the Fediverse?",
"duration": "PT98S",
"uuid": "4294a720-f263-4ea4-9392-cf9cea4d5277",
"tag": [
{
"type": "Hashtag",
"name": "fediverse"
},
{
"type": "Hashtag",
"name": "framasoft"
},
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"type": "Hashtag",
"name": "Mastodon"
"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
},
{
"type": "Hashtag",
"name": "PeerTube "
"pt": "https://joinpeertube.org/ns#",
"sc": "http://schema.org/",
"Hashtag": "as:Hashtag",
"uuid": "sc:identifier",
"category": "sc:category",
"licence": "sc:license",
"subtitleLanguage": "sc:subtitleLanguage",
"sensitive": "as:sensitive",
"language": "sc:inLanguage",
"identifier": "sc:identifier",
"isLiveBroadcast": "sc:isLiveBroadcast",
"liveSaveReplay": {
"@type": "sc:Boolean",
"@id": "pt:liveSaveReplay"
},
"permanentLive": {
"@type": "sc:Boolean",
"@id": "pt:permanentLive"
},
"latencyMode": {
"@type": "sc:Number",
"@id": "pt:latencyMode"
},
"Infohash": "pt:Infohash",
"tileWidth": {
"@type": "sc:Number",
"@id": "pt:tileWidth"
},
"tileHeight": {
"@type": "sc:Number",
"@id": "pt:tileHeight"
},
"tileDuration": {
"@type": "sc:Number",
"@id": "pt:tileDuration"
},
"originallyPublishedAt": "sc:datePublished",
"uploadDate": "sc:uploadDate",
"hasParts": "sc:hasParts",
"views": {
"@type": "sc:Number",
"@id": "pt:views"
},
"state": {
"@type": "sc:Number",
"@id": "pt:state"
},
"size": {
"@type": "sc:Number",
"@id": "pt:size"
},
"fps": {
"@type": "sc:Number",
"@id": "pt:fps"
},
"commentsEnabled": {
"@type": "sc:Boolean",
"@id": "pt:commentsEnabled"
},
"downloadEnabled": {
"@type": "sc:Boolean",
"@id": "pt:downloadEnabled"
},
"waitTranscoding": {
"@type": "sc:Boolean",
"@id": "pt:waitTranscoding"
},
"support": {
"@type": "sc:Text",
"@id": "pt:support"
},
"likes": {
"@id": "as:likes",
"@type": "@id"
},
"dislikes": {
"@id": "as:dislikes",
"@type": "@id"
},
"shares": {
"@id": "as:shares",
"@type": "@id"
},
"comments": {
"@id": "as:comments",
"@type": "@id"
}
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://peertube.stream/accounts/createurs/followers"],
"type": "Video",
"id": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60",
"name": "VU du 12/12/23 : Démission \"refrusée\"",
"duration": "PT383S",
"uuid": "46cc7342-fdd5-4583-ae16-2eeb340d3b60",
"category": {
"identifier": "15",
"name": "Science & Technology"
},
"licence": {
"identifier": "2",
"name": "Attribution - Share Alike"
"identifier": "11",
"name": "News & Politics"
},
"language": {
"identifier": "en",
"name": "English"
},
"views": 4805,
"views": 83,
"sensitive": false,
"waitTranscoding": true,
"isLiveBroadcast": false,
"liveSaveReplay": null,
"permanentLive": null,
"state": 1,
"commentsEnabled": true,
"downloadEnabled": true,
"published": "2022-04-28T11:51:16.293Z",
"originallyPublishedAt": null,
"updated": "2022-05-03T11:39:02.489Z",
"mediaType": "text/markdown",
"content": "Help us translate the subtitles [on our translation tool](https://weblate.framasoft.org/projects/what-is-the-fediverse-video/subtitles/).\r\n\r\n**Animation Produced by** [LILA](https://libreart.info/) - [ZeMarmot Team](https://film.zemarmot.net/)\r\n**Direction & Animation** by Aryeom\r\n**Script & Technology** by Jehan\r\n**Voice by** Paul Peterson\r\n**Licence**: [CC-By-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)\r\n\r\n**Sponsored by** [Framasoft](https://framasoft.org/)\r\n\r\n**Sound by** ORL - [AMMD](https://ammd.net/)\r\n\r\n**Music**: \"Dolling\" by CyberSDF - [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/)",
"support": null,
"subtitleLanguage": [
{
"identifier": "ca",
"name": "Catalan",
"url": "https://framatube.org/lazy-static/video-captions/6f8aedd2-c61b-47f6-a2c9-75b15af24d14-ca.vtt"
},
{
"identifier": "en",
"name": "English",
"url": "https://framatube.org/lazy-static/video-captions/2f199e59-5cf8-4529-a033-9d6dd4a858ca-en.vtt"
},
{
"identifier": "es",
"name": "Spanish",
"url": "https://framatube.org/lazy-static/video-captions/3f74c16b-925f-45e1-8388-e358428c2436-es.vtt"
},
{
"identifier": "eu",
"name": "Basque",
"url": "https://framatube.org/lazy-static/video-captions/c4c88e7e-b9d4-4192-bcf2-caf025ddc9fd-eu.vtt"
},
{
"identifier": "fr",
"name": "French",
"url": "https://framatube.org/lazy-static/video-captions/c18906e3-6257-43e7-90e4-fa2c8ded258b-fr.vtt"
},
{
"identifier": "hu",
"name": "Hungarian",
"url": "https://framatube.org/lazy-static/video-captions/0a8a295d-a288-404b-b7b3-a2272bc2a6fb-hu.vtt"
},
{
"identifier": "it",
"name": "Italian",
"url": "https://framatube.org/lazy-static/video-captions/cf857bd9-8b04-4018-af9a-23fa1ff7662d-it.vtt"
},
{
"identifier": "nb",
"name": "Norwegian Bokmål",
"url": "https://framatube.org/lazy-static/video-captions/12e3a0e9-a29e-4b06-8538-91bed2a11242-nb.vtt"
},
{
"identifier": "oc",
"name": "Occitan",
"url": "https://framatube.org/lazy-static/video-captions/d841af30-97bf-4a0c-b1f9-e163ba77f23f-oc.vtt"
},
{
"identifier": "sh",
"name": "Serbo-Croatian",
"url": "https://framatube.org/lazy-static/video-captions/7afe4dae-745f-4769-9f17-9c3a079235cf-sh.vtt"
},
"published": "2023-12-12T17:02:02.188Z",
"originallyPublishedAt": "2023-12-11T23:00:00.000Z",
"updated": "2023-12-14T06:40:34.279Z",
"tag": [
{
"identifier": "tr",
"name": "Turkish",
"url": "https://framatube.org/lazy-static/video-captions/1b2ea189-760c-4a3e-98d3-16f596c151f0-tr.vtt"
"type": "Hashtag",
"name": "France3"
},
{
"identifier": "vi",
"name": "Vietnamese",
"url": "https://framatube.org/lazy-static/video-captions/552b4086-54ab-4eb3-a8b3-7611a2175e77-vi.vtt"
"type": "Hashtag",
"name": "lezapping"
}
],
"mediaType": "text/markdown",
"content": "Un regard impertinent et libre, orchestré par Patrick Menais et son équipe, sur le monde de limage.\n\nEn avant-première du lundi au samedi à17h00 sur Facebook, Twitter et YouTube.\n\nDu lundi au samedi à 20h00 sur France 5.\n\nhttps://www.facebook.com/vufrancetv\nhttps://twitter.com/VuFrancetv",
"support": null,
"subtitleLanguage": [],
"icon": [
{
"type": "Image",
"url": "https://framatube.org/static/thumbnails/1f9eb76e-c089-4bdd-af14-602935a6db72.jpg",
"url": "https://peertube.stream/lazy-static/thumbnails/208d2248-6fa3-4a58-a2e6-c6f176559457.jpg",
"mediaType": "image/jpeg",
"width": 280,
"height": 157
},
{
"type": "Image",
"url": "https://framatube.org/lazy-static/previews/8f89d4d8-696f-4512-9a1a-72f1d12caede.jpg",
"url": "https://peertube.stream/lazy-static/previews/73d34e91-0233-443b-a1c3-d98a7ec6a87c.jpg",
"mediaType": "image/jpeg",
"width": 850,
"height": 480
}
],
"preview": [
{
"type": "Image",
"rel": ["storyboard"],
"url": [
{
"mediaType": "image/jpeg",
"href": "https://peertube.stream/lazy-static/storyboards/fb103d5f-8f76-4c8b-bc81-f952961cacfd.jpg",
"width": 1920,
"height": 1080,
"tileWidth": 192,
"tileHeight": 108,
"tileDuration": "PT4S"
}
]
}
],
"url": [
{
"type": "Link",
"mediaType": "text/html",
"href": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277"
"href": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60"
},
{
"type": "Link",
"mediaType": "application/x-mpegURL",
"href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/adc259cb-06f7-496c-8a50-599e58358b29-master.m3u8",
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/7847c00b-17f0-4cd9-b788-94283bd96d5b-master.m3u8",
"tag": [
{
"type": "Infohash",
"name": "caf7178ddd2013e28c9fbcbb7be28df25d03a023"
"name": "f50d9a3e851756a1fc1da7fe8b6e40f849c1f3a1"
},
{
"type": "Infohash",
"name": "fdddadfcf01c52808a5716ac9c0f09e379a1ca69"
},
{
"type": "Infohash",
"name": "cc18bb140f51f64090ba41c951fba85705cafa38"
"name": "c309597f071c6ab59e1a6935be3dc1ceb58c9250"
},
{
"type": "Infohash",
"name": "595513d823a1aecc18abacac94a1ebb0c31ec009"
"name": "5c28ed3e05102a678dc047a126650fe53d45ded4"
},
{
"type": "Infohash",
"name": "6ae0ce749a57d0f8ff70286878ea7661f85eebf7"
"name": "085f2c72c69af02913177534ec601349ca2b4f01"
},
{
"type": "Infohash",
"name": "4eb799f42d461929ed8dd4befae274c9a4404b99"
"name": "37b9dbeab6f433e94f80a614f888e9a1e9ee3534"
},
{
"type": "Infohash",
"name": "b48d1ea795657668783544fd1c9baf637198a323"
"name": "cc15513891e63a92743730ba65ab256f8825f071"
},
{
"type": "Link",
"name": "sha256",
"mediaType": "application/json",
"href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/b414eda3-c8af-4271-8dde-253db28aacd1-segments-sha256.json"
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/a3f5af94-ba6b-4349-a4b0-151cebdf9af6-segments-sha256.json"
},
{
"type": "Link",
"mediaType": "video/mp4",
"href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/64147344-1957-480d-9106-59dd7bbf5661-1080-fragmented.mp4",
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/5a3db28f-a4b2-49ae-963e-7fd9414efe7c-1080-fragmented.mp4",
"height": 1080,
"size": 14653991,
"fps": 24
"size": 90186372,
"fps": 25
},
{
"type": "Link",
"rel": ["metadata", "video/mp4"],
"mediaType": "application/json",
"href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421492",
"href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570438",
"height": 1080,
"fps": 24
"fps": 25
},
{
"type": "Link",
"mediaType": "application/x-bittorrent",
"href": "https://framatube.org/lazy-static/torrents/83fa27e3-aba7-4e01-9e66-931086374176-1080-hls.torrent",
"href": "https://peertube.stream/lazy-static/torrents/c3dd78f2-ff9b-41f1-899d-55440f512e09-1080-hls.torrent",
"height": 1080
},
{
"type": "Link",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2F83fa27e3-aba7-4e01-9e66-931086374176-1080-hls.torrent&xt=urn:btih:5651916e4301c812412f51381c5af0c1f627bfcb&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2F64147344-1957-480d-9106-59dd7bbf5661-1080-fragmented.mp4",
"href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2Fc3dd78f2-ff9b-41f1-899d-55440f512e09-1080-hls.torrent&xt=urn:btih:944323d8a38e077cdea5c1b1aa82300d1f49076a&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F5a3db28f-a4b2-49ae-963e-7fd9414efe7c-1080-fragmented.mp4",
"height": 1080
},
{
"type": "Link",
"mediaType": "video/mp4",
"href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/0efaeae5-7468-4c45-ade5-d3b6c732621f-720-fragmented.mp4",
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/557f45f0-60b7-418c-bddd-e55701b387bb-720-fragmented.mp4",
"height": 720,
"size": 9939723,
"fps": 24
"size": 50950797,
"fps": 25
},
{
"type": "Link",
"rel": ["metadata", "video/mp4"],
"mediaType": "application/json",
"href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421496",
"href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570447",
"height": 720,
"fps": 24
"fps": 25
},
{
"type": "Link",
"mediaType": "application/x-bittorrent",
"href": "https://framatube.org/lazy-static/torrents/b325c824-c052-46e2-9b46-887595055521-720-hls.torrent",
"href": "https://peertube.stream/lazy-static/torrents/0529c736-0c49-4efd-a9ff-c4989b4c2071-720-hls.torrent",
"height": 720
},
{
"type": "Link",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2Fb325c824-c052-46e2-9b46-887595055521-720-hls.torrent&xt=urn:btih:b5a1db245fe156edab7f1981693178dcd47075d2&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2F0efaeae5-7468-4c45-ade5-d3b6c732621f-720-fragmented.mp4",
"href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F0529c736-0c49-4efd-a9ff-c4989b4c2071-720-hls.torrent&xt=urn:btih:a2662d0714edf3882193f782814441eb904460be&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F557f45f0-60b7-418c-bddd-e55701b387bb-720-fragmented.mp4",
"height": 720
},
{
"type": "Link",
"mediaType": "video/mp4",
"href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/201f9772-4971-4bc3-8356-9b85b405ae5d-480-fragmented.mp4",
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/097e6338-4c6e-4c21-8fed-7df0a245c9b3-480-fragmented.mp4",
"height": 480,
"size": 7398758,
"fps": 24
"size": 31542462,
"fps": 25
},
{
"type": "Link",
"rel": ["metadata", "video/mp4"],
"mediaType": "application/json",
"href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421494",
"href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570441",
"height": 480,
"fps": 24
"fps": 25
},
{
"type": "Link",
"mediaType": "application/x-bittorrent",
"href": "https://framatube.org/lazy-static/torrents/bd99f84e-e9bc-4d36-bea6-6f06000f87c5-480-hls.torrent",
"href": "https://peertube.stream/lazy-static/torrents/56b47f85-b2de-44b1-9089-db13c8534e1c-480-hls.torrent",
"height": 480
},
{
"type": "Link",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2Fbd99f84e-e9bc-4d36-bea6-6f06000f87c5-480-hls.torrent&xt=urn:btih:6cbe09b50cf7788923a2ec4852a3b2bfd1cd1907&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2F201f9772-4971-4bc3-8356-9b85b405ae5d-480-fragmented.mp4",
"href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F56b47f85-b2de-44b1-9089-db13c8534e1c-480-hls.torrent&xt=urn:btih:9d1cc84a448ba531d2f5422a8910fd79580768ff&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F097e6338-4c6e-4c21-8fed-7df0a245c9b3-480-fragmented.mp4",
"height": 480
},
{
"type": "Link",
"mediaType": "video/mp4",
"href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/b2313ae6-da36-4fe3-bec5-aa352824a38a-360-fragmented.mp4",
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/b6db1f0c-0b6f-4f26-b811-d38631f4c42b-360-fragmented.mp4",
"height": 360,
"size": 6133890,
"fps": 24
"size": 23389554,
"fps": 25
},
{
"type": "Link",
"rel": ["metadata", "video/mp4"],
"mediaType": "application/json",
"href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421495",
"href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570442",
"height": 360,
"fps": 24
"fps": 25
},
{
"type": "Link",
"mediaType": "application/x-bittorrent",
"href": "https://framatube.org/lazy-static/torrents/b939430a-fdfd-4da7-a030-759ecafa6ac7-360-hls.torrent",
"href": "https://peertube.stream/lazy-static/torrents/89df203a-586e-4d09-b645-21c321ae81c2-360-hls.torrent",
"height": 360
},
{
"type": "Link",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2Fb939430a-fdfd-4da7-a030-759ecafa6ac7-360-hls.torrent&xt=urn:btih:16693f14ad9e53fc41d335e3fa409c2f943d7b68&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2Fb2313ae6-da36-4fe3-bec5-aa352824a38a-360-fragmented.mp4",
"href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F89df203a-586e-4d09-b645-21c321ae81c2-360-hls.torrent&xt=urn:btih:40dbe1b6fb96d87d0750b32b26fd52913f22c84e&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2Fb6db1f0c-0b6f-4f26-b811-d38631f4c42b-360-fragmented.mp4",
"height": 360
},
{
"type": "Link",
"mediaType": "video/mp4",
"href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/06a866f2-0527-4d68-93b7-c656d7374e86-240-fragmented.mp4",
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/d0d23e04-a7b2-47f9-8072-94a06dc0c402-240-fragmented.mp4",
"height": 240,
"size": 4861464,
"fps": 24
"size": 16040535,
"fps": 25
},
{
"type": "Link",
"rel": ["metadata", "video/mp4"],
"mediaType": "application/json",
"href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421497",
"href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570448",
"height": 240,
"fps": 24
"fps": 25
},
{
"type": "Link",
"mediaType": "application/x-bittorrent",
"href": "https://framatube.org/lazy-static/torrents/072001ee-18ad-4859-af10-9d7bf12d640c-240-hls.torrent",
"href": "https://peertube.stream/lazy-static/torrents/29c43d5c-b26f-404c-a286-7aff2e2bb139-240-hls.torrent",
"height": 240
},
{
"type": "Link",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2F072001ee-18ad-4859-af10-9d7bf12d640c-240-hls.torrent&xt=urn:btih:b823f54d8cd73f9d7a55266ce683f43bf772d26a&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2F06a866f2-0527-4d68-93b7-c656d7374e86-240-fragmented.mp4",
"href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F29c43d5c-b26f-404c-a286-7aff2e2bb139-240-hls.torrent&xt=urn:btih:f3f102c22d48b8a0aec19be463d8f04fb3a3f499&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2Fd0d23e04-a7b2-47f9-8072-94a06dc0c402-240-fragmented.mp4",
"height": 240
},
{
"type": "Link",
"mediaType": "video/mp4",
"href": "https://framatube.org/static/streaming-playlists/hls/4294a720-f263-4ea4-9392-cf9cea4d5277/f8a1caed-057f-4700-a28e-004efc158b15-0-fragmented.mp4",
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/6f3b1939-67c4-45f0-bd93-2508721dda69-144-fragmented.mp4",
"height": 144,
"size": 10969421,
"fps": 25
},
{
"type": "Link",
"rel": ["metadata", "video/mp4"],
"mediaType": "application/json",
"href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570449",
"height": 144,
"fps": 25
},
{
"type": "Link",
"mediaType": "application/x-bittorrent",
"href": "https://peertube.stream/lazy-static/torrents/e39095d9-8fa2-4543-a66f-b4b9d6165a4e-144-hls.torrent",
"height": 144
},
{
"type": "Link",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2Fe39095d9-8fa2-4543-a66f-b4b9d6165a4e-144-hls.torrent&xt=urn:btih:8b263d7e814d611597a36dcd9655d959c86605a4&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F6f3b1939-67c4-45f0-bd93-2508721dda69-144-fragmented.mp4",
"height": 144
},
{
"type": "Link",
"mediaType": "video/mp4",
"href": "https://peertube.stream/static/streaming-playlists/hls/46cc7342-fdd5-4583-ae16-2eeb340d3b60/86ab6cca-46e5-4c6e-9c2c-8aef803b85f2-0-fragmented.mp4",
"height": 0,
"size": 3141179,
"size": 6074306,
"fps": 0
},
{
"type": "Link",
"rel": ["metadata", "video/mp4"],
"mediaType": "application/json",
"href": "https://framatube.org/api/v1/videos/4294a720-f263-4ea4-9392-cf9cea4d5277/metadata/1421493",
"href": "https://peertube.stream/api/v1/videos/46cc7342-fdd5-4583-ae16-2eeb340d3b60/metadata/1570439",
"height": 0,
"fps": 0
},
{
"type": "Link",
"mediaType": "application/x-bittorrent",
"href": "https://framatube.org/lazy-static/torrents/77cb6940-7e90-48d1-a391-bfa463b9600c-0-hls.torrent",
"href": "https://peertube.stream/lazy-static/torrents/25ae194d-c3ec-412a-886f-3b0d02599ca7-0-hls.torrent",
"height": 0
},
{
"type": "Link",
"mediaType": "application/x-bittorrent;x-scheme-handler/magnet",
"href": "magnet:?xs=https%3A%2F%2Fframatube.org%2Flazy-static%2Ftorrents%2F77cb6940-7e90-48d1-a391-bfa463b9600c-0-hls.torrent&xt=urn:btih:9bc7717ed01869507041e31a7e65baffa78ba651&dn=What+is+the+Fediverse%3F&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fframatube.org%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fstreaming-playlists%2Fhls%2F4294a720-f263-4ea4-9392-cf9cea4d5277%2Ff8a1caed-057f-4700-a28e-004efc158b15-0-fragmented.mp4",
"href": "magnet:?xs=https%3A%2F%2Fpeertube.stream%2Flazy-static%2Ftorrents%2F25ae194d-c3ec-412a-886f-3b0d02599ca7-0-hls.torrent&xt=urn:btih:e4458f2445732a228e9a83e2ae53a103f5e1097e&dn=VU+du+12%2F12%2F23+%3A+D%C3%A9mission+%22refrus%C3%A9e%22&tr=https%3A%2F%2Fpeertube.stream%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube.stream%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube.stream%2Fstatic%2Fstreaming-playlists%2Fhls%2F46cc7342-fdd5-4583-ae16-2eeb340d3b60%2F86ab6cca-46e5-4c6e-9c2c-8aef803b85f2-0-fragmented.mp4",
"height": 0
}
]
@ -342,124 +409,33 @@
"type": "Link",
"name": "tracker-http",
"rel": ["tracker", "http"],
"href": "https://framatube.org/tracker/announce"
"href": "https://peertube.stream/tracker/announce"
},
{
"type": "Link",
"name": "tracker-websocket",
"rel": ["tracker", "websocket"],
"href": "wss://framatube.org:443/tracker/socket"
"href": "wss://peertube.stream:443/tracker/socket"
}
],
"likes": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/likes",
"dislikes": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/dislikes",
"shares": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/announces",
"comments": "https://framatube.org/videos/watch/4294a720-f263-4ea4-9392-cf9cea4d5277/comments",
"likes": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/likes",
"dislikes": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/dislikes",
"shares": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/announces",
"comments": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/comments",
"hasParts": "https://peertube.stream/videos/watch/46cc7342-fdd5-4583-ae16-2eeb340d3b60/chapters",
"attributedTo": [
{
"type": "Person",
"id": "https://framatube.org/accounts/framasoft"
"id": "https://peertube.stream/accounts/createurs"
},
{
"type": "Group",
"id": "https://framatube.org/video-channels/joinpeertube"
"id": "https://peertube.stream/video-channels/vu"
}
],
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": ["https://framatube.org/accounts/framasoft/followers"],
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"RsaSignature2017": "https://w3id.org/security#RsaSignature2017"
},
{
"pt": "https://joinpeertube.org/ns#",
"sc": "http://schema.org#",
"Hashtag": "as:Hashtag",
"uuid": "sc:identifier",
"category": "sc:category",
"licence": "sc:license",
"subtitleLanguage": "sc:subtitleLanguage",
"sensitive": "as:sensitive",
"language": "sc:inLanguage",
"isLiveBroadcast": "sc:isLiveBroadcast",
"liveSaveReplay": {
"@type": "sc:Boolean",
"@id": "pt:liveSaveReplay"
},
"permanentLive": {
"@type": "sc:Boolean",
"@id": "pt:permanentLive"
},
"Infohash": "pt:Infohash",
"Playlist": "pt:Playlist",
"PlaylistElement": "pt:PlaylistElement",
"originallyPublishedAt": "sc:datePublished",
"views": {
"@type": "sc:Number",
"@id": "pt:views"
},
"state": {
"@type": "sc:Number",
"@id": "pt:state"
},
"size": {
"@type": "sc:Number",
"@id": "pt:size"
},
"fps": {
"@type": "sc:Number",
"@id": "pt:fps"
},
"startTimestamp": {
"@type": "sc:Number",
"@id": "pt:startTimestamp"
},
"stopTimestamp": {
"@type": "sc:Number",
"@id": "pt:stopTimestamp"
},
"position": {
"@type": "sc:Number",
"@id": "pt:position"
},
"commentsEnabled": {
"@type": "sc:Boolean",
"@id": "pt:commentsEnabled"
},
"downloadEnabled": {
"@type": "sc:Boolean",
"@id": "pt:downloadEnabled"
},
"waitTranscoding": {
"@type": "sc:Boolean",
"@id": "pt:waitTranscoding"
},
"support": {
"@type": "sc:Text",
"@id": "pt:support"
},
"likes": {
"@id": "as:likes",
"@type": "@id"
},
"dislikes": {
"@id": "as:dislikes",
"@type": "@id"
},
"playlists": {
"@id": "pt:playlists",
"@type": "@id"
},
"shares": {
"@id": "as:shares",
"@type": "@id"
},
"comments": {
"@id": "as:comments",
"@type": "@id"
}
}
]
"isLiveBroadcast": false,
"liveSaveReplay": null,
"permanentLive": null,
"latencyMode": null,
"peertubeLiveChat": false
}

@ -225,11 +225,12 @@ where
Ok(())
}
pub async fn handle_outgoing_activities(context: Data<LemmyContext>) -> LemmyResult<()> {
pub async fn handle_outgoing_activities(context: Data<LemmyContext>) {
while let Some(data) = ActivityChannel::retrieve_activity().await {
match_outgoing_activities(data, &context.reset_request_count()).await?
if let Err(e) = match_outgoing_activities(data, &context.reset_request_count()).await {
tracing::warn!("error while saving outgoing activity to db: {e}");
}
}
Ok(())
}
pub async fn match_outgoing_activities(

@ -65,7 +65,6 @@ pub async fn read_person(
saved_only,
local_user: local_user_view.as_ref(),
community_id,
is_profile_view: true,
page,
limit,
creator_id,
@ -79,7 +78,6 @@ pub async fn read_person(
sort: sort.map(post_to_comment_sort_type),
saved_only,
community_id,
is_profile_view: true,
page,
limit,
creator_id,

@ -96,6 +96,13 @@ pub(crate) async fn get_activity(
if sensitive {
Ok(HttpResponse::Forbidden().finish())
} else {
create_apub_response(&activity.data)
// Don't use create_apub_response() to avoid duplicate context (the activity stored in db
// already includes context).
let json = serde_json::to_string_pretty(&activity.data)?;
Ok(
HttpResponse::Ok()
.content_type(FEDERATION_CONTENT_TYPE)
.body(json),
)
}
}

@ -52,6 +52,7 @@ pub struct Group {
pub(crate) summary: Option<String>,
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) source: Option<Source>,
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) icon: Option<ImageObject>,
/// banner
pub(crate) image: Option<ImageObject>,

@ -38,6 +38,7 @@ pub struct Person {
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) source: Option<Source>,
/// user avatar
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) icon: Option<ImageObject>,
/// user banner
pub(crate) image: Option<ImageObject>,

@ -67,7 +67,7 @@ once_cell = { workspace = true, optional = true }
diesel_ltree = { workspace = true, optional = true }
async-trait = { workspace = true }
tracing = { workspace = true }
deadpool = { version = "0.9.5", features = ["rt_tokio_1"], optional = true }
deadpool = { version = "0.10.0", features = ["rt_tokio_1"], optional = true }
ts-rs = { workspace = true, optional = true }
futures-util = { workspace = true }
tokio = { workspace = true, optional = true }

@ -59,52 +59,55 @@ impl Comment {
) -> Result<Comment, Error> {
let conn = &mut get_conn(pool).await?;
// Insert, to get the id
let inserted_comment = insert_into(comment)
.values(comment_form)
.on_conflict(ap_id)
.do_update()
.set(comment_form)
.get_result::<Self>(conn)
.await;
if let Ok(comment_insert) = inserted_comment {
let comment_id = comment_insert.id;
// You need to update the ltree column
let ltree = Ltree(if let Some(parent_path) = parent_path {
// The previous parent will already have 0 in it
// Append this comment id
format!("{}.{}", parent_path.0, comment_id)
} else {
// '0' is always the first path, append to that
format!("{}.{}", 0, comment_id)
});
let updated_comment = diesel::update(comment.find(comment_id))
.set(path.eq(ltree))
.get_result::<Self>(conn)
.await;
// Update the child count for the parent comment_aggregates
// You could do this with a trigger, but since you have to do this manually anyway,
// you can just have it here
if let Some(parent_path) = parent_path {
// You have to update counts for all parents, not just the immediate one
// TODO if the performance of this is terrible, it might be better to do this as part of a
// scheduled query... although the counts would often be wrong.
//
// The child_count query for reference:
// select c.id, c.path, count(c2.id) as child_count from comment c
// left join comment c2 on c2.path <@ c.path and c2.path != c.path
// group by c.id
let parent_id = parent_path.0.split('.').nth(1);
if let Some(parent_id) = parent_id {
let top_parent = format!("0.{}", parent_id);
let update_child_count_stmt = format!(
"
conn
.build_transaction()
.run(|conn| {
Box::pin(async move {
// Insert, to get the id
let inserted_comment = insert_into(comment)
.values(comment_form)
.on_conflict(ap_id)
.do_update()
.set(comment_form)
.get_result::<Self>(conn)
.await?;
let comment_id = inserted_comment.id;
// You need to update the ltree column
let ltree = Ltree(if let Some(parent_path) = parent_path {
// The previous parent will already have 0 in it
// Append this comment id
format!("{}.{}", parent_path.0, comment_id)
} else {
// '0' is always the first path, append to that
format!("{}.{}", 0, comment_id)
});
let updated_comment = diesel::update(comment.find(comment_id))
.set(path.eq(ltree))
.get_result::<Self>(conn)
.await?;
// Update the child count for the parent comment_aggregates
// You could do this with a trigger, but since you have to do this manually anyway,
// you can just have it here
if let Some(parent_path) = parent_path {
// You have to update counts for all parents, not just the immediate one
// TODO if the performance of this is terrible, it might be better to do this as part of a
// scheduled query... although the counts would often be wrong.
//
// The child_count query for reference:
// select c.id, c.path, count(c2.id) as child_count from comment c
// left join comment c2 on c2.path <@ c.path and c2.path != c.path
// group by c.id
let parent_id = parent_path.0.split('.').nth(1);
if let Some(parent_id) = parent_id {
let top_parent = format!("0.{}", parent_id);
let update_child_count_stmt = format!(
"
update comment_aggregates ca set child_count = c.child_count
from (
select c.id, c.path, count(c2.id) as child_count from comment c
@ -113,15 +116,15 @@ from (
group by c.id
) as c
where ca.comment_id = c.id"
);
sql_query(update_child_count_stmt).execute(conn).await?;
}
}
updated_comment
} else {
inserted_comment
}
);
sql_query(update_child_count_stmt).execute(conn).await?;
}
}
Ok(updated_comment)
}) as _
})
.await
}
pub async fn read_from_apub_id(
pool: &mut DbPool<'_>,

@ -13,12 +13,17 @@ use crate::{
federation_queue_state::FederationQueueState,
instance::{Instance, InstanceForm},
},
utils::{functions::lower, get_conn, naive_now, now, DbPool},
utils::{
functions::{coalesce, lower},
get_conn,
naive_now,
now,
DbPool,
},
};
use diesel::{
dsl::{count_star, insert_into},
result::Error,
sql_types::{Nullable, Timestamptz},
ExpressionMethods,
NullableExpressionMethods,
QueryDsl,
@ -157,5 +162,3 @@ impl Instance {
.await
}
}
sql_function! { fn coalesce(x: Nullable<Timestamptz>, y: Timestamptz) -> Timestamptz; }

@ -1,4 +1,3 @@
use super::instance::coalesce;
use crate::{
newtypes::{CommunityId, DbUrl, PersonId, PostId},
schema::post::dsl::{
@ -29,7 +28,16 @@ use crate::{
PostUpdateForm,
},
traits::{Crud, Likeable, Saveable},
utils::{get_conn, naive_now, DbPool, DELETED_REPLACEMENT_TEXT, FETCH_LIMIT_MAX},
utils::{
functions::coalesce,
get_conn,
naive_now,
DbPool,
DELETED_REPLACEMENT_TEXT,
FETCH_LIMIT_MAX,
SITEMAP_DAYS,
SITEMAP_LIMIT,
},
};
use ::url::Url;
use chrono::{Duration, Utc};
@ -109,8 +117,9 @@ impl Post {
.filter(local.eq(true))
.filter(deleted.eq(false))
.filter(removed.eq(false))
.filter(published.ge(Utc::now().naive_utc() - Duration::days(1)))
.filter(published.ge(Utc::now().naive_utc() - Duration::days(SITEMAP_DAYS)))
.order(published.desc())
.limit(SITEMAP_LIMIT)
.load::<(DbUrl, chrono::DateTime<Utc>)>(conn)
.await
}

@ -31,7 +31,11 @@ pub mod schema;
#[cfg(feature = "full")]
pub mod aliases {
use crate::schema::{community_moderator, person};
diesel::alias!(person as person1: Person1, person as person2: Person2, community_moderator as community_moderator1: CommunityModerator1);
diesel::alias!(
person as person1: Person1,
person as person2: Person2,
community_moderator as community_moderator1: CommunityModerator1
);
}
pub mod source;
#[cfg(feature = "full")]

@ -24,6 +24,7 @@ use diesel_async::{
pooled_connection::{
deadpool::{Object as PooledConnection, Pool},
AsyncDieselConnectionManager,
ManagerConfig,
},
};
use diesel_migrations::EmbeddedMigrations;
@ -45,7 +46,10 @@ use url::Url;
const FETCH_LIMIT_DEFAULT: i64 = 10;
pub const FETCH_LIMIT_MAX: i64 = 50;
pub const SITEMAP_LIMIT: i64 = 50000;
pub const SITEMAP_DAYS: i64 = 31;
const POOL_TIMEOUT: Option<Duration> = Some(Duration::from_secs(5));
pub const RANK_DEFAULT: f64 = 0.0001;
pub type ActualDbPool = Pool<AsyncPgConnection>;
@ -261,7 +265,9 @@ pub async fn build_db_pool() -> Result<ActualDbPool, LemmyError> {
let manager = if tls_enabled {
// diesel-async does not support any TLS connections out of the box, so we need to manually
// provide a setup function which handles creating the connection
AsyncDieselConnectionManager::<AsyncPgConnection>::new_with_setup(&db_url, establish_connection)
let mut config = ManagerConfig::default();
config.custom_setup = Box::new(establish_connection);
AsyncDieselConnectionManager::<AsyncPgConnection>::new_with_config(&db_url, config)
} else {
AsyncDieselConnectionManager::<AsyncPgConnection>::new(&db_url)
};

@ -41,3 +41,4 @@ actix-web = { workspace = true, optional = true }
[dev-dependencies]
serial_test = { workspace = true }
tokio = { workspace = true }
chrono = { workspace = true }

@ -105,16 +105,18 @@ fn queries<'a>() -> Queries<
query = query.filter(post::community_id.eq(community_id));
}
// If viewing all reports, order by newest, but if viewing unresolved only, show the oldest first (FIFO)
if options.unresolved_only {
query = query.filter(comment_report::resolved.eq(false));
query = query
.filter(comment_report::resolved.eq(false))
.order_by(comment_report::published.asc());
} else {
query = query.order_by(comment_report::published.desc());
}
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
query = query
.order_by(comment_report::published.asc())
.limit(limit)
.offset(offset);
query = query.limit(limit).offset(offset);
// If its not an admin, get only the ones you mod
if !user.local_user.admin {
@ -230,7 +232,7 @@ mod tests {
post::{Post, PostInsertForm},
},
traits::{Crud, Joinable, Reportable},
utils::build_db_pool_for_tests,
utils::{build_db_pool_for_tests, RANK_DEFAULT},
};
use serial_test::serial;
@ -431,7 +433,7 @@ mod tests {
downvotes: 0,
published: agg.published,
child_count: 0,
hot_rank: 0.1728,
hot_rank: RANK_DEFAULT,
controversy_rank: 0.0,
},
my_vote: None,
@ -475,8 +477,8 @@ mod tests {
assert_eq!(
reports,
[
expected_sara_report_view.clone(),
expected_jessica_report_view.clone(),
expected_sara_report_view.clone(),
]
);

@ -177,14 +177,18 @@ fn queries<'a>() -> Queries<
};
let list = move |mut conn: DbConn<'a>, options: CommentQuery<'a>| async move {
let person_id = options.local_user.map(|l| l.person.id);
let local_user_id = options.local_user.map(|l| l.local_user.id);
let my_person_id = options.local_user.map(|l| l.person.id);
let my_local_user_id = options.local_user.map(|l| l.local_user.id);
// The left join below will return None in this case
let person_id_join = person_id.unwrap_or(PersonId(-1));
let local_user_id_join = local_user_id.unwrap_or(LocalUserId(-1));
let person_id_join = my_person_id.unwrap_or(PersonId(-1));
let local_user_id_join = my_local_user_id.unwrap_or(LocalUserId(-1));
let mut query = all_joins(comment::table.into_boxed(), person_id, options.saved_only);
let mut query = all_joins(
comment::table.into_boxed(),
my_person_id,
options.saved_only,
);
if let Some(creator_id) = options.creator_id {
query = query.filter(comment::creator_id.eq(creator_id));
@ -373,7 +377,6 @@ pub struct CommentQuery<'a> {
pub saved_only: bool,
pub liked_only: bool,
pub disliked_only: bool,
pub is_profile_view: bool,
pub page: Option<i64>,
pub limit: Option<i64>,
pub max_depth: Option<i32>,
@ -410,7 +413,7 @@ mod tests {
post::{Post, PostInsertForm},
},
traits::{Blockable, Crud, Joinable, Likeable},
utils::build_db_pool_for_tests,
utils::{build_db_pool_for_tests, RANK_DEFAULT},
SubscribedType,
};
use serial_test::serial;
@ -1045,7 +1048,7 @@ mod tests {
downvotes: 0,
published: agg.published,
child_count: 5,
hot_rank: 0.1728,
hot_rank: RANK_DEFAULT,
controversy_rank: 0.0,
},
}

@ -5,7 +5,14 @@ use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::{LocalUserId, PersonId},
schema::{local_user, person, person_aggregates},
utils::{functions::lower, DbConn, DbPool, ListFn, Queries, ReadFn},
utils::{
functions::{coalesce, lower},
DbConn,
DbPool,
ListFn,
Queries,
ReadFn,
},
};
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use std::future::{ready, Ready};
@ -34,7 +41,9 @@ fn queries<'a>(
let mut query = local_user::table.into_boxed();
query = match search {
ReadBy::Id(local_user_id) => query.filter(local_user::id.eq(local_user_id)),
ReadBy::Email(from_email) => query.filter(local_user::email.eq(from_email)),
ReadBy::Email(from_email) => {
query.filter(lower(coalesce(local_user::email, "")).eq(from_email.to_lowercase()))
}
_ => query,
};
let mut query = query.inner_join(person::table);
@ -43,8 +52,8 @@ fn queries<'a>(
ReadBy::Name(name) => query.filter(lower(person::name).eq(name.to_lowercase())),
ReadBy::NameOrEmail(name_or_email) => query.filter(
lower(person::name)
.eq(lower(name_or_email))
.or(local_user::email.eq(name_or_email)),
.eq(lower(name_or_email.to_lowercase()))
.or(lower(coalesce(local_user::email, "")).eq(name_or_email.to_lowercase())),
),
_ => query,
};

@ -83,16 +83,18 @@ fn queries<'a>() -> Queries<
query = query.filter(post::community_id.eq(community_id));
}
// If viewing all reports, order by newest, but if viewing unresolved only, show the oldest first (FIFO)
if options.unresolved_only {
query = query.filter(post_report::resolved.eq(false));
query = query
.filter(post_report::resolved.eq(false))
.order_by(post_report::published.asc());
} else {
query = query.order_by(post_report::published.desc());
}
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
query = query
.order_by(post_report::published.asc())
.limit(limit)
.offset(offset);
query = query.limit(limit).offset(offset);
// If its not an admin, get only the ones you mod
if !user.local_user.admin {
@ -337,8 +339,8 @@ mod tests {
.await
.unwrap();
assert_eq!(reports[0].creator.id, inserted_sara.id);
assert_eq!(reports[1].creator.id, inserted_jessica.id);
assert_eq!(reports[1].creator.id, inserted_sara.id);
assert_eq!(reports[0].creator.id, inserted_jessica.id);
// Make sure the counts are correct
let report_count = PostReportView::get_report_count(pool, inserted_timmy.id, false, None)

@ -1,15 +1,12 @@
use crate::structs::{LocalUserView, PaginationCursor, PostView};
use diesel::{
debug_query,
dsl::{self, exists, not, IntervalDsl},
expression::AsExpression,
dsl::{exists, not, IntervalDsl},
pg::Pg,
result::Error,
sql_function,
sql_types::{self, SingleValue, SqlType, Timestamptz},
sql_types,
BoolExpressionMethods,
BoxableExpression,
Expression,
ExpressionMethods,
IntoSql,
JoinOnDsl,
@ -35,66 +32,54 @@ use lemmy_db_schema::{
person_block,
person_post_aggregates,
post,
post_aggregates::{self, newest_comment_time},
post_aggregates,
post_like,
post_read,
post_saved,
},
utils::{fuzzy_search, get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
utils::{
functions::coalesce,
fuzzy_search,
get_conn,
limit_and_offset,
now,
DbConn,
DbPool,
ListFn,
Queries,
ReadFn,
},
ListingType,
SortType,
};
use tracing::debug;
sql_function!(fn coalesce(x: sql_types::Nullable<sql_types::BigInt>, y: sql_types::BigInt) -> sql_types::BigInt);
fn order_and_page_filter_desc<Q, C, T>(
query: Q,
column: C,
options: &PostQuery,
getter: impl Fn(&PostAggregates) -> T,
) -> Q
where
Q: diesel::query_dsl::methods::ThenOrderDsl<dsl::Desc<C>, Output = Q>
+ diesel::query_dsl::methods::ThenOrderDsl<dsl::Asc<C>, Output = Q>
+ diesel::query_dsl::methods::FilterDsl<dsl::GtEq<C, T>, Output = Q>
+ diesel::query_dsl::methods::FilterDsl<dsl::LtEq<C, T>, Output = Q>,
C: Expression + Copy,
C::SqlType: SingleValue + SqlType,
T: AsExpression<C::SqlType>,
{
let mut query = query.then_order_by(column.desc());
if let Some(before) = &options.page_before_or_equal {
query = query.filter(column.ge(getter(&before.0)));
}
if let Some(after) = &options.page_after {
query = query.filter(column.le(getter(&after.0)));
}
query
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Ord {
Desc,
Asc,
}
fn order_and_page_filter_asc<Q, C, T>(
query: Q,
column: C,
options: &PostQuery,
getter: impl Fn(&PostAggregates) -> T,
) -> Q
where
Q: diesel::query_dsl::methods::ThenOrderDsl<dsl::Asc<C>, Output = Q>
+ diesel::query_dsl::methods::FilterDsl<dsl::LtEq<C, T>, Output = Q>
+ diesel::query_dsl::methods::FilterDsl<dsl::GtEq<C, T>, Output = Q>,
C: Expression + Copy,
C::SqlType: SingleValue + SqlType,
T: AsExpression<C::SqlType>,
{
let mut query = query.then_order_by(column.asc());
if let Some(before) = &options.page_before_or_equal {
query = query.filter(column.le(getter(&before.0)));
}
if let Some(after) = &options.page_after {
query = query.filter(column.ge(getter(&after.0)));
}
query
struct PaginationCursorField<Q, QS> {
then_order_by_desc: fn(Q) -> Q,
then_order_by_asc: fn(Q) -> Q,
le: fn(&PostAggregates) -> Box<dyn BoxableExpression<QS, Pg, SqlType = sql_types::Bool>>,
ge: fn(&PostAggregates) -> Box<dyn BoxableExpression<QS, Pg, SqlType = sql_types::Bool>>,
ne: fn(&PostAggregates) -> Box<dyn BoxableExpression<QS, Pg, SqlType = sql_types::Bool>>,
}
/// Returns `PaginationCursorField<_, _>` for the given name
macro_rules! field {
($name:ident) => {
// Type inference doesn't work if normal method call syntax is used
PaginationCursorField {
then_order_by_desc: |query| QueryDsl::then_order_by(query, post_aggregates::$name.desc()),
then_order_by_asc: |query| QueryDsl::then_order_by(query, post_aggregates::$name.asc()),
le: |e| Box::new(post_aggregates::$name.le(e.$name)),
ge: |e| Box::new(post_aggregates::$name.ge(e.$name)),
ne: |e| Box::new(post_aggregates::$name.ne(e.$name)),
}
};
}
fn queries<'a>() -> Queries<
@ -274,8 +259,16 @@ fn queries<'a>() -> Queries<
// Hide deleted and removed for non-admins or mods
if !is_mod_or_admin {
query = query
.filter(community::removed.eq(false))
.filter(post::removed.eq(false))
.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
@ -293,16 +286,16 @@ fn queries<'a>() -> Queries<
};
let list = move |mut conn: DbConn<'a>, options: PostQuery<'a>| async move {
let person_id = options.local_user.map(|l| l.person.id);
let local_user_id = options.local_user.map(|l| l.local_user.id);
let my_person_id = options.local_user.map(|l| l.person.id);
let my_local_user_id = options.local_user.map(|l| l.local_user.id);
// The left join below will return None in this case
let person_id_join = person_id.unwrap_or(PersonId(-1));
let local_user_id_join = local_user_id.unwrap_or(LocalUserId(-1));
let person_id_join = my_person_id.unwrap_or(PersonId(-1));
let local_user_id_join = my_local_user_id.unwrap_or(LocalUserId(-1));
let mut query = all_joins(
post_aggregates::table.into_boxed(),
person_id,
my_person_id,
options.saved_only,
);
@ -310,7 +303,7 @@ fn queries<'a>() -> Queries<
query = query.filter(community::deleted.eq(false));
// only show deleted posts to creator
if let Some(person_id) = person_id {
if let Some(person_id) = my_person_id {
query = query.filter(post::deleted.eq(false).or(post::creator_id.eq(person_id)));
} else {
query = query.filter(post::deleted.eq(false));
@ -321,21 +314,11 @@ fn queries<'a>() -> Queries<
.map(|l| l.local_user.admin)
.unwrap_or(false);
// only show removed posts to admin when viewing user profile
if !(options.is_profile_view && is_admin) {
if !(options.creator_id.is_some() && is_admin) {
query = query
.filter(community::removed.eq(false))
.filter(post::removed.eq(false));
}
if options.community_id.is_none() || options.community_id_just_for_prefetch {
query = order_and_page_filter_desc(query, post_aggregates::featured_local, &options, |e| {
e.featured_local
});
} else {
query =
order_and_page_filter_desc(query, post_aggregates::featured_community, &options, |e| {
e.featured_community
});
}
if let Some(community_id) = options.community_id {
query = query.filter(post_aggregates::community_id.eq(community_id));
}
@ -344,41 +327,47 @@ fn queries<'a>() -> Queries<
query = query.filter(post_aggregates::creator_id.eq(creator_id));
}
if let Some(person_id) = person_id {
let is_subscribed = exists(
community_follower::table.filter(
post_aggregates::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id)),
),
);
match options.listing_type.unwrap_or_default() {
ListingType::Subscribed => query = query.filter(is_subscribed),
ListingType::Local => {
query = query
.filter(community::local.eq(true))
.filter(community::hidden.eq(false).or(is_subscribed));
}
ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)),
ListingType::ModeratorView => {
query = query.filter(exists(
community_moderator::table.filter(
post::community_id
.eq(community_moderator::community_id)
.and(community_moderator::person_id.eq(person_id)),
),
));
if let Some(listing_type) = options.listing_type {
if let Some(person_id) = my_person_id {
let is_subscribed = exists(
community_follower::table.filter(
post_aggregates::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id)),
),
);
match listing_type {
ListingType::Subscribed => query = query.filter(is_subscribed),
ListingType::Local => {
query = query
.filter(community::local.eq(true))
.filter(community::hidden.eq(false).or(is_subscribed));
}
ListingType::All => query = query.filter(community::hidden.eq(false).or(is_subscribed)),
ListingType::ModeratorView => {
query = query.filter(exists(
community_moderator::table.filter(
post::community_id
.eq(community_moderator::community_id)
.and(community_moderator::person_id.eq(person_id)),
),
));
}
}
}
} else {
match options.listing_type.unwrap_or_default() {
ListingType::Local => {
query = query
.filter(community::local.eq(true))
.filter(community::hidden.eq(false));
// If your person_id is missing, only show local
else {
match listing_type {
ListingType::Local => {
query = query
.filter(community::local.eq(true))
.filter(community::hidden.eq(false));
}
_ => query = query.filter(community::hidden.eq(false)),
}
_ => query = query.filter(community::hidden.eq(false)),
}
} else {
query = query.filter(community::hidden.eq(false));
}
if let Some(url_search) = &options.url_search {
@ -412,7 +401,7 @@ fn queries<'a>() -> Queries<
query = query.filter(person::bot_account.eq(false));
};
if let (true, Some(person_id)) = (options.saved_only, person_id) {
if let (true, Some(person_id)) = (options.saved_only, my_person_id) {
query = query.filter(is_saved(person_id));
}
// Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read
@ -423,12 +412,13 @@ fn queries<'a>() -> Queries<
.unwrap_or(true)
{
// Do not hide read posts when it is a user profile view
if let (false, Some(person_id)) = (options.is_profile_view, person_id) {
// Or, only hide read posts on non-profile views
if let (None, Some(person_id)) = (options.creator_id, my_person_id) {
query = query.filter(not(is_read(person_id)));
}
}
if let Some(person_id) = person_id {
if let Some(person_id) = my_person_id {
if options.liked_only {
query = query.filter(score(person_id).eq(1));
} else if options.disliked_only {
@ -438,7 +428,7 @@ fn queries<'a>() -> Queries<
// Dont filter blocks or missing languages for moderator view type
if let (Some(person_id), false) = (
person_id,
my_person_id,
options.listing_type.unwrap_or_default() == ListingType::ModeratorView,
) {
// Filter out the rows with missing languages
@ -467,85 +457,95 @@ fn queries<'a>() -> Queries<
)));
query = query.filter(not(is_creator_blocked(person_id)));
}
let now = diesel::dsl::now.into_sql::<Timestamptz>();
let featured_field = if options.community_id.is_none() || options.community_id_just_for_prefetch
{
use post_aggregates::{
comments,
controversy_rank,
hot_rank,
hot_rank_active,
published,
scaled_rank,
score,
field!(featured_local)
} else {
field!(featured_community)
};
let (main_sort, top_sort_interval) = match options.sort.unwrap_or(SortType::Hot) {
SortType::Active => ((Ord::Desc, field!(hot_rank_active)), None),
SortType::Hot => ((Ord::Desc, field!(hot_rank)), None),
SortType::Scaled => ((Ord::Desc, field!(scaled_rank)), None),
SortType::Controversial => ((Ord::Desc, field!(controversy_rank)), None),
SortType::New => ((Ord::Desc, field!(published)), None),
SortType::Old => ((Ord::Asc, field!(published)), None),
SortType::NewComments => ((Ord::Desc, field!(newest_comment_time)), None),
SortType::MostComments => ((Ord::Desc, field!(comments)), None),
SortType::TopAll => ((Ord::Desc, field!(score)), None),
SortType::TopYear => ((Ord::Desc, field!(score)), Some(1.years())),
SortType::TopMonth => ((Ord::Desc, field!(score)), Some(1.months())),
SortType::TopWeek => ((Ord::Desc, field!(score)), Some(1.weeks())),
SortType::TopDay => ((Ord::Desc, field!(score)), Some(1.days())),
SortType::TopHour => ((Ord::Desc, field!(score)), Some(1.hours())),
SortType::TopSixHour => ((Ord::Desc, field!(score)), Some(6.hours())),
SortType::TopTwelveHour => ((Ord::Desc, field!(score)), Some(12.hours())),
SortType::TopThreeMonths => ((Ord::Desc, field!(score)), Some(3.months())),
SortType::TopSixMonths => ((Ord::Desc, field!(score)), Some(6.months())),
SortType::TopNineMonths => ((Ord::Desc, field!(score)), Some(9.months())),
};
if let Some(interval) = top_sort_interval {
query = query.filter(post_aggregates::published.gt(now() - interval));
}
let sorts = [
Some((Ord::Desc, featured_field)),
Some(main_sort),
Some((Ord::Desc, field!(post_id))),
];
let sorts_iter = sorts.iter().flatten();
// This loop does almost the same thing as sorting by and comparing tuples. If the rows were
// only sorted by 1 field called `foo` in descending order, then it would be like this:
//
// ```
// query = query.then_order_by(foo.desc());
// if let Some(first) = &options.page_after {
// query = query.filter(foo.le(first.foo));
// }
// if let Some(last) = &page_before_or_equal {
// query = query.filter(foo.ge(last.foo));
// }
// ```
//
// If multiple rows have the same value for a sorted field, then they are
// grouped together, and the rows in that group are sorted by the next fields.
// When checking if a row is within the range determined by the cursors, a field
// that's sorted after other fields is only compared if the row and the cursor
// are in the same group created by the previous sort, which is checked by using
// `or` to skip the comparison if any previously sorted field is not equal.
for (i, (order, field)) in sorts_iter.clone().enumerate() {
// Both cursors are treated as inclusive here. `page_after` is made exclusive
// by adding `1` to the offset.
let (then_order_by_field, compare_first, compare_last) = match order {
Ord::Desc => (field.then_order_by_desc, field.le, field.ge),
Ord::Asc => (field.then_order_by_asc, field.ge, field.le),
};
match options.sort.as_ref().unwrap_or(&SortType::Hot) {
SortType::Active => {
query =
order_and_page_filter_desc(query, hot_rank_active, &options, |e| e.hot_rank_active);
query = order_and_page_filter_desc(query, published, &options, |e| e.published);
}
SortType::Hot => {
query = order_and_page_filter_desc(query, hot_rank, &options, |e| e.hot_rank);
query = order_and_page_filter_desc(query, published, &options, |e| e.published);
}
SortType::Scaled => {
query = order_and_page_filter_desc(query, scaled_rank, &options, |e| e.scaled_rank);
query = order_and_page_filter_desc(query, published, &options, |e| e.published);
}
SortType::Controversial => {
query =
order_and_page_filter_desc(query, controversy_rank, &options, |e| e.controversy_rank);
query = order_and_page_filter_desc(query, published, &options, |e| e.published);
}
SortType::New => {
query = order_and_page_filter_desc(query, published, &options, |e| e.published)
}
SortType::Old => {
query = order_and_page_filter_asc(query, published, &options, |e| e.published)
}
SortType::NewComments => {
query = order_and_page_filter_desc(query, newest_comment_time, &options, |e| {
e.newest_comment_time
})
}
SortType::MostComments => {
query = order_and_page_filter_desc(query, comments, &options, |e| e.comments);
query = order_and_page_filter_desc(query, published, &options, |e| e.published);
}
SortType::TopAll => {
query = order_and_page_filter_desc(query, score, &options, |e| e.score);
query = order_and_page_filter_desc(query, published, &options, |e| e.published);
}
o @ (SortType::TopYear
| SortType::TopMonth
| SortType::TopWeek
| SortType::TopDay
| SortType::TopHour
| SortType::TopSixHour
| SortType::TopTwelveHour
| SortType::TopThreeMonths
| SortType::TopSixMonths
| SortType::TopNineMonths) => {
let interval = match o {
SortType::TopYear => 1.years(),
SortType::TopMonth => 1.months(),
SortType::TopWeek => 1.weeks(),
SortType::TopDay => 1.days(),
SortType::TopHour => 1.hours(),
SortType::TopSixHour => 6.hours(),
SortType::TopTwelveHour => 12.hours(),
SortType::TopThreeMonths => 3.months(),
SortType::TopSixMonths => 6.months(),
SortType::TopNineMonths => 9.months(),
_ => return Err(Error::NotFound),
};
query = query.filter(post_aggregates::published.gt(now - interval));
query = order_and_page_filter_desc(query, score, &options, |e| e.score);
query = order_and_page_filter_desc(query, published, &options, |e| e.published);
query = then_order_by_field(query);
for (cursor_data, compare) in [
(&options.page_after, compare_first),
(&options.page_before_or_equal, compare_last),
] {
let Some(cursor_data) = cursor_data else {
continue;
};
let mut condition: Box<dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>> =
Box::new(compare(&cursor_data.0));
// For each field that was sorted before the current one, skip the filter by changing
// `condition` to `true` if the row's value doesn't equal the cursor's value.
for (_, other_field) in sorts_iter.clone().take(i) {
condition = Box::new(condition.or((other_field.ne)(&cursor_data.0)));
}
query = query.filter(condition);
}
};
}
let (limit, mut offset) = limit_and_offset(options.page, options.limit)?;
if options.page_after.is_some() {
@ -621,8 +621,6 @@ pub struct PostQuery<'a> {
pub saved_only: bool,
pub liked_only: bool,
pub disliked_only: bool,
pub moderator_view: bool,
pub is_profile_view: bool,
pub page: Option<i64>,
pub limit: Option<i64>,
pub page_after: Option<PaginationCursorData>,
@ -725,15 +723,17 @@ mod tests {
#![allow(clippy::indexing_slicing)]
use crate::{
post_view::{PostQuery, PostView},
post_view::{PaginationCursorData, PostQuery, PostView},
structs::LocalUserView,
};
use chrono::Utc;
use lemmy_db_schema::{
aggregates::structs::PostAggregates,
impls::actor_language::UNDETERMINED_ID,
newtypes::LanguageId,
source::{
actor_language::LocalUserLanguage,
comment::{Comment, CommentInsertForm},
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
community_block::{CommunityBlock, CommunityBlockForm},
instance::Instance,
@ -742,22 +742,24 @@ mod tests {
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
person::{Person, PersonInsertForm},
person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm},
},
traits::{Blockable, Crud, Joinable, Likeable},
utils::{build_db_pool_for_tests, DbPool},
utils::{build_db_pool_for_tests, DbPool, RANK_DEFAULT},
SortType,
SubscribedType,
};
use serial_test::serial;
use std::{collections::HashSet, time::Duration};
struct Data {
inserted_instance: Instance,
local_user_view: LocalUserView,
inserted_blocked_person: Person,
blocked_local_user_view: LocalUserView,
inserted_bot: Person,
inserted_community: Community,
inserted_post: Post,
inserted_bot_post: Post,
}
async fn init_data(pool: &mut DbPool<'_>) -> Data {
@ -809,6 +811,14 @@ mod tests {
let inserted_blocked_person = Person::create(pool, &blocked_person).await.unwrap();
let blocked_local_user_form = LocalUserInsertForm::builder()
.person_id(inserted_blocked_person.id)
.password_encrypted(String::new())
.build();
let inserted_blocked_local_user = LocalUser::create(pool, &blocked_local_user_form)
.await
.unwrap();
let post_from_blocked_person = PostInsertForm::builder()
.name("blocked_person_post".to_string())
.creator_id(inserted_blocked_person.id)
@ -842,20 +852,26 @@ mod tests {
.community_id(inserted_community.id)
.build();
let _inserted_bot_post = Post::create(pool, &new_bot_post).await.unwrap();
let inserted_bot_post = Post::create(pool, &new_bot_post).await.unwrap();
let local_user_view = LocalUserView {
local_user: inserted_local_user,
person: inserted_person,
counts: Default::default(),
};
let blocked_local_user_view = LocalUserView {
local_user: inserted_blocked_local_user,
person: inserted_blocked_person,
counts: Default::default(),
};
Data {
inserted_instance,
local_user_view,
inserted_blocked_person,
blocked_local_user_view,
inserted_bot,
inserted_community,
inserted_post,
inserted_bot_post,
}
}
@ -1245,7 +1261,7 @@ mod tests {
// Remove the post
Post::update(
pool,
data.inserted_post.id,
data.inserted_bot_post.id,
&PostUpdateForm {
removed: Some(true),
..Default::default()
@ -1265,18 +1281,21 @@ mod tests {
.unwrap();
assert_eq!(1, post_listings_no_admin.len());
// Removed post is shown to admins on profile page
// Removed bot post is shown to admins on its profile page
data.local_user_view.local_user.admin = true;
let post_listings_is_admin = PostQuery {
sort: Some(SortType::New),
creator_id: Some(data.inserted_bot.id),
local_user: Some(&data.local_user_view),
is_profile_view: true,
..Default::default()
}
.list(pool)
.await
.unwrap();
assert_eq!(2, post_listings_is_admin.len());
assert_eq!(
data.inserted_bot.id,
post_listings_is_admin[0].post.creator_id
);
cleanup(data, pool).await;
}
@ -1300,34 +1319,25 @@ mod tests {
.await
.unwrap();
// Make sure you don't see the deleted post in the results
let post_listings_no_creator = PostQuery {
sort: Some(SortType::New),
..Default::default()
}
.list(pool)
.await
.unwrap();
let not_contains_deleted = post_listings_no_creator
// Deleted post is only shown to creator
for (local_user, expect_contains_deleted) in [
(None, false),
(Some(&data.blocked_local_user_view), false),
(Some(&data.local_user_view), true),
] {
let contains_deleted = PostQuery {
sort: Some(SortType::New),
local_user,
..Default::default()
}
.list(pool)
.await
.unwrap()
.iter()
.map(|p| p.post.id)
.all(|p| p != data.inserted_post.id);
assert!(not_contains_deleted);
.any(|p| p.post.id == data.inserted_post.id);
// Deleted post is shown to creator
let post_listings_is_creator = PostQuery {
sort: Some(SortType::New),
local_user: Some(&data.local_user_view),
..Default::default()
assert_eq!(expect_contains_deleted, contains_deleted);
}
.list(pool)
.await
.unwrap();
let contains_deleted = post_listings_is_creator
.iter()
.map(|p| p.post.id)
.any(|p| p == data.inserted_post.id);
assert!(contains_deleted);
cleanup(data, pool).await;
}
@ -1410,6 +1420,125 @@ mod tests {
cleanup(data, pool).await;
}
#[tokio::test]
#[serial]
async fn pagination_includes_each_post_once() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
let community_form = CommunityInsertForm::builder()
.name("yes".to_string())
.title("yes".to_owned())
.public_key("pubkey".to_string())
.instance_id(data.inserted_instance.id)
.build();
let inserted_community = Community::create(pool, &community_form).await.unwrap();
let mut inserted_post_ids = vec![];
let mut inserted_comment_ids = vec![];
// Create 150 posts with varying non-correlating values for publish date, number of comments, and featured
for comments in 0..10 {
for _ in 0..15 {
let post_form = PostInsertForm::builder()
.name("keep Christ in Christmas".to_owned())
.creator_id(data.local_user_view.person.id)
.community_id(inserted_community.id)
.featured_local(Some((comments % 2) == 0))
.featured_community(Some((comments % 2) == 0))
.published(Some(Utc::now() - Duration::from_secs(comments % 3)))
.build();
let inserted_post = Post::create(pool, &post_form).await.unwrap();
inserted_post_ids.push(inserted_post.id);
for _ in 0..comments {
let comment_form = CommentInsertForm::builder()
.creator_id(data.local_user_view.person.id)
.post_id(inserted_post.id)
.content("yes".to_owned())
.build();
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
inserted_comment_ids.push(inserted_comment.id);
}
}
}
let mut listed_post_ids = vec![];
let mut page_after = None;
loop {
let post_listings = PostQuery {
community_id: Some(inserted_community.id),
sort: Some(SortType::MostComments),
limit: Some(10),
page_after,
..Default::default()
}
.list(pool)
.await
.unwrap();
listed_post_ids.extend(post_listings.iter().map(|p| p.post.id));
if let Some(p) = post_listings.into_iter().last() {
page_after = Some(PaginationCursorData(p.counts));
} else {
break;
}
}
inserted_post_ids.sort_unstable_by_key(|id| id.0);
listed_post_ids.sort_unstable_by_key(|id| id.0);
assert_eq!(inserted_post_ids, listed_post_ids);
Community::delete(pool, inserted_community.id)
.await
.unwrap();
cleanup(data, pool).await;
}
#[tokio::test]
#[serial]
async fn post_listings_hide_read() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let mut data = init_data(pool).await;
// Make sure local user hides read posts
let local_user_form = LocalUserUpdateForm {
show_read_posts: Some(false),
..Default::default()
};
let inserted_local_user =
LocalUser::update(pool, data.local_user_view.local_user.id, &local_user_form)
.await
.unwrap();
data.local_user_view.local_user = inserted_local_user;
// Mark a post as read
PostRead::mark_as_read(
pool,
HashSet::from([data.inserted_bot_post.id]),
data.local_user_view.person.id,
)
.await
.unwrap();
// Make sure you don't see the read post in the results
let post_listings_hide_read = PostQuery {
sort: Some(SortType::New),
local_user: Some(&data.local_user_view),
..Default::default()
}
.list(pool)
.await
.unwrap();
assert_eq!(1, post_listings_hide_read.len());
cleanup(data, pool).await;
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
let num_deleted = Post::delete(pool, data.inserted_post.id).await.unwrap();
Community::delete(pool, data.inserted_community.id)
@ -1419,7 +1548,7 @@ mod tests {
.await
.unwrap();
Person::delete(pool, data.inserted_bot.id).await.unwrap();
Person::delete(pool, data.inserted_blocked_person.id)
Person::delete(pool, data.blocked_local_user_view.person.id)
.await
.unwrap();
Instance::delete(pool, data.inserted_instance.id)
@ -1526,10 +1655,10 @@ mod tests {
newest_comment_time: inserted_post.published,
featured_community: false,
featured_local: false,
hot_rank: 0.1728,
hot_rank_active: 0.1728,
hot_rank: RANK_DEFAULT,
hot_rank_active: RANK_DEFAULT,
controversy_rank: 0.0,
scaled_rank: 0.3621,
scaled_rank: RANK_DEFAULT,
community_id: inserted_post.community_id,
creator_id: inserted_post.creator_id,
instance_id: data.inserted_instance.id,

@ -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();
}
}

@ -49,8 +49,13 @@ fn queries<'a>() -> Queries<
let list = move |mut conn: DbConn<'a>, options: RegistrationApplicationQuery| async move {
let mut query = all_joins(registration_application::table.into_boxed());
// If viewing all applications, order by newest, but if viewing unresolved only, show the oldest first (FIFO)
if options.unread_only {
query = query.filter(registration_application::admin_id.is_null())
query = query
.filter(registration_application::admin_id.is_null())
.order_by(registration_application::published.asc());
} else {
query = query.order_by(registration_application::published.desc());
}
if options.verified_email_only {
@ -59,10 +64,7 @@ fn queries<'a>() -> Queries<
let (limit, offset) = limit_and_offset(options.page, options.limit)?;
query = query
.limit(limit)
.offset(offset)
.order_by(registration_application::published.asc());
query = query.limit(limit).offset(offset);
query.load::<RegistrationApplicationView>(&mut conn).await
};

@ -1,6 +1,5 @@
use crate::structs::PersonView;
use diesel::{
dsl::exists,
pg::Pg,
result::Error,
BoolExpressionMethods,
@ -13,7 +12,17 @@ use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::PersonId,
schema::{local_user, person, person_aggregates},
utils::{fuzzy_search, limit_and_offset, now, DbConn, DbPool, ListFn, Queries, ReadFn},
utils::{
functions::coalesce,
fuzzy_search,
limit_and_offset,
now,
DbConn,
DbPool,
ListFn,
Queries,
ReadFn,
},
SortType,
};
use serde::{Deserialize, Serialize};
@ -48,21 +57,15 @@ fn post_to_person_sort_type(sort: SortType) -> PersonSortType {
fn queries<'a>(
) -> Queries<impl ReadFn<'a, PersonView, PersonId>, impl ListFn<'a, PersonView, ListMode>> {
let creator_is_admin = exists(
local_user::table.filter(
person::id
.eq(local_user::person_id)
.and(local_user::admin.eq(true)),
),
);
let all_joins = move |query: person::BoxedQuery<'a, Pg>| {
query
.inner_join(person_aggregates::table)
.left_join(local_user::table)
.filter(person::deleted.eq(false))
.select((
person::all_columns,
person_aggregates::all_columns,
creator_is_admin,
coalesce(local_user::admin.nullable(), false),
))
};
@ -77,7 +80,7 @@ fn queries<'a>(
match mode {
ListMode::Admins => {
query = query
.filter(creator_is_admin.eq(true))
.filter(local_user::admin.eq(true))
.filter(person::deleted.eq(false))
.order_by(person::published);
}

@ -1,5 +1,6 @@
[package]
name = "lemmy_federate"
publish = false
version.workspace = true
edition.workspace = true
description.workspace = true
@ -30,5 +31,5 @@ reqwest.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["full"] }
tracing.workspace = true
moka = { version = "0.11.3", features = ["future"] }
tokio-util = "0.7.9"
moka.workspace = true
tokio-util = "0.7.10"

@ -154,7 +154,6 @@ async fn receive_print_stats(
tokio::select! {
ele = receiver.recv() => {
let Some((domain, ele)) = ele else {
tracing::info!("done. quitting");
print_stats(pool, &stats).await;
return;
};
@ -181,9 +180,9 @@ async fn print_stats(pool: &mut DbPool<'_>, stats: &HashMap<String, FederationQu
.expect("0 is valid nanos")
.to_rfc3339()
);
// todo: less noisy output (only output failing instances and summary for successful)
// todo: more stats (act/sec, avg http req duration)
let mut ok_count = 0;
let mut behind_count = 0;
for (domain, stat) in stats {
let behind = last_id.0 - stat.last_successful_id.map(|e| e.0).unwrap_or(0);
if stat.fail_count > 0 {
@ -195,10 +194,11 @@ async fn print_stats(pool: &mut DbPool<'_>, stats: &HashMap<String, FederationQu
federate_retry_sleep_duration(stat.fail_count)
);
} else if behind > 0 {
tracing::info!("{}: Ok. {} behind", domain, behind);
tracing::debug!("{}: Ok. {} activities behind", domain, behind);
behind_count += 1;
} else {
ok_count += 1;
}
}
tracing::info!("{ok_count} others up to date");
tracing::info!("{ok_count} others up to date. {behind_count} instances behind.");
}

@ -171,6 +171,7 @@ impl InstanceWorker {
.await
.context("failed reading activity from db")?
else {
tracing::debug!("{}: {:?} does not exist", self.instance.domain, id);
self.state.last_successful_id = Some(id);
continue;
};
@ -221,7 +222,7 @@ impl InstanceWorker {
SendActivityTask::prepare(object, actor.as_ref(), inbox_urls, &self.context).await?;
for task in requests {
// usually only one due to shared inbox
tracing::info!("sending out {}", task);
tracing::debug!("sending out {}", task);
while let Err(e) = task.sign_and_send(&self.context).await {
self.state.fail_count += 1;
self.state.last_retry = Some(Utc::now());

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

@ -25,13 +25,7 @@ use lemmy_utils::{
utils::markdown::{markdown_to_html, sanitize_html},
};
use once_cell::sync::Lazy;
use rss::{
extension::dublincore::DublinCoreExtensionBuilder,
ChannelBuilder,
GuidBuilder,
Item,
ItemBuilder,
};
use rss::{extension::dublincore::DublinCoreExtension, Channel, Guid, Item};
use serde::Deserialize;
use std::{collections::BTreeMap, str::FromStr};
@ -146,18 +140,19 @@ async fn get_feed_data(
let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?;
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.clone())
.title(&format!("{} - {}", site_view.site.name, listing_type))
.link(context.settings().get_protocol_and_hostname())
.items(items);
let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - {}", site_view.site.name, listing_type),
link: context.settings().get_protocol_and_hostname(),
items,
..Default::default()
};
if let Some(site_desc) = site_view.site.description {
channel_builder.description(&site_desc);
channel.set_description(&site_desc);
}
let rss = channel_builder.build().to_string();
let rss = channel.to_string();
Ok(
HttpResponse::Ok()
.content_type("application/rss+xml")
@ -217,7 +212,7 @@ async fn get_feed(
}
.map_err(ErrorBadRequest)?;
let rss = builder.build().to_string();
let rss = builder.to_string();
Ok(
HttpResponse::Ok()
@ -233,7 +228,7 @@ async fn get_feed_user(
limit: &i64,
page: &i64,
user_name: &str,
) -> Result<ChannelBuilder, LemmyError> {
) -> Result<Channel, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let person = Person::read_from_name(&mut context.pool(), user_name, false).await?;
@ -252,14 +247,15 @@ async fn get_feed_user(
let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?;
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.clone())
.title(&format!("{} - {}", site_view.site.name, person.name))
.link(person.actor_id.to_string())
.items(items);
let channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - {}", site_view.site.name, person.name),
link: person.actor_id.to_string(),
items,
..Default::default()
};
Ok(channel_builder)
Ok(channel)
}
#[tracing::instrument(skip_all)]
@ -269,7 +265,7 @@ async fn get_feed_community(
limit: &i64,
page: &i64,
community_name: &str,
) -> Result<ChannelBuilder, LemmyError> {
) -> Result<Channel, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let community = Community::read_from_name(&mut context.pool(), community_name, false).await?;
@ -287,18 +283,19 @@ async fn get_feed_community(
let items = create_post_items(posts, &context.settings().get_protocol_and_hostname())?;
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.clone())
.title(&format!("{} - {}", site_view.site.name, community.name))
.link(community.actor_id.to_string())
.items(items);
let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - {}", site_view.site.name, community.name),
link: community.actor_id.to_string(),
items,
..Default::default()
};
if let Some(community_desc) = community.description {
channel_builder.description(markdown_to_html(&community_desc));
channel.set_description(markdown_to_html(&community_desc));
}
Ok(channel_builder)
Ok(channel)
}
#[tracing::instrument(skip_all)]
@ -308,7 +305,7 @@ async fn get_feed_front(
limit: &i64,
page: &i64,
jwt: &str,
) -> Result<ChannelBuilder, LemmyError> {
) -> Result<Channel, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_user = local_user_view_from_jwt(jwt, context).await?;
@ -328,22 +325,23 @@ async fn get_feed_front(
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
let items = create_post_items(posts, &protocol_and_hostname)?;
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.clone())
.title(&format!("{} - Subscribed", site_view.site.name))
.link(protocol_and_hostname)
.items(items);
let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - Subscribed", site_view.site.name),
link: protocol_and_hostname,
items,
..Default::default()
};
if let Some(site_desc) = site_view.site.description {
channel_builder.description(markdown_to_html(&site_desc));
channel.set_description(markdown_to_html(&site_desc));
}
Ok(channel_builder)
Ok(channel)
}
#[tracing::instrument(skip_all)]
async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> Result<ChannelBuilder, LemmyError> {
async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> Result<Channel, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_user = local_user_view_from_jwt(jwt, context).await?;
let person_id = local_user.local_user.person_id;
@ -378,18 +376,19 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> Result<ChannelBuil
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
let items = create_reply_and_mention_items(replies, mentions, &protocol_and_hostname)?;
let mut channel_builder = ChannelBuilder::default();
channel_builder
.namespaces(RSS_NAMESPACE.clone())
.title(&format!("{} - Inbox", site_view.site.name))
.link(format!("{protocol_and_hostname}/inbox",))
.items(items);
let mut channel = Channel {
namespaces: RSS_NAMESPACE.clone(),
title: format!("{} - Inbox", site_view.site.name),
link: format!("{protocol_and_hostname}/inbox"),
items,
..Default::default()
};
if let Some(site_desc) = site_view.site.description {
channel_builder.description(&site_desc);
channel.set_description(&site_desc);
}
Ok(channel_builder)
Ok(channel)
}
#[tracing::instrument(skip_all)]
@ -438,22 +437,26 @@ fn build_item(
content: &str,
protocol_and_hostname: &str,
) -> Result<Item, LemmyError> {
let mut i = ItemBuilder::default();
i.title(format!("Reply from {creator_name}"));
let author_url = format!("{protocol_and_hostname}/u/{creator_name}");
i.author(format!(
"/u/{creator_name} <a href=\"{author_url}\">(link)</a>"
));
let dt = published;
i.pub_date(dt.to_rfc2822());
i.comments(url.to_owned());
let guid = GuidBuilder::default().permalink(true).value(url).build();
i.guid(guid);
i.link(url.to_owned());
// TODO add images
let html = markdown_to_html(content);
i.description(html);
Ok(i.build())
let author_url = format!("{protocol_and_hostname}/u/{creator_name}");
let guid = Some(Guid {
permalink: true,
value: url.to_owned(),
});
let description = Some(markdown_to_html(content));
Ok(Item {
title: Some(format!("Reply from {creator_name}")),
author: Some(format!(
"/u/{creator_name} <a href=\"{author_url}\">(link)</a>"
)),
pub_date: Some(published.to_rfc2822()),
comments: Some(url.to_owned()),
link: Some(url.to_owned()),
guid,
description,
..Default::default()
})
}
#[tracing::instrument(skip_all)]
@ -464,31 +467,21 @@ fn create_post_items(
let mut items: Vec<Item> = Vec::new();
for p in posts {
let mut i = ItemBuilder::default();
let mut dc_extension = DublinCoreExtensionBuilder::default();
i.title(sanitize_html(&p.post.name));
dc_extension.creators(vec![p.creator.actor_id.to_string()]);
let dt = p.post.published;
i.pub_date(dt.to_rfc2822());
// TODO add images
let post_url = format!("{}/post/{}", protocol_and_hostname, p.post.id);
i.comments(post_url.clone());
let guid = GuidBuilder::default()
.permalink(true)
.value(&post_url)
.build();
i.guid(guid);
let community_url = format!(
"{}/c/{}",
protocol_and_hostname,
sanitize_html(&p.community.name)
);
// TODO add images
let dublin_core_ext = Some(DublinCoreExtension {
creators: vec![p.creator.actor_id.to_string()],
..DublinCoreExtension::default()
});
let guid = Some(Guid {
permalink: true,
value: post_url.clone(),
});
let mut description = format!("submitted by <a href=\"{}\">{}</a> to <a href=\"{}\">{}</a><br>{} points | <a href=\"{}\">{} comments</a>",
p.creator.actor_id,
sanitize_html(&p.creator.name),
@ -499,23 +492,31 @@ fn create_post_items(
p.counts.comments);
// If its a url post, add it to the description
if let Some(url) = p.post.url {
let link = Some(if let Some(url) = p.post.url {
let link_html = format!("<br><a href=\"{url}\">{url}</a>");
description.push_str(&link_html);
i.link(url.to_string());
url.to_string()
} else {
i.link(post_url.clone());
}
post_url.clone()
});
if let Some(body) = p.post.body {
let html = markdown_to_html(&body);
description.push_str(&html);
}
i.description(description);
i.dublin_core_ext(dc_extension.build());
items.push(i.build());
let i = Item {
title: Some(sanitize_html(&p.post.name)),
pub_date: Some(p.post.published.to_rfc2822()),
comments: Some(post_url.clone()),
guid,
description: Some(description),
dublin_core_ext,
link,
..Default::default()
};
items.push(i);
}
Ok(items)

@ -37,12 +37,11 @@ async fn get_webfinger_response(
) -> Result<HttpResponse, LemmyError> {
let name = extract_webfinger_name(&info.resource, &context)?;
let name_ = name.clone();
let user_id: Option<Url> = Person::read_from_name(&mut context.pool(), &name_, false)
let user_id: Option<Url> = Person::read_from_name(&mut context.pool(), name, false)
.await
.ok()
.map(|c| c.actor_id.into());
let community_id: Option<Url> = Community::read_from_name(&mut context.pool(), &name, false)
let community_id: Option<Url> = Community::read_from_name(&mut context.pool(), name, false)
.await
.ok()
.map(|c| c.actor_id.into());

@ -41,12 +41,12 @@ uuid = { workspace = true, features = ["serde", "v4"] }
rosetta-i18n = { workspace = true }
tokio = { workspace = true }
urlencoding = { workspace = true }
openssl = "0.10.57"
openssl = "0.10.61"
html2text = "0.6.0"
deser-hjson = "1.2.0"
deser-hjson = "2.2.4"
smart-default = "0.7.1"
lettre = { version = "0.10.4", features = ["tokio1", "tokio1-native-tls"] }
markdown-it = "0.5.1"
lettre = { version = "0.11.2", features = ["tokio1", "tokio1-native-tls"] }
markdown-it = "0.6.0"
ts-rs = { workspace = true, optional = true }
enum-map = { workspace = true }

@ -221,14 +221,13 @@ pub enum LemmyErrorType {
CouldntSendWebmention,
ContradictingFilters,
InstanceBlockAlreadyExists,
/// `jwt` cookie must be marked secure and httponly
AuthCookieInsecure,
/// Thrown when an API call is submitted with more than 1000 array elements, see [[MAX_API_PARAM_ELEMENTS]]
TooManyItems,
CommunityHasNoFollowers,
BanExpirationInPast,
InvalidUnixTime,
InvalidBotAction,
CantBlockLocalInstance,
Unknown(String),
}

@ -36,11 +36,11 @@ impl RateLimitCell {
let state_weak_ref = Arc::downgrade(&state);
tokio::spawn(async move {
let hour = Duration::from_secs(3600);
let interval = Duration::from_secs(120);
// This loop stops when all other references to `state` are dropped
while let Some(state) = state_weak_ref.upgrade() {
tokio::time::sleep(hour).await;
tokio::time::sleep(interval).await;
state
.lock()
.expect("Failed to lock rate limit mutex for reading")

@ -4,9 +4,6 @@ use once_cell::sync::Lazy;
use regex::{Regex, RegexBuilder};
use url::Url;
static VALID_POST_TITLE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r".*\S{3,200}.*").expect("compile regex"));
// 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})?$")
@ -150,7 +147,8 @@ pub fn is_valid_matrix_id(matrix_id: &str) -> LemmyResult<()> {
}
pub fn is_valid_post_title(title: &str) -> LemmyResult<()> {
let check = VALID_POST_TITLE_REGEX.is_match(title) && !has_newline(title);
let length = title.trim().len();
let check = (3..=200).contains(&length) && !has_newline(title);
if !check {
Err(LemmyErrorType::InvalidPostTitle.into())
} else {
@ -330,9 +328,13 @@ mod tests {
fn regex_checks() {
assert!(is_valid_post_title("hi").is_err());
assert!(is_valid_post_title("him").is_ok());
assert!(is_valid_post_title(" him ").is_ok());
assert!(is_valid_post_title("n\n\n\n\nanother").is_err());
assert!(is_valid_post_title("hello there!\n this is a test.").is_err());
assert!(is_valid_post_title("hello there! this is a test.").is_ok());
assert!(is_valid_post_title(("12345".repeat(40) + "x").as_str()).is_err());
assert!(is_valid_post_title("12345".repeat(40).as_str()).is_ok());
assert!(is_valid_post_title((("12345".repeat(40)) + " ").as_str()).is_ok());
}
#[test]

@ -1 +1 @@
Subproject commit a36865ee8ca3658fea31ba948b67b75a812e84fc
Subproject commit 15815aea74fe97360afc03496b3ad62588649af0

@ -1,10 +1,17 @@
# syntax=docker/dockerfile:1.6
ARG RUST_VERSION=1.72.1
ARG CARGO_BUILD_FEATURES=default
ARG RUST_RELEASE_MODE=debug
ARG AMD_BUILDER_IMAGE=rust:${RUST_VERSION}
ARG ARM_BUILDER_IMAGE=blackdex/rust-musl:aarch64-musl-stable-${RUST_VERSION}-openssl3
ARG ARM_BUILDER_IMAGE="ghcr.io/raskyld/aarch64-lemmy-linux-gnu:v0.1.0"
ARG AMD_RUNNER_IMAGE=debian:bookworm-slim
ARG ARM_RUNNER_IMAGE=alpine:3.18
ARG ARM_RUNNER_IMAGE=debian:bookworm-slim
ARG UNAME=lemmy
ARG UID=1000
ARG GID=1000
# AMD64 builder
FROM --platform=${BUILDPLATFORM} ${AMD_BUILDER_IMAGE} AS build-amd64
@ -21,91 +28,83 @@ 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/debug/lemmy_server ./lemmy; \
mv target/"${RUST_RELEASE_MODE}"/lemmy_server ./lemmy_server; \
fi
# Release build
RUN set -ex; \
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/release/lemmy_server ./lemmy; \
mv target/"${RUST_RELEASE_MODE}"/lemmy_server ./lemmy_server; \
fi
# ARM64 builder
# TODO currently broken
# FROM --platform=${BUILDPLATFORM} ${ARM_BUILDER_IMAGE} as build-arm64
# ENV DEBIAN_FRONTEND=noninteractive
# ENV CARGO_HOME=/root/.cargo
# ENV PQ_LIB_DIR=/usr/local/musl/pq15/lib
# NB(raskyld): this is a hack to be able to COPY --from= this image, because the variable doesn't
# seem to be expended in --form arg of COPY :(
FROM --platform=linux/amd64 ${ARM_BUILDER_IMAGE} AS build-arm64
# RUN apt update && apt install -y \
# --no-install-recommends \
# git
# RUN mkdir -pv "${CARGO_HOME}" && \
# rustup set profile minimal && \
# rustup target add aarch64-unknown-linux-musl
ARG RUST_RELEASE_MODE
ARG CARGO_BUILD_FEATURES
# ARG CARGO_BUILD_FEATURES
# ARG RUST_RELEASE_MODE
WORKDIR /home/lemmy/src
USER 10001:10001
# WORKDIR /lemmy
COPY --chown=lemmy:lemmy . ./
# COPY . ./
ENV PATH="/home/lemmy/.cargo/bin:${PATH}"
ENV RUST_RELEASE_MODE=${RUST_RELEASE_MODE} \
CARGO_BUILD_FEATURES=${CARGO_BUILD_FEATURES}
# # 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 --target=aarch64-unknown-linux-musl --features "${CARGO_BUILD_FEATURES}"; \
# mv target/aarch64-unknown-linux-musl/debug/lemmy_server ./lemmy; \
# fi
# 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
# # Release build
# RUN set -ex; \
# if [ "${RUST_RELEASE_MODE}" = "release" ]; then \
# echo "pub const VERSION: &str = \"$(git describe --tag)\";" > crates/utils/src/version.rs; \
# cargo build --target=aarch64-unknown-linux-musl --features "${CARGO_BUILD_FEATURES}" --release; \
# mv target/aarch64-unknown-linux-musl/release/lemmy_server ./lemmy; \
# fi
# 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; \
fi
## Final image
FROM ${AMD_RUNNER_IMAGE}
# amd64 base runner
FROM ${AMD_RUNNER_IMAGE} AS runner-linux-amd64
# Federation needs CA certificates
RUN apt update && apt install -y libssl-dev libpq-dev ca-certificates
# Debian / Ubuntu non-root user creds
ARG UNAME=lemmy
ARG UID=1000
ARG GID=1000
RUN groupadd -g $GID -o $UNAME
RUN useradd -m -u $UID -g $GID -o -s /bin/bash $UNAME
USER $UNAME
COPY --from=build-amd64 --chmod=0755 /lemmy/lemmy_server /usr/local/bin
COPY --from=build-amd64 /lemmy/lemmy ./
CMD ["./lemmy"]
EXPOSE 8536
STOPSIGNAL SIGTERM
# arm base runner
FROM ${ARM_RUNNER_IMAGE} AS runner-linux-arm64
## Arm Runner
# FROM --platform=${BUILDPLATFORM} ${ARM_RUNNER_IMAGE}
RUN apt update && apt install -y ca-certificates libssl-dev libpq-dev
# ARG UNAME=lemmy
# ARG UID=1000
# ARG GID=1000
COPY --from=build-arm64 --chmod=0755 /home/lemmy/lemmy_server /usr/local/bin
# RUN apk add --no-cache ca-certificates
# Final image that use a base runner based on the target OS and ARCH
FROM runner-${TARGETOS}-${TARGETARCH}
# COPY --from=build-arm64 --chmod=0755 /lemmy/lemmy /usr/local/bin
LABEL org.opencontainers.image.authors="The Lemmy Authors"
LABEL org.opencontainers.image.source="https://github.com/LemmyNet/lemmy"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
LABEL org.opencontainers.image.description="A link aggregator and forum for the fediverse"
# RUN addgroup -S -g ${GID} ${UNAME} && \
# adduser -S -H -D -G ${UNAME} -u ${UID} -g "" -s /sbin/nologin ${UNAME}
# USER $UNAME
ARG UNAME
ARG GID
ARG UID
# CMD ["lemmy"]
# EXPOSE 8536
# STOPSIGNAL SIGTERM
RUN groupadd -g ${GID} -o ${UNAME} && \
useradd -m -u ${UID} -g ${GID} -o -s /bin/bash ${UNAME}
USER $UNAME
ENTRYPOINT ["lemmy_server"]
EXPOSE 8536
STOPSIGNAL SIGTERM

@ -0,0 +1,21 @@
# Building Lemmy Images
Lemmy's images are meant to be **built** on `linux/amd64`,
but they can be **executed** on both `linux/amd64` and `linux/arm64`.
To do so we need to use a _cross toolchain_ whose goal is to build
**from** amd64 **to** arm64.
Namely, we need to link the _lemmy_server_ with `pq` and `openssl`
shared libraries and a few others, and they need to be in `arm64`,
indeed.
The toolchain we use to cross-compile is specifically tailored for
Lemmy's needs, see [the image repository][image-repo].
#### References
- [The Linux Documentation Project on Shared Libraries][tldp-lib]
[tldp-lib]: https://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html
[image-repo]: https://github.com/raskyld/lemmy-cross-toolchains

@ -25,7 +25,7 @@ services:
lemmy:
# use "image" to pull down an already compiled lemmy. make sure to comment out "build".
# image: dessalines/lemmy:0.18.4
# image: dessalines/lemmy:0.19.0
# platform: linux/x86_64 # no arm64 support. uncomment platform if using m1.
# use "build" to build your local lemmy server image for development. make sure to comment out "image".
# run: docker compose up --build
@ -43,8 +43,8 @@ services:
- RUST_LOG="warn,lemmy_server=debug,lemmy_api=debug,lemmy_api_common=debug,lemmy_api_crud=debug,lemmy_apub=debug,lemmy_db_schema=debug,lemmy_db_views=debug,lemmy_db_views_actor=debug,lemmy_db_views_moderator=debug,lemmy_routes=debug,lemmy_utils=debug,lemmy_websocket=debug"
- RUST_BACKTRACE=full
ports:
# prometheus metrics available at the path /metrics on port 10002 by default
# enable prometheus metrics by setting the CARGO_BUILD_FEATURES build arg above to "prometheus-metrics"
# prometheus metrics can be enabled with the `prometheus` config option. they are available on
# port 10002, path /metrics by default
- "10002:10002"
volumes:
- ./lemmy.hjson:/config/config.hjson:Z
@ -55,7 +55,7 @@ services:
lemmy-ui:
# use "image" to pull down an already compiled lemmy-ui. make sure to comment out "build".
image: dessalines/lemmy-ui:0.19.0-rc.3
image: dessalines/lemmy-ui:0.19.0
# platform: linux/x86_64 # no arm64 support. uncomment platform if using m1.
# use "build" to build your local lemmy ui image for development. make sure to comment out "image".
# run: docker compose up --build
@ -77,7 +77,7 @@ services:
init: true
pictrs:
image: asonix/pictrs:0.5.0-alpha.20
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

@ -2,7 +2,7 @@ version: "3.7"
x-ui-default: &ui-default
init: true
image: dessalines/lemmy-ui:0.19.0-rc.3
image: dessalines/lemmy-ui:0.19.0
# assuming lemmy-ui is cloned besides lemmy directory
# build:
# context: ../../../lemmy-ui
@ -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,11 @@
ALTER TABLE community_aggregates
ALTER COLUMN hot_rank SET DEFAULT 0.1728;
ALTER TABLE comment_aggregates
ALTER COLUMN hot_rank SET DEFAULT 0.1728;
ALTER TABLE post_aggregates
ALTER COLUMN hot_rank SET DEFAULT 0.1728,
ALTER COLUMN hot_rank_active SET DEFAULT 0.1728,
ALTER COLUMN scaled_rank SET DEFAULT 0.3621;

@ -0,0 +1,16 @@
-- Change the hot_ranks to a miniscule number, so that new / fetched content
-- won't crowd out existing content.
--
-- They must be non-zero, in order for them to be picked up by the hot_ranks updater.
-- See https://github.com/LemmyNet/lemmy/issues/4178
ALTER TABLE community_aggregates
ALTER COLUMN hot_rank SET DEFAULT 0.0001;
ALTER TABLE comment_aggregates
ALTER COLUMN hot_rank SET DEFAULT 0.0001;
ALTER TABLE post_aggregates
ALTER COLUMN hot_rank SET DEFAULT 0.0001,
ALTER COLUMN hot_rank_active SET DEFAULT 0.0001,
ALTER COLUMN scaled_rank SET DEFAULT 0.0001;

@ -0,0 +1,72 @@
CREATE OR REPLACE FUNCTION community_aggregates_activity (i text)
RETURNS TABLE (
count_ bigint,
community_id_ integer)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN query
SELECT
count(*),
community_id
FROM (
SELECT
c.creator_id,
p.community_id
FROM
comment c
INNER JOIN post p ON c.post_id = p.id
INNER JOIN person pe ON c.creator_id = pe.id
WHERE
c.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE
UNION
SELECT
p.creator_id,
p.community_id
FROM
post p
INNER JOIN person pe ON p.creator_id = pe.id
WHERE
p.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE) a
GROUP BY
community_id;
END;
$$;
CREATE OR REPLACE FUNCTION site_aggregates_activity (i text)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
count_ integer;
BEGIN
SELECT
count(*) INTO count_
FROM (
SELECT
c.creator_id
FROM
comment c
INNER JOIN person u ON c.creator_id = u.id
INNER JOIN person pe ON c.creator_id = pe.id
WHERE
c.published > ('now'::timestamp - i::interval)
AND u.local = TRUE
AND pe.bot_account = FALSE
UNION
SELECT
p.creator_id
FROM
post p
INNER JOIN person u ON p.creator_id = u.id
INNER JOIN person pe ON p.creator_id = pe.id
WHERE
p.published > ('now'::timestamp - i::interval)
AND u.local = TRUE
AND pe.bot_account = FALSE) a;
RETURN count_;
END;
$$;

@ -0,0 +1,114 @@
-- Edit community aggregates to include voters as active users
CREATE OR REPLACE FUNCTION community_aggregates_activity (i text)
RETURNS TABLE (
count_ bigint,
community_id_ integer)
LANGUAGE plpgsql
AS $$
BEGIN
RETURN query
SELECT
count(*),
community_id
FROM (
SELECT
c.creator_id,
p.community_id
FROM
comment c
INNER JOIN post p ON c.post_id = p.id
INNER JOIN person pe ON c.creator_id = pe.id
WHERE
c.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE
UNION
SELECT
p.creator_id,
p.community_id
FROM
post p
INNER JOIN person pe ON p.creator_id = pe.id
WHERE
p.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE
UNION
SELECT
pl.person_id,
p.community_id
FROM
post_like pl
INNER JOIN post p ON pl.post_id = p.id
INNER JOIN person pe ON pl.person_id = pe.id
WHERE
pl.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE
UNION
SELECT
cl.person_id,
p.community_id
FROM
comment_like cl
INNER JOIN post p ON cl.post_id = p.id
INNER JOIN person pe ON cl.person_id = pe.id
WHERE
cl.published > ('now'::timestamp - i::interval)
AND pe.bot_account = FALSE) a
GROUP BY
community_id;
END;
$$;
-- Edit site aggregates to include voters and people who have read posts as active users
CREATE OR REPLACE FUNCTION site_aggregates_activity (i text)
RETURNS integer
LANGUAGE plpgsql
AS $$
DECLARE
count_ integer;
BEGIN
SELECT
count(*) INTO count_
FROM (
SELECT
c.creator_id
FROM
comment c
INNER JOIN person pe ON c.creator_id = pe.id
WHERE
c.published > ('now'::timestamp - i::interval)
AND pe.local = TRUE
AND pe.bot_account = FALSE
UNION
SELECT
p.creator_id
FROM
post p
INNER JOIN person pe ON p.creator_id = pe.id
WHERE
p.published > ('now'::timestamp - i::interval)
AND pe.local = TRUE
AND pe.bot_account = FALSE
UNION
SELECT
pl.person_id
FROM
post_like pl
INNER JOIN person pe ON pl.person_id = pe.id
WHERE
pl.published > ('now'::timestamp - i::interval)
AND pe.local = TRUE
AND pe.bot_account = FALSE
UNION
SELECT
cl.person_id
FROM
comment_like cl
INNER JOIN person pe ON cl.person_id = pe.id
WHERE
cl.published > ('now'::timestamp - i::interval)
AND pe.local = TRUE
AND pe.bot_account = FALSE) a;
RETURN count_;
END;
$$;

@ -7,9 +7,12 @@ CWD="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
cd $CWD/../
find migrations -type f -name "*.sql" -print0 | while read -d $'\0' FILE
do
TMP_FILE="/tmp/tmp_pg_format.sql"
pg_format $FILE > $TMP_FILE
diff -u $FILE $TMP_FILE
done
# Copy the files to a temp dir
TMP_DIR=$(mktemp -d)
cp -a migrations/. $TMP_DIR
# Format the new files
find $TMP_DIR -type f -name '*.sql' -exec pg_format -i {} +
# Diff the directories
diff -r migrations $TMP_DIR

@ -16,14 +16,15 @@ use activitypub_federation::config::{FederationConfig, FederationMiddleware};
use actix_cors::Cors;
use actix_web::{
dev::{ServerHandle, ServiceResponse},
middleware::{self, ErrorHandlerResponse, ErrorHandlers},
middleware::{self, Condition, ErrorHandlerResponse, ErrorHandlers},
web::Data,
App,
HttpResponse,
HttpServer,
Result,
};
use clap::{ArgAction, Parser};
use actix_web_prom::PrometheusMetricsBuilder;
use clap::Parser;
use lemmy_api_common::{
context::LemmyContext,
lemmy_db_views::structs::SiteView,
@ -48,7 +49,9 @@ use lemmy_utils::{
rate_limit::RateLimitCell,
response::jsonify_plain_text_errors,
settings::{structs::Settings, SETTINGS},
version,
};
use prometheus::default_registry;
use prometheus_metrics::serve_prometheus;
use reqwest_middleware::ClientBuilder;
use reqwest_tracing::TracingMiddleware;
@ -69,24 +72,23 @@ use url::Url;
long_about = "A link aggregator for the fediverse.\n\nThis is the Lemmy backend API server. This will connect to a PostgreSQL database, run any pending migrations and start accepting API requests."
)]
pub struct CmdArgs {
#[arg(long, default_value_t = false)]
/// Disables running scheduled tasks.
/// Don't run scheduled tasks.
///
/// If you are running multiple Lemmy server processes,
/// you probably want to disable scheduled tasks on all but one of the processes,
/// to avoid running the tasks more often than intended.
/// If you are running multiple Lemmy server processes, you probably want to disable scheduled tasks on
/// all but one of the processes, to avoid running the tasks more often than intended.
#[arg(long, default_value_t = false)]
disable_scheduled_tasks: bool,
/// Whether or not to run the HTTP server.
/// Disables the HTTP server.
///
/// This can be used to run a Lemmy server process that only runs scheduled tasks.
#[arg(long, default_value_t = true, action=ArgAction::Set)]
http_server: bool,
/// Whether or not to emit outgoing ActivityPub messages.
/// This can be used to run a Lemmy server process that only performs scheduled tasks or activity sending.
#[arg(long, default_value_t = false)]
disable_http_server: bool,
/// Disable sending outgoing ActivityPub messages.
///
/// Set to true for a simple setup. Only set to false for horizontally scaled setups.
/// See https://join-lemmy.org/docs/administration/horizontal_scaling.html for detail.
#[arg(long, default_value_t = true, action=ArgAction::Set)]
federate_activities: bool,
/// Only pass this for horizontally scaled setups.
/// See https://join-lemmy.org/docs/administration/horizontal_scaling.html for details.
#[arg(long, default_value_t = false)]
disable_activity_sending: bool,
/// The index of this outgoing federation process.
///
/// Defaults to 1/1. If you want to split the federation workload onto n servers, run each server 1≤i≤n with these args:
@ -106,9 +108,12 @@ 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);
// return error 503 while running db migrations and startup tasks
let mut startup_server_handle = None;
if args.http_server {
if !args.disable_http_server {
startup_server_handle = Some(create_startup_server()?);
}
@ -131,7 +136,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
let federation_enabled = local_site.federation_enabled;
if federation_enabled {
println!("federation enabled, host is {}", &SETTINGS.hostname);
println!("Federation enabled, host is {}", &SETTINGS.hostname);
}
check_private_instance_and_federation_enabled(&local_site)?;
@ -142,7 +147,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
let rate_limit_cell = RateLimitCell::new(rate_limit_config);
println!(
"Starting http server at {}:{}",
"Starting HTTP server at {}:{}",
SETTINGS.bind, SETTINGS.port
);
@ -188,7 +193,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
let request_data = federation_config.to_request_data();
let outgoing_activities_task = tokio::task::spawn(handle_outgoing_activities(request_data));
let server = if args.http_server {
let server = if !args.disable_http_server {
if let Some(startup_server_handle) = startup_server_handle {
startup_server_handle.stop(true).await;
}
@ -201,7 +206,7 @@ pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
} else {
None
};
let federate = args.federate_activities.then(|| {
let federate = (!args.disable_activity_sending).then(|| {
start_stop_federation_workers_cancellable(
Opts {
process_index: args.federate_process_index,
@ -265,7 +270,6 @@ fn create_http_server(
) -> Result<ServerHandle, LemmyError> {
// this must come before the HttpServer creation
// creates a middleware that populates http metrics for each path, method, and status code
#[cfg(feature = "prometheus-metrics")]
let prom_api_metrics = PrometheusMetricsBuilder::new("lemmy_api")
.registry(default_registry().clone())
.build()
@ -295,7 +299,11 @@ fn create_http_server(
.app_data(Data::new(context.clone()))
.app_data(Data::new(rate_limit_cell.clone()))
.wrap(FederationMiddleware::new(federation_config.clone()))
.wrap(SessionMiddleware::new(context.clone()));
.wrap(SessionMiddleware::new(context.clone()))
.wrap(Condition::new(
SETTINGS.prometheus.is_some(),
prom_api_metrics.clone(),
));
// The routes
app
@ -325,7 +333,7 @@ fn cors_config(settings: &Settings) -> Cors {
(Some(origin), false) => {
// Need to call send_wildcard() explicitly, passing this into allowed_origin() results in error
if cors_origin_setting.as_deref() == Some("*") {
Cors::default().send_wildcard()
Cors::default().allow_any_origin().send_wildcard()
} else {
Cors::default()
.allowed_origin(&origin)

@ -18,13 +18,10 @@ pub async fn main() -> Result<(), LemmyError> {
.port()
.unwrap_or(8080);
let pictrs_address = ["127.0.0.1", &pictrs_port.to_string()].join(":");
pict_rs::ConfigSource::memory(serde_json::json!({
let pictrs_config = pict_rs::ConfigSource::memory(serde_json::json!({
"server": {
"address": pictrs_address
},
"old_db": {
"path": "./pictrs/old"
},
"repo": {
"type": "sled",
"path": "./pictrs/sled-repo"
@ -36,7 +33,7 @@ pub async fn main() -> Result<(), LemmyError> {
}))
.init::<&str>(None)
.expect("initialize pictrs config");
let (lemmy, pictrs) = tokio::join!(start_lemmy_server(args), pict_rs::run());
let (lemmy, pictrs) = tokio::join!(start_lemmy_server(args), pictrs_config.run_on_localset());
lemmy?;
pictrs.expect("run pictrs");
}

Loading…
Cancel
Save