Merge branch 'main' into markdown-link-rule

cleanup-request-rs
Felix Ableitner 2023-10-25 12:57:24 +02:00
commit f057abff71
34 changed files with 1854 additions and 1312 deletions

2318
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -70,15 +70,15 @@ lemmy_routes = { version = "=0.19.0-rc.3", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.0-rc.3", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.0-rc.3", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.0-rc.3", path = "./crates/db_views_moderator" }
activitypub_federation = { version = "0.5.0-beta.3", default-features = false, features = [
activitypub_federation = { git = "https://github.com/LemmyNet/activitypub-federation-rust.git", branch = "webfinger-alphabets", default-features = false, features = [
"actix-web",
] }
diesel = "2.1.0"
diesel = "2.1.3"
diesel_migrations = "2.1.0"
diesel-async = "0.3.1"
serde = { version = "1.0.167", features = ["derive"] }
serde_with = "3.0.0"
actix-web = { version = "4.3.1", default-features = false, features = [
diesel-async = "0.3.2"
serde = { version = "1.0.189", features = ["derive"] }
serde_with = "3.4.0"
actix-web = { version = "4.4.0", default-features = false, features = [
"macros",
"rustls",
"compress-brotli",
@ -86,37 +86,37 @@ actix-web = { version = "4.3.1", default-features = false, features = [
"compress-zstd",
"cookies",
] }
tracing = "0.1.37"
tracing-actix-web = { version = "0.7.5", default-features = false }
tracing = "0.1.40"
tracing-actix-web = { version = "0.7.8", default-features = false }
tracing-error = "0.2.0"
tracing-log = "0.1.3"
tracing-log = "0.1.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = { version = "2.4.0", features = ["serde"] }
reqwest = { version = "0.11.18", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.2"
reqwest-tracing = "0.4.5"
url = { version = "2.4.1", features = ["serde"] }
reqwest = { version = "0.11.22", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.4"
reqwest-tracing = "0.4.6"
clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.0"
chrono = { version = "0.4.26", features = ["serde"], default-features = false }
serde_json = { version = "1.0.100", features = ["preserve_order"] }
base64 = "0.21.2"
uuid = { version = "1.4.0", features = ["serde", "v4"] }
async-trait = "0.1.71"
chrono = { version = "0.4.31", features = ["serde"], default-features = false }
serde_json = { version = "1.0.107", features = ["preserve_order"] }
base64 = "0.21.5"
uuid = { version = "1.5.0", features = ["serde", "v4"] }
async-trait = "0.1.74"
captcha = "0.0.9"
anyhow = { version = "1.0.71", features = [
anyhow = { version = "1.0.75", features = [
"backtrace",
] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.0"
typed-builder = "0.15.0"
typed-builder = "0.15.2"
serial_test = "2.0.0"
tokio = { version = "1.29.1", features = ["full"] }
regex = "1.9.0"
tokio = { version = "1.33.0", features = ["full"] }
regex = "1.10.2"
once_cell = "1.18.0"
diesel-derive-newtype = "2.1.0"
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = "0.25.0"
strum_macros = "0.25.1"
strum_macros = "0.25.3"
itertools = "0.11.0"
futures = "0.3.28"
http = "0.2.9"
@ -125,12 +125,12 @@ 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.3", features = ["dangerous_configuration"] }
rustls = { version = "0.21.8", features = ["dangerous_configuration"] }
futures-util = "0.3.28"
tokio-postgres = "0.7.8"
tokio-postgres = "0.7.10"
tokio-postgres-rustls = "0.10.0"
urlencoding = "2.1.3"
enum-map = "2.6"
enum-map = "2.7"
[dependencies]
lemmy_api = { workspace = true }
@ -162,7 +162,7 @@ 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.0-rc.12", optional = true }
pict-rs = { version = "0.4.5", optional = true }
tokio.workspace = true
actix-cors = "0.6.4"
rustls = { workspace = true }
@ -173,5 +173,5 @@ chrono = { workspace = true }
prometheus = { version = "0.13.3", features = ["process"], optional = true }
actix-web-prom = { version = "0.6.0", optional = true }
serial_test = { workspace = true }
clap = { version = "4.3.19", features = ["derive"] }
clap = { version = "4.4.7", features = ["derive"] }
actix-web-httpauth = "0.8.1"

View File

@ -12,11 +12,11 @@
"api-test": "jest -i follow.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts && jest -i private_message.spec.ts && jest -i user.spec.ts && jest -i community.spec.ts"
},
"devDependencies": {
"@types/jest": "^29.5.1",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"eslint": "^8.51.0",
"@types/jest": "^29.5.6",
"@types/node": "^20.8.7",
"@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.8.0",
"eslint": "^8.52.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.5.0",
"lemmy-js-client": "0.19.0-rc.12",

View File

@ -25,7 +25,6 @@ import {
getCommunityByName,
blockInstance,
waitUntil,
delay,
alphaUrl,
delta,
betaAllowedInstances,

View File

@ -17,8 +17,9 @@ import {
saveUserSettingsFederated,
setupLogins,
alphaUrl,
saveUserSettings,
} from "./shared";
import { LemmyHttp } from "lemmy-js-client";
import { LemmyHttp, SaveUserSettings } from "lemmy-js-client";
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
beforeAll(async () => {
@ -57,6 +58,15 @@ test("Set some user settings, check that they are federated", async () => {
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
let betaPerson = (await resolvePerson(beta, apShortname)).person;
assertUserFederation(alphaPerson, betaPerson);
// Catches a bug where when only the person or local_user changed
let form: SaveUserSettings = {
theme: "test",
};
await saveUserSettings(beta, form);
let site = await getSite(beta);
expect(site.my_user?.local_user_view.local_user.theme).toBe("test");
});
test("Delete user", async () => {
@ -119,3 +129,21 @@ test("Requests with invalid auth should be treated as unauthenticated", async ()
let posts = invalid_auth.getPosts(form);
expect((await posts).posts).toBeDefined();
});
test("Create user with Arabic name", async () => {
let userRes = await registerUser(alpha, "تجريب");
expect(userRes.jwt).toBeDefined();
let user = new LemmyHttp(alphaUrl, {
headers: { Authorization: `Bearer ${userRes.jwt ?? ""}` },
});
let site = await getSite(user);
expect(site.my_user).toBeDefined();
if (!site.my_user) {
throw "Missing site user";
}
apShortname = `@${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
expect(alphaPerson).toBeDefined();
});

View File

@ -329,17 +329,17 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.51.0":
version "8.51.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.51.0.tgz#6d419c240cfb2b66da37df230f7e7eef801c32fa"
integrity sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==
"@eslint/js@8.52.0":
version "8.52.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.52.0.tgz#78fe5f117840f69dc4a353adf9b9cd926353378c"
integrity sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==
"@humanwhocodes/config-array@^0.11.11":
version "0.11.11"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.11.tgz#88a04c570dbbc7dd943e4712429c3df09bc32844"
integrity sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==
"@humanwhocodes/config-array@^0.11.13":
version "0.11.13"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297"
integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==
dependencies:
"@humanwhocodes/object-schema" "^1.2.1"
"@humanwhocodes/object-schema" "^2.0.1"
debug "^4.1.1"
minimatch "^3.0.5"
@ -348,10 +348,10 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/object-schema@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@humanwhocodes/object-schema@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044"
integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0"
@ -704,10 +704,10 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@^29.5.1":
version "29.5.5"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.5.tgz#727204e06228fe24373df9bae76b90f3e8236a2a"
integrity sha512-ebylz2hnsWR9mYvmBFbXJXr+33UPc4+ZdxyDXh5w0FlPBTfCVN3wPL+kuOiQt3xvrK419v7XWeAs+AeOksafXg==
"@types/jest@^29.5.6":
version "29.5.6"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.6.tgz#f4cf7ef1b5b0bfc1aa744e41b24d9cc52533130b"
integrity sha512-/t9NnzkOpXb4Nfvg17ieHE6EeSjDS2SGSpNYfoLbUAeL/EOueU/RSdOWFpfQTXBEM7BguYW1XQ0EbM+6RlIh6w==
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.8.6":
version "20.8.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.6.tgz#0dbd4ebcc82ad0128df05d0e6f57e05359ee47fa"
integrity sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==
"@types/node@^20.8.7":
version "20.8.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.7.tgz#ad23827850843de973096edfc5abc9e922492a25"
integrity sha512-21TKHHh3eUHIi2MloeptJWALuCu5H7HQTdTrWIFReA8ad+aggoX+lRes3ex7/FtpC+sVUpFMQ+QTfYr74mruiQ==
dependencies:
undici-types "~5.25.1"
@ -751,16 +751,16 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^6.7.5":
version "6.7.5"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.5.tgz#f4024b9f63593d0c2b5bd6e4ca027e6f30934d4f"
integrity sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw==
"@typescript-eslint/eslint-plugin@^6.8.0":
version "6.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.8.0.tgz#06abe4265e7c82f20ade2dcc0e3403c32d4f148b"
integrity sha512-GosF4238Tkes2SHPQ1i8f6rMtG6zlKwMEB0abqSJ3Npvos+doIlc/ATG+vX1G9coDF3Ex78zM3heXHLyWEwLUw==
dependencies:
"@eslint-community/regexpp" "^4.5.1"
"@typescript-eslint/scope-manager" "6.7.5"
"@typescript-eslint/type-utils" "6.7.5"
"@typescript-eslint/utils" "6.7.5"
"@typescript-eslint/visitor-keys" "6.7.5"
"@typescript-eslint/scope-manager" "6.8.0"
"@typescript-eslint/type-utils" "6.8.0"
"@typescript-eslint/utils" "6.8.0"
"@typescript-eslint/visitor-keys" "6.8.0"
debug "^4.3.4"
graphemer "^1.4.0"
ignore "^5.2.4"
@ -768,74 +768,79 @@
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/parser@^6.7.5":
version "6.7.5"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.5.tgz#8d7ca3d1fbd9d5a58cc4d30b2aa797a760137886"
integrity sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==
"@typescript-eslint/parser@^6.8.0":
version "6.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.8.0.tgz#bb2a969d583db242f1ee64467542f8b05c2e28cb"
integrity sha512-5tNs6Bw0j6BdWuP8Fx+VH4G9fEPDxnVI7yH1IAPkQH5RUtvKwRoqdecAPdQXv4rSOADAaz1LFBZvZG7VbXivSg==
dependencies:
"@typescript-eslint/scope-manager" "6.7.5"
"@typescript-eslint/types" "6.7.5"
"@typescript-eslint/typescript-estree" "6.7.5"
"@typescript-eslint/visitor-keys" "6.7.5"
"@typescript-eslint/scope-manager" "6.8.0"
"@typescript-eslint/types" "6.8.0"
"@typescript-eslint/typescript-estree" "6.8.0"
"@typescript-eslint/visitor-keys" "6.8.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@6.7.5":
version "6.7.5"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.5.tgz#1cf33b991043886cd67f4f3600b8e122fc14e711"
integrity sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==
"@typescript-eslint/scope-manager@6.8.0":
version "6.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.8.0.tgz#5cac7977385cde068ab30686889dd59879811efd"
integrity sha512-xe0HNBVwCph7rak+ZHcFD6A+q50SMsFwcmfdjs9Kz4qDh5hWhaPhFjRs/SODEhroBI5Ruyvyz9LfwUJ624O40g==
dependencies:
"@typescript-eslint/types" "6.7.5"
"@typescript-eslint/visitor-keys" "6.7.5"
"@typescript-eslint/types" "6.8.0"
"@typescript-eslint/visitor-keys" "6.8.0"
"@typescript-eslint/type-utils@6.7.5":
version "6.7.5"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.7.5.tgz#0a65949ec16588d8956f6d967f7d9c84ddb2d72a"
integrity sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g==
"@typescript-eslint/type-utils@6.8.0":
version "6.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.8.0.tgz#50365e44918ca0fd159844b5d6ea96789731e11f"
integrity sha512-RYOJdlkTJIXW7GSldUIHqc/Hkto8E+fZN96dMIFhuTJcQwdRoGN2rEWA8U6oXbLo0qufH7NPElUb+MceHtz54g==
dependencies:
"@typescript-eslint/typescript-estree" "6.7.5"
"@typescript-eslint/utils" "6.7.5"
"@typescript-eslint/typescript-estree" "6.8.0"
"@typescript-eslint/utils" "6.8.0"
debug "^4.3.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/types@6.7.5":
version "6.7.5"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.5.tgz#4571320fb9cf669de9a95d9849f922c3af809790"
integrity sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==
"@typescript-eslint/types@6.8.0":
version "6.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.8.0.tgz#1ab5d4fe1d613e3f65f6684026ade6b94f7e3ded"
integrity sha512-p5qOxSum7W3k+llc7owEStXlGmSl8FcGvhYt8Vjy7FqEnmkCVlM3P57XQEGj58oqaBWDQXbJDZxwUWMS/EAPNQ==
"@typescript-eslint/typescript-estree@6.7.5":
version "6.7.5"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.5.tgz#4578de1a26e9f24950f029a4f00d1bfe41f15a39"
integrity sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==
"@typescript-eslint/typescript-estree@6.8.0":
version "6.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.8.0.tgz#9565f15e0cd12f55cf5aa0dfb130a6cb0d436ba1"
integrity sha512-ISgV0lQ8XgW+mvv5My/+iTUdRmGspducmQcDw5JxznasXNnZn3SKNrTRuMsEXv+V/O+Lw9AGcQCfVaOPCAk/Zg==
dependencies:
"@typescript-eslint/types" "6.7.5"
"@typescript-eslint/visitor-keys" "6.7.5"
"@typescript-eslint/types" "6.8.0"
"@typescript-eslint/visitor-keys" "6.8.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.7.5":
version "6.7.5"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.5.tgz#ab847b53d6b65e029314b8247c2336843dba81ab"
integrity sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==
"@typescript-eslint/utils@6.8.0":
version "6.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.8.0.tgz#d42939c2074c6b59844d0982ce26a51d136c4029"
integrity sha512-dKs1itdE2qFG4jr0dlYLQVppqTE+Itt7GmIf/vX6CSvsW+3ov8PbWauVKyyfNngokhIO9sKZeRGCUo1+N7U98Q==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@types/json-schema" "^7.0.12"
"@types/semver" "^7.5.0"
"@typescript-eslint/scope-manager" "6.7.5"
"@typescript-eslint/types" "6.7.5"
"@typescript-eslint/typescript-estree" "6.7.5"
"@typescript-eslint/scope-manager" "6.8.0"
"@typescript-eslint/types" "6.8.0"
"@typescript-eslint/typescript-estree" "6.8.0"
semver "^7.5.4"
"@typescript-eslint/visitor-keys@6.7.5":
version "6.7.5"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.5.tgz#84c68d6ceb5b12d5246b918b84f2b79affd6c2f1"
integrity sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==
"@typescript-eslint/visitor-keys@6.8.0":
version "6.8.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.8.0.tgz#cffebed56ae99c45eba901c378a6447b06be58b8"
integrity sha512-oqAnbA7c+pgOhW2OhGvxm0t1BULX5peQI/rLsNDpGM78EebV3C9IGbX5HNZabuZ6UQrYveCLjKo8Iy/lLlBkkg==
dependencies:
"@typescript-eslint/types" "6.7.5"
"@typescript-eslint/types" "6.8.0"
eslint-visitor-keys "^3.4.1"
"@ungap/structured-clone@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
@ -1328,18 +1333,19 @@ 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.51.0:
version "8.51.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.51.0.tgz#4a82dae60d209ac89a5cff1604fea978ba4950f3"
integrity sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==
eslint@^8.52.0:
version "8.52.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.52.0.tgz#d0cd4a1fac06427a61ef9242b9353f36ea7062fc"
integrity sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.2"
"@eslint/js" "8.51.0"
"@humanwhocodes/config-array" "^0.11.11"
"@eslint/js" "8.52.0"
"@humanwhocodes/config-array" "^0.11.13"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
"@ungap/structured-clone" "^1.2.0"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"

View File

@ -34,7 +34,7 @@ chrono = { workspace = true }
url = { workspace = true }
wav = "1.0.0"
sitemap-rs = "0.2.0"
totp-rs = { version = "5.0.2", features = ["gen_secret", "otpauth"] }
totp-rs = { version = "5.4.0", features = ["gen_secret", "otpauth"] }
actix-web-httpauth = "0.8.1"
[dev-dependencies]

View File

@ -21,7 +21,7 @@ use lemmy_db_schema::{
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
error::{LemmyError, LemmyErrorType},
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id},
};
@ -98,9 +98,11 @@ pub async fn save_user_settings(
..Default::default()
};
// Ignore errors, because 'no fields updated' will return an error.
// https://github.com/LemmyNet/lemmy/issues/4076
Person::update(&mut context.pool(), person_id, &person_form)
.await
.with_lemmy_type(LemmyErrorType::UserAlreadyExists)?;
.ok();
if let Some(discussion_languages) = data.discussion_languages.clone() {
LocalUserLanguage::update(&mut context.pool(), discussion_languages, local_user_id).await?;
@ -128,7 +130,11 @@ pub async fn save_user_settings(
..Default::default()
};
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?;
// Ignore errors, because 'no fields updated' will return an error.
// https://github.com/LemmyNet/lemmy/issues/4076
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form)
.await
.ok();
Ok(Json(SuccessResponse::default()))
}

View File

@ -35,6 +35,9 @@ pub async fn leave_admin(
local_user_view.local_user.id,
&LocalUserUpdateForm {
admin: Some(false),
// Necessary because admins can bypass the registration applications (if they're turned on)
// but then won't be able to log in because they haven't been approved.
accepted_application: Some(true),
..Default::default()
},
)

View File

@ -47,6 +47,7 @@ pub struct GetPost {
pub comment_id: Option<CommentId>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]

View File

@ -8,6 +8,7 @@ use lemmy_utils::{
REQWEST_TIMEOUT,
};
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
use reqwest::{Client, ClientBuilder};
use reqwest_middleware::ClientWithMiddleware;
use serde::Deserialize;
use tracing::info;
@ -288,12 +289,17 @@ async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> Resu
}
}
pub fn build_user_agent(settings: &Settings) -> String {
format!(
pub fn client_builder(settings: &Settings) -> ClientBuilder {
let user_agent = format!(
"Lemmy/{}; +{}",
VERSION,
settings.get_protocol_and_hostname()
)
);
Client::builder()
.user_agent(user_agent.clone())
.timeout(REQWEST_TIMEOUT)
.connect_timeout(REQWEST_TIMEOUT)
}
#[cfg(test)]
@ -301,12 +307,7 @@ mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::request::{
build_user_agent,
fetch_site_metadata,
html_to_site_metadata,
SiteMetadata,
};
use crate::request::{client_builder, fetch_site_metadata, html_to_site_metadata, SiteMetadata};
use lemmy_utils::settings::SETTINGS;
use url::Url;
@ -314,11 +315,7 @@ mod tests {
#[tokio::test]
async fn test_site_metadata() {
let settings = &SETTINGS.clone();
let client = reqwest::Client::builder()
.user_agent(build_user_agent(settings))
.build()
.unwrap()
.into();
let client = client_builder(settings).build().unwrap().into();
let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap();
let sample_res = fetch_site_metadata(&client, &sample_url).await.unwrap();
assert_eq!(

View File

@ -18,7 +18,7 @@ pub async fn get_private_message(
let limit = data.limit;
let unread_only = data.unread_only.unwrap_or_default();
let creator_id = data.creator_id;
let mut messages = PrivateMessageQuery {
let messages = PrivateMessageQuery {
page,
limit,
unread_only,
@ -27,14 +27,6 @@ pub async fn get_private_message(
.list(&mut context.pool(), person_id)
.await?;
// Messages sent by ourselves should be marked as read. The `read` column in database is only
// for the recipient, and shouldnt be exposed to sender.
messages.iter_mut().for_each(|pmv| {
if pmv.creator.id == person_id {
pmv.private_message.read = true
}
});
Ok(Json(PrivateMessagesResponse {
private_messages: messages,
}))

View File

@ -59,10 +59,10 @@ pub(crate) mod tests {
use activitypub_federation::config::{Data, FederationConfig};
use anyhow::anyhow;
use lemmy_api_common::{context::LemmyContext, request::build_user_agent};
use lemmy_api_common::{context::LemmyContext, request::client_builder};
use lemmy_db_schema::{source::secret::Secret, utils::build_db_pool_for_tests};
use lemmy_utils::{rate_limit::RateLimitCell, settings::SETTINGS};
use reqwest::{Client, Request, Response};
use reqwest::{Request, Response};
use reqwest_middleware::{ClientBuilder, Middleware, Next};
use task_local_extensions::Extensions;
@ -86,11 +86,7 @@ pub(crate) mod tests {
// call this to run migrations
let pool = build_db_pool_for_tests().await;
let settings = SETTINGS.clone();
let client = Client::builder()
.user_agent(build_user_agent(&settings))
.build()
.unwrap();
let client = client_builder(&SETTINGS).build().unwrap();
let client = ClientBuilder::new(client).with(BlockedMiddleware).build();
let secret = Secret {

View File

@ -71,8 +71,10 @@ pub struct PersonAggregates {
pub id: i32,
pub person_id: PersonId,
pub post_count: i64,
#[serde(skip)]
pub post_score: i64,
pub comment_count: i64,
#[serde(skip)]
pub comment_score: i64,
}

View File

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

View File

@ -12,6 +12,7 @@ use diesel::{
use diesel_async::RunQueryDsl;
use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions};
use lemmy_db_schema::{
aliases,
newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId},
schema::{
comment,
@ -90,6 +91,17 @@ fn queries<'a>() -> Queries<
.and(community_moderator::person_id.eq(person_id_join)),
),
)
.left_join(
aliases::community_moderator1.on(
community::id
.eq(aliases::community_moderator1.field(community_moderator::community_id))
.and(
aliases::community_moderator1
.field(community_moderator::person_id)
.eq(comment::creator_id),
),
),
)
};
let selection = (
@ -99,6 +111,10 @@ fn queries<'a>() -> Queries<
community::all_columns,
comment_aggregates::all_columns,
community_person_ban::id.nullable().is_not_null(),
aliases::community_moderator1
.field(community_moderator::id)
.nullable()
.is_not_null(),
CommunityFollower::select_subscribed_type(),
comment_saved::id.nullable().is_not_null(),
person_block::id.nullable().is_not_null(),
@ -338,7 +354,7 @@ mod tests {
source::{
actor_language::LocalUserLanguage,
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm},
community::{Community, CommunityInsertForm},
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
instance::Instance,
language::Language,
local_user::{LocalUser, LocalUserInsertForm},
@ -346,7 +362,7 @@ mod tests {
person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm},
},
traits::{Blockable, Crud, Likeable},
traits::{Blockable, Crud, Joinable, Likeable},
utils::build_db_pool_for_tests,
SubscribedType,
};
@ -779,6 +795,30 @@ mod tests {
cleanup(data, pool).await;
}
#[tokio::test]
#[serial]
async fn test_creator_is_moderator() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
// Make one of the inserted persons a moderator
let person_id = data.inserted_person_2.id;
let community_id = data.inserted_community.id;
let form = CommunityModeratorForm {
community_id,
person_id,
};
CommunityModerator::join(pool, &form).await.unwrap();
// Make sure that they come back as a mod in the list
let comments = CommentQuery::default().list(pool).await.unwrap();
assert!(comments[1].creator_is_moderator);
cleanup(data, pool).await;
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
CommentLike::remove(
pool,
@ -814,6 +854,7 @@ mod tests {
.unwrap();
CommentView {
creator_banned_from_community: false,
creator_is_moderator: false,
my_vote: None,
subscribed: SubscribedType::NotSubscribed,
saved: false,

View File

@ -107,6 +107,13 @@ fn queries<'a>() -> Queries<
.and(community_person_ban::person_id.eq(post_aggregates::creator_id)),
),
);
let creator_is_moderator = exists(
community_moderator::table.filter(
post_aggregates::community_id
.eq(community_moderator::community_id)
.and(community_moderator::person_id.eq(post_aggregates::creator_id)),
),
);
let is_saved = |person_id| {
exists(
@ -226,6 +233,7 @@ fn queries<'a>() -> Queries<
person::all_columns,
community::all_columns,
is_creator_banned_from_community,
creator_is_moderator,
post_aggregates::all_columns,
subscribed_type_selection,
is_saved_selection,
@ -542,16 +550,10 @@ impl PostView {
my_person_id: Option<PersonId>,
is_mod_or_admin: bool,
) -> Result<Self, Error> {
let mut res = queries()
let res = queries()
.read(pool, (post_id, my_person_id, is_mod_or_admin))
.await?;
// If a person is given, then my_vote, if None, should be 0, not null
// Necessary to differentiate between other person's votes
if my_person_id.is_some() && res.my_vote.is_none() {
res.my_vote = Some(0)
};
Ok(res)
}
}
@ -711,7 +713,7 @@ mod tests {
newtypes::LanguageId,
source::{
actor_language::LocalUserLanguage,
community::{Community, CommunityInsertForm},
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
community_block::{CommunityBlock, CommunityBlockForm},
instance::Instance,
instance_block::{InstanceBlock, InstanceBlockForm},
@ -721,7 +723,7 @@ mod tests {
person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
},
traits::{Blockable, Crud, Likeable},
traits::{Blockable, Crud, Joinable, Likeable},
utils::{build_db_pool_for_tests, DbPool},
SortType,
SubscribedType,
@ -877,7 +879,7 @@ mod tests {
assert_eq!(1, read_post_listing.len());
assert_eq!(expected_post_listing_with_user, read_post_listing[0]);
expected_post_listing_with_user.my_vote = Some(0);
expected_post_listing_with_user.my_vote = None;
assert_eq!(
expected_post_listing_with_user,
post_listing_single_with_person
@ -1069,6 +1071,36 @@ mod tests {
cleanup(data, pool).await;
}
#[tokio::test]
#[serial]
async fn creator_is_moderator() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
// Make one of the inserted persons a moderator
let person_id = data.local_user_view.person.id;
let community_id = data.inserted_community.id;
let form = CommunityModeratorForm {
community_id,
person_id,
};
CommunityModerator::join(pool, &form).await.unwrap();
let post_listing = PostQuery {
sort: (Some(SortType::New)),
community_id: (Some(data.inserted_community.id)),
local_user: (Some(&data.local_user_view)),
..Default::default()
}
.list(pool)
.await
.unwrap();
assert!(post_listing[1].creator_is_moderator);
cleanup(data, pool).await;
}
#[tokio::test]
#[serial]
async fn post_listing_person_language() {
@ -1402,6 +1434,7 @@ mod tests {
last_refreshed_at: inserted_person.last_refreshed_at,
},
creator_banned_from_community: false,
creator_is_moderator: false,
community: Community {
id: inserted_community.id,
name: inserted_community.name.clone(),

View File

@ -12,7 +12,7 @@ use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
aliases,
newtypes::{PersonId, PrivateMessageId},
schema::{person, private_message},
schema::{person, person_block, private_message},
utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
};
use tracing::debug;
@ -27,6 +27,13 @@ fn queries<'a>() -> Queries<
.inner_join(
aliases::person1.on(private_message::recipient_id.eq(aliases::person1.field(person::id))),
)
.left_join(
person_block::table.on(
private_message::creator_id
.eq(person_block::target_id)
.and(person_block::person_id.eq(aliases::person1.field(person::id))),
),
)
};
let selection = (
@ -45,7 +52,10 @@ fn queries<'a>() -> Queries<
let list = move |mut conn: DbConn<'a>,
(options, recipient_id): (PrivateMessageQuery, PersonId)| async move {
let mut query = all_joins(private_message::table.into_boxed()).select(selection);
let mut query = all_joins(private_message::table.into_boxed())
.select(selection)
// Dont show replies from blocked users
.filter(person_block::person_id.is_null());
// If its unread, I only want the ones to me
if options.unread_only {
@ -106,6 +116,15 @@ impl PrivateMessageView {
use diesel::dsl::count;
let conn = &mut get_conn(pool).await?;
private_message::table
.left_join(
person_block::table.on(
private_message::creator_id
.eq(person_block::target_id)
.and(person_block::person_id.eq(my_person_id)),
),
)
// Dont count replies from blocked users
.filter(person_block::person_id.is_null())
.filter(private_message::read.eq(false))
.filter(private_message::recipient_id.eq(my_person_id))
.filter(private_message::deleted.eq(false))
@ -138,14 +157,15 @@ mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::private_message_view::PrivateMessageQuery;
use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView};
use lemmy_db_schema::{
source::{
instance::Instance,
person::{Person, PersonInsertForm},
person_block::{PersonBlock, PersonBlockForm},
private_message::{PrivateMessage, PrivateMessageInsertForm},
},
traits::Crud,
traits::{Blockable, Crud},
utils::build_db_pool_for_tests,
};
use serial_test::serial;
@ -280,5 +300,39 @@ mod tests {
assert_eq!(timmy_sara_unread_messages.len(), 1);
assert_eq!(timmy_sara_unread_messages[0].creator.id, sara.id);
assert_eq!(timmy_sara_unread_messages[0].recipient.id, timmy.id);
// Make sure blocks are working
let timmy_blocks_sara_form = PersonBlockForm {
person_id: timmy.id,
target_id: sara.id,
};
let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form)
.await
.unwrap();
let expected_block = PersonBlock {
id: inserted_block.id,
person_id: timmy.id,
target_id: sara.id,
published: inserted_block.published,
};
assert_eq!(expected_block, inserted_block);
let timmy_messages = PrivateMessageQuery {
unread_only: true,
creator_id: Option::None,
..Default::default()
}
.list(pool, timmy.id)
.await
.unwrap();
assert_eq!(timmy_messages.len(), 1);
let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id)
.await
.unwrap();
assert_eq!(timmy_unread_messages, 1);
}
}

View File

@ -56,6 +56,7 @@ pub struct CommentView {
pub community: Community,
pub counts: CommentAggregates,
pub creator_banned_from_community: bool,
pub creator_is_moderator: bool,
pub subscribed: SubscribedType,
pub saved: bool,
pub creator_blocked: bool,
@ -107,6 +108,7 @@ pub struct PostView {
pub creator: Person,
pub community: Community,
pub creator_banned_from_community: bool,
pub creator_is_moderator: bool,
pub counts: PostAggregates,
pub subscribed: SubscribedType,
pub saved: bool,

View File

@ -31,3 +31,7 @@ ts-rs = { workspace = true, optional = true }
chrono.workspace = true
strum = { workspace = true }
strum_macros = { workspace = true }
[dev-dependencies]
serial_test = { workspace = true }
tokio = { workspace = true }

View File

@ -164,6 +164,15 @@ impl CommentReplyView {
comment_reply::table
.inner_join(comment::table)
.left_join(
person_block::table.on(
comment::creator_id
.eq(person_block::target_id)
.and(person_block::person_id.eq(my_person_id)),
),
)
// Dont count replies from blocked users
.filter(person_block::person_id.is_null())
.filter(comment_reply::recipient_id.eq(my_person_id))
.filter(comment_reply::read.eq(false))
.filter(comment::deleted.eq(false))

View File

@ -52,6 +52,7 @@ fn queries<'a>(
query
.inner_join(person_aggregates::table)
.left_join(local_user::table)
.filter(person::deleted.eq(false))
.select((person::all_columns, person_aggregates::all_columns))
};
@ -151,3 +152,165 @@ impl PersonQuery {
queries().list(pool, ListMode::Query(self)).await
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use diesel::NotFound;
use lemmy_db_schema::{
source::{
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
person::{Person, PersonInsertForm, PersonUpdateForm},
},
traits::Crud,
utils::build_db_pool_for_tests,
};
use serial_test::serial;
struct Data {
alice: Person,
alice_local_user: LocalUser,
bob: Person,
bob_local_user: LocalUser,
}
async fn init_data(pool: &mut DbPool<'_>) -> Data {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let alice_form = PersonInsertForm::builder()
.name("alice".to_string())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let alice = Person::create(pool, &alice_form).await.unwrap();
let alice_local_user_form = LocalUserInsertForm::builder()
.person_id(alice.id)
.password_encrypted(String::new())
.build();
let alice_local_user = LocalUser::create(pool, &alice_local_user_form)
.await
.unwrap();
let bob_form = PersonInsertForm::builder()
.name("bob".to_string())
.bot_account(Some(true))
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();
let bob = Person::create(pool, &bob_form).await.unwrap();
let bob_local_user_form = LocalUserInsertForm::builder()
.person_id(bob.id)
.password_encrypted(String::new())
.build();
let bob_local_user = LocalUser::create(pool, &bob_local_user_form).await.unwrap();
Data {
alice,
alice_local_user,
bob,
bob_local_user,
}
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
LocalUser::delete(pool, data.alice_local_user.id)
.await
.unwrap();
LocalUser::delete(pool, data.bob_local_user.id)
.await
.unwrap();
Person::delete(pool, data.alice.id).await.unwrap();
Person::delete(pool, data.bob.id).await.unwrap();
Instance::delete(pool, data.bob.instance_id).await.unwrap();
}
#[tokio::test]
#[serial]
async fn exclude_deleted() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
Person::update(
pool,
data.alice.id,
&PersonUpdateForm {
deleted: Some(true),
..Default::default()
},
)
.await
.unwrap();
let read = PersonView::read(pool, data.alice.id).await;
assert_eq!(read.err(), Some(NotFound));
let list = PersonQuery::default().list(pool).await.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].person.id, data.bob.id);
cleanup(data, pool).await;
}
#[tokio::test]
#[serial]
async fn list_banned() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
Person::update(
pool,
data.alice.id,
&PersonUpdateForm {
banned: Some(true),
..Default::default()
},
)
.await
.unwrap();
let list = PersonView::banned(pool).await.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].person.id, data.alice.id);
cleanup(data, pool).await;
}
#[tokio::test]
#[serial]
async fn list_admins() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
LocalUser::update(
pool,
data.alice_local_user.id,
&LocalUserUpdateForm {
admin: Some(true),
..Default::default()
},
)
.await
.unwrap();
let list = PersonView::admins(pool).await.unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].person.id, data.alice.id);
let is_admin = PersonView::is_admin(pool, data.alice.id).await.unwrap();
assert!(is_admin);
let is_admin = PersonView::is_admin(pool, data.bob.id).await.unwrap();
assert!(!is_admin);
cleanup(data, pool).await;
}
}

View File

@ -30,12 +30,12 @@ serde.workspace = true
tokio = { workspace = true, features = ["full"] }
tracing.workspace = true
async-trait = "0.1.71"
bytes = "1.4.0"
async-trait = "0.1.74"
bytes = "1.5.0"
enum_delegate = "0.2.0"
moka = { version = "0.11.2", features = ["future"] }
openssl = "0.10.55"
reqwest-middleware = "0.2.2"
reqwest-tracing = "0.4.5"
tokio-util = "0.7.8"
moka = { version = "0.11.3", features = ["future"] }
openssl = "0.10.57"
reqwest-middleware = "0.2.4"
reqwest-tracing = "0.4.6"
tokio-util = "0.7.9"
tracing-subscriber = "0.3.17"

View File

@ -31,4 +31,4 @@ once_cell = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
urlencoding = { workspace = true }
rss = "2.0.4"
rss = "2.0.6"

View File

@ -2,7 +2,7 @@ use crate::local_user_view_from_jwt;
use actix_web::{error::ErrorBadRequest, web, Error, HttpRequest, HttpResponse, Result};
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{context::LemmyContext, utils::check_private_instance};
use lemmy_db_schema::{
source::{community::Community, person::Person},
traits::ApubActor,
@ -132,6 +132,8 @@ async fn get_feed_data(
) -> Result<HttpResponse, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
check_private_instance(&None, &site_view.local_site)?;
let posts = PostQuery {
listing_type: (Some(listing_type)),
sort: (Some(sort_type)),
@ -235,6 +237,8 @@ async fn get_feed_user(
let site_view = SiteView::read_local(&mut context.pool()).await?;
let person = Person::read_from_name(&mut context.pool(), user_name, false).await?;
check_private_instance(&None, &site_view.local_site)?;
let posts = PostQuery {
listing_type: (Some(ListingType::All)),
sort: (Some(*sort_type)),
@ -269,6 +273,8 @@ async fn get_feed_community(
let site_view = SiteView::read_local(&mut context.pool()).await?;
let community = Community::read_from_name(&mut context.pool(), community_name, false).await?;
check_private_instance(&None, &site_view.local_site)?;
let posts = PostQuery {
sort: (Some(*sort_type)),
community_id: (Some(community.id)),
@ -306,6 +312,8 @@ async fn get_feed_front(
let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_user = local_user_view_from_jwt(jwt, context).await?;
check_private_instance(&Some(local_user.clone()), &site_view.local_site)?;
let posts = PostQuery {
listing_type: (Some(ListingType::Subscribed)),
local_user: (Some(&local_user)),
@ -343,6 +351,8 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> Result<ChannelBuil
let sort = CommentSortType::New;
check_private_instance(&Some(local_user.clone()), &site_view.local_site)?;
let replies = CommentReplyQuery {
recipient_id: (Some(person_id)),
my_person_id: (Some(person_id)),

View File

@ -23,8 +23,13 @@ use reqwest_middleware::{ClientWithMiddleware, RequestBuilder};
use serde::{Deserialize, Serialize};
use std::time::Duration;
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
pub fn config(
cfg: &mut web::ServiceConfig,
client: ClientWithMiddleware,
rate_limit: &RateLimitCell,
) {
cfg
.app_data(web::Data::new(client))
.service(
web::resource("/pictrs/image")
.wrap(rate_limit.image())
@ -90,13 +95,14 @@ async fn upload(
body: web::Payload,
// require login
local_user_view: LocalUserView,
client: web::Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>,
) -> Result<HttpResponse, Error> {
// TODO: check rate limit here
let pictrs_config = context.settings().pictrs_config()?;
let image_url = format!("{}image", pictrs_config.url);
let mut client_req = adapt_request(&req, context.client(), image_url);
let mut client_req = adapt_request(&req, &client, image_url);
if let Some(addr) = req.head().peer_addr {
client_req = client_req.header("X-Forwarded-For", addr.to_string())
@ -130,6 +136,7 @@ async fn full_res(
filename: web::Path<String>,
web::Query(params): web::Query<PictrsParams>,
req: HttpRequest,
client: web::Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> Result<HttpResponse, Error> {
@ -160,7 +167,7 @@ async fn full_res(
url
};
image(url, req, context.client()).await
image(url, req, &client).await
}
async fn image(
@ -196,6 +203,7 @@ async fn image(
async fn delete(
components: web::Path<(String, String)>,
req: HttpRequest,
client: web::Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>,
// require login
_local_user_view: LocalUserView,
@ -205,7 +213,7 @@ async fn delete(
let pictrs_config = context.settings().pictrs_config()?;
let url = format!("{}image/delete/{}/{}", pictrs_config.url, &token, &file);
let mut client_req = adapt_request(&req, context.client(), url);
let mut client_req = adapt_request(&req, &client, url);
if let Some(addr) = req.head().peer_addr {
client_req = client_req.header("X-Forwarded-For", addr.to_string());

View File

@ -17,6 +17,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
"/nodeinfo/2.0.json",
web::get().to(node_info).wrap(cache_1hour()),
)
.service(web::redirect("/version", "/nodeinfo/2.0.json"))
.route(
"/.well-known/nodeinfo",
web::get().to(node_info_well_known).wrap(cache_3days()),

View File

@ -41,7 +41,7 @@ typed-builder = { workspace = true }
percent-encoding = { workspace = true }
tokio = { workspace = true }
urlencoding = { workspace = true }
openssl = "0.10.55"
openssl = "0.10.57"
html2text = "0.6.0"
deser-hjson = "1.2.0"
smart-default = "0.7.1"

View File

@ -56,6 +56,9 @@ impl Display for LemmyError {
impl actix_web::error::ResponseError for LemmyError {
fn status_code(&self) -> http::StatusCode {
if self.error_type == LemmyErrorType::IncorrectLogin {
return http::StatusCode::UNAUTHORIZED;
}
match self.inner.downcast_ref::<diesel::result::Error>() {
Some(diesel::result::Error::NotFound) => http::StatusCode::NOT_FOUND,
_ => http::StatusCode::BAD_REQUEST,

View File

@ -4,18 +4,20 @@ use once_cell::sync::Lazy;
use regex::{Regex, RegexBuilder};
use url::Url;
static VALID_ACTOR_NAME_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex"));
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._=-]+:[A-Za-z0-9.-]+\.[A-Za-z]{2,}$").expect("compile regex")
Regex::new(r"^@[A-Za-z0-9\\x21-\\x39\\x3B-\\x7F]+:[A-Za-z0-9.-]+(:[0-9]{2,5})?$")
.expect("compile regex")
});
// taken from https://en.wikipedia.org/wiki/UTM_parameters
static CLEAN_URL_PARAMS_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|gclsrc|dclid|fbclid$")
.expect("compile regex")
});
const ALLOWED_POST_URL_SCHEMES: [&str; 3] = ["http", "https", "magnet"];
const BODY_MAX_LENGTH: usize = 10000;
const POST_BODY_MAX_LENGTH: usize = 50000;
@ -85,23 +87,52 @@ fn has_newline(name: &str) -> bool {
}
pub fn is_valid_actor_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> {
let check = name.chars().count() <= actor_name_max_length
&& VALID_ACTOR_NAME_REGEX.is_match(name)
&& !has_newline(name);
if !check {
static VALID_ACTOR_NAME_REGEX_EN: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex"));
static VALID_ACTOR_NAME_REGEX_AR: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[\p{Arabic}0-9_]{3,}$").expect("compile regex"));
static VALID_ACTOR_NAME_REGEX_RU: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[\p{Cyrillic}0-9_]{3,}$").expect("compile regex"));
let check = name.chars().count() <= actor_name_max_length && !has_newline(name);
// Only allow characters from a single alphabet per username. This avoids problems with lookalike
// characters like `o` which looks identical in Latin and Cyrillic, and can be used to imitate
// other users. Checks for additional alphabets can be added in the same way.
let lang_check = VALID_ACTOR_NAME_REGEX_EN.is_match(name)
|| VALID_ACTOR_NAME_REGEX_AR.is_match(name)
|| VALID_ACTOR_NAME_REGEX_RU.is_match(name);
if !check || !lang_check {
Err(LemmyErrorType::InvalidName.into())
} else {
Ok(())
}
}
fn has_3_permitted_display_chars(name: &str) -> bool {
let mut num_non_fdc: i8 = 0;
for c in name.chars() {
if !FORBIDDEN_DISPLAY_CHARS.contains(&c) {
num_non_fdc += 1;
if num_non_fdc >= 3 {
break;
}
}
}
if num_non_fdc >= 3 {
return true;
}
false
}
// Can't do a regex here, reverse lookarounds not supported
pub fn is_valid_display_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> {
let check = !name.contains(FORBIDDEN_DISPLAY_CHARS)
&& !name.starts_with('@')
&& name.chars().count() >= 3
let check = !name.starts_with('@')
&& !name.starts_with(FORBIDDEN_DISPLAY_CHARS)
&& name.chars().count() <= actor_name_max_length
&& !has_newline(name);
&& !has_newline(name)
&& has_3_permitted_display_chars(name);
if !check {
Err(LemmyErrorType::InvalidDisplayName.into())
} else {
@ -247,7 +278,7 @@ pub fn check_site_visibility_valid(
pub fn check_url_scheme(url: &Option<Url>) -> LemmyResult<()> {
if let Some(url) = url {
if url.scheme() != "http" && url.scheme() != "https" {
if !ALLOWED_POST_URL_SCHEMES.contains(&url.scheme()) {
Err(LemmyErrorType::InvalidUrlScheme.into())
} else {
Ok(())
@ -309,8 +340,18 @@ mod tests {
let actor_name_max_length = 20;
assert!(is_valid_actor_name("Hello_98", actor_name_max_length).is_ok());
assert!(is_valid_actor_name("ten", actor_name_max_length).is_ok());
assert!(is_valid_actor_name("تجريب", actor_name_max_length).is_ok());
assert!(is_valid_actor_name("تجريب_123", actor_name_max_length).is_ok());
assert!(is_valid_actor_name("Владимир", actor_name_max_length).is_ok());
// mixed scripts
assert!(is_valid_actor_name("تجريب_abc", actor_name_max_length).is_err());
assert!(is_valid_actor_name("Влад_abc", actor_name_max_length).is_err());
// dash
assert!(is_valid_actor_name("Hello-98", actor_name_max_length).is_err());
// too short
assert!(is_valid_actor_name("a", actor_name_max_length).is_err());
// empty
assert!(is_valid_actor_name("", actor_name_max_length).is_err());
}
@ -319,6 +360,13 @@ mod tests {
let actor_name_max_length = 20;
assert!(is_valid_display_name("hello @there", actor_name_max_length).is_ok());
assert!(is_valid_display_name("@hello there", actor_name_max_length).is_err());
assert!(is_valid_display_name("\u{200d}hello", actor_name_max_length).is_err());
assert!(is_valid_display_name(
"\u{1f3f3}\u{fe0f}\u{200d}\u{26a7}\u{fe0f}Name",
actor_name_max_length
)
.is_ok());
assert!(is_valid_display_name("\u{2003}1\u{ffa0}2\u{200d}", actor_name_max_length).is_err());
// Make sure zero-space with an @ doesn't work
assert!(
@ -336,9 +384,11 @@ mod tests {
#[test]
fn test_valid_matrix_id() {
assert!(is_valid_matrix_id("@dess:matrix.org").is_ok());
assert!(is_valid_matrix_id("@dess:matrix.org:443").is_ok());
assert!(is_valid_matrix_id("dess:matrix.org").is_err());
assert!(is_valid_matrix_id(" @dess:matrix.org").is_err());
assert!(is_valid_matrix_id("@dess:matrix.org t").is_err());
assert!(is_valid_matrix_id("@dess:matrix.org t").is_err());
}
#[test]
@ -472,7 +522,11 @@ mod tests {
assert!(check_url_scheme(&None).is_ok());
assert!(check_url_scheme(&Some(Url::parse("http://example.com").unwrap())).is_ok());
assert!(check_url_scheme(&Some(Url::parse("https://example.com").unwrap())).is_ok());
assert!(check_url_scheme(&Some(Url::parse("https://example.com").unwrap())).is_ok());
assert!(check_url_scheme(&Some(Url::parse("ftp://example.com").unwrap())).is_err());
assert!(check_url_scheme(&Some(Url::parse("javascript:void").unwrap())).is_err());
let magnet_link="magnet:?xt=urn:btih:4b390af3891e323778959d5abfff4b726510f14c&dn=Ravel%20Complete%20Piano%20Sheet%20Music%20-%20Public%20Domain&tr=udp%3A%2F%2Fopen.tracker.cl%3A1337%2Fannounce";
assert!(check_url_scheme(&Some(Url::parse(magnet_link).unwrap())).is_ok());
}
}

View File

@ -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.18.4
image: dessalines/lemmy-ui:0.19.0-rc.3
# 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

View File

@ -45,9 +45,9 @@ if [ "$ARCH" = 'arm64' ]; then
fi
echo "$LOG_PREFIX Initializing images in the background. Please be patient if compiling from source..."
docker compose up -d --build
docker compose up --build
else
sudo docker compose up -d --build
sudo docker compose up --build
fi
echo "$LOG_PREFIX Complete! You can now access the UI at http://localhost:1236."

View File

@ -56,7 +56,7 @@ http {
}
# backend
location ~ ^/(api|pictrs|feeds|nodeinfo|.well-known) {
location ~ ^/(api|pictrs|feeds|nodeinfo|version|.well-known) {
proxy_pass "http://lemmy";
# proxy common stuff
proxy_http_version 1.1;

View File

@ -28,7 +28,7 @@ use clap::{ArgAction, Parser};
use lemmy_api_common::{
context::LemmyContext,
lemmy_db_views::structs::SiteView,
request::build_user_agent,
request::client_builder,
send_activity::{ActivityChannel, MATCH_OUTGOING_ACTIVITIES},
utils::{
check_private_instance_and_federation_enabled,
@ -52,11 +52,10 @@ use lemmy_utils::{
response::jsonify_plain_text_errors,
settings::{structs::Settings, SETTINGS},
};
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
use reqwest_tracing::TracingMiddleware;
use serde_json::json;
use std::{env, ops::Deref, time::Duration};
use std::{env, ops::Deref};
use tokio::signal::unix::SignalKind;
use tracing::subscriber::set_global_default;
use tracing_actix_web::TracingLogger;
@ -112,13 +111,9 @@ pub struct CmdArgs {
#[arg(long, default_value_t = 1)]
federate_process_count: i32,
}
/// Max timeout for http requests
pub(crate) const REQWEST_TIMEOUT: Duration = Duration::from_secs(10);
/// 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> {
let settings = SETTINGS.to_owned();
// return error 503 while running db migrations and startup tasks
let mut startup_server_handle = None;
if args.http_server {
@ -126,14 +121,14 @@ pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
}
// Run the DB migrations
let db_url = get_database_url(Some(&settings));
let db_url = get_database_url(Some(&SETTINGS));
run_migrations(&db_url);
// Set up the connection pool
let pool = build_db_pool(&settings).await?;
let pool = build_db_pool(&SETTINGS).await?;
// Run the Code-required migrations
run_advanced_migrations(&mut (&pool).into(), &settings).await?;
run_advanced_migrations(&mut (&pool).into(), &SETTINGS).await?;
// Initialize the secrets
let secret = Secret::init(&mut (&pool).into())
@ -148,7 +143,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)?;
@ -160,20 +155,12 @@ pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
println!(
"Starting http server at {}:{}",
settings.bind, settings.port
SETTINGS.bind, SETTINGS.port
);
let user_agent = build_user_agent(&settings);
let reqwest_client = Client::builder()
.user_agent(user_agent.clone())
.timeout(REQWEST_TIMEOUT)
.connect_timeout(REQWEST_TIMEOUT)
.build()?;
let client = ClientBuilder::new(reqwest_client.clone())
let client = ClientBuilder::new(client_builder(&SETTINGS).build()?)
.with(TracingMiddleware::default())
.build();
let context = LemmyContext::create(
pool.clone(),
client.clone(),
@ -187,10 +174,10 @@ pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
}
#[cfg(feature = "prometheus-metrics")]
serve_prometheus(settings.prometheus.as_ref(), context.clone());
serve_prometheus(SETTINGS.prometheus.as_ref(), context.clone());
let federation_config = FederationConfig::builder()
.domain(settings.hostname.clone())
.domain(SETTINGS.hostname.clone())
.app_data(context.clone())
.client(client.clone())
.http_fetch_limit(FEDERATION_HTTP_FETCH_LIMIT)
@ -212,9 +199,10 @@ pub async fn start_lemmy_server(args: CmdArgs) -> Result<(), LemmyError> {
if let Some(startup_server_handle) = startup_server_handle {
startup_server_handle.stop(true).await;
}
Some(create_http_server(
federation_config.clone(),
settings.clone(),
SETTINGS.clone(),
federation_enabled,
)?)
} else {
@ -293,6 +281,12 @@ fn create_http_server(
let context: LemmyContext = federation_config.deref().clone();
let rate_limit_cell = federation_config.rate_limit_cell().clone();
let self_origin = settings.get_protocol_and_hostname();
// Pictrs cannot use proxy
let pictrs_client = ClientBuilder::new(client_builder(&SETTINGS).no_proxy().build()?)
.with(TracingMiddleware::default())
.build();
// Create Http server with websocket support
let server = HttpServer::new(move || {
let cors_origin = env::var("LEMMY_CORS_ORIGIN");
@ -335,7 +329,7 @@ fn create_http_server(
}
})
.configure(feeds::config)
.configure(|cfg| images::config(cfg, &rate_limit_cell))
.configure(|cfg| images::config(cfg, pictrs_client.clone(), &rate_limit_cell))
.configure(nodeinfo::config)
})
.disable_signals()
@ -358,9 +352,9 @@ pub fn init_logging(opentelemetry_url: &Option<Url>) -> Result<(), LemmyError> {
let format_layer = {
#[cfg(feature = "json-log")]
let layer = tracing_subscriber::fmt::layer().json();
let layer = tracing_subscriber::fmt::layer().with_ansi(false).json();
#[cfg(not(feature = "json-log"))]
let layer = tracing_subscriber::fmt::layer();
let layer = tracing_subscriber::fmt::layer().with_ansi(false);
layer.with_filter(targets.clone())
};