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 = { 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_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" } 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", "actix-web",
] } ] }
diesel = "2.1.0" diesel = "2.1.3"
diesel_migrations = "2.1.0" diesel_migrations = "2.1.0"
diesel-async = "0.3.1" diesel-async = "0.3.2"
serde = { version = "1.0.167", features = ["derive"] } serde = { version = "1.0.189", features = ["derive"] }
serde_with = "3.0.0" serde_with = "3.4.0"
actix-web = { version = "4.3.1", default-features = false, features = [ actix-web = { version = "4.4.0", default-features = false, features = [
"macros", "macros",
"rustls", "rustls",
"compress-brotli", "compress-brotli",
@ -86,37 +86,37 @@ actix-web = { version = "4.3.1", default-features = false, features = [
"compress-zstd", "compress-zstd",
"cookies", "cookies",
] } ] }
tracing = "0.1.37" tracing = "0.1.40"
tracing-actix-web = { version = "0.7.5", default-features = false } tracing-actix-web = { version = "0.7.8", default-features = false }
tracing-error = "0.2.0" tracing-error = "0.2.0"
tracing-log = "0.1.3" tracing-log = "0.1.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
url = { version = "2.4.0", features = ["serde"] } url = { version = "2.4.1", features = ["serde"] }
reqwest = { version = "0.11.18", features = ["json", "blocking", "gzip"] } reqwest = { version = "0.11.22", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.2" reqwest-middleware = "0.2.4"
reqwest-tracing = "0.4.5" reqwest-tracing = "0.4.6"
clokwerk = "0.4.0" clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] } doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.0" bcrypt = "0.15.0"
chrono = { version = "0.4.26", features = ["serde"], default-features = false } chrono = { version = "0.4.31", features = ["serde"], default-features = false }
serde_json = { version = "1.0.100", features = ["preserve_order"] } serde_json = { version = "1.0.107", features = ["preserve_order"] }
base64 = "0.21.2" base64 = "0.21.5"
uuid = { version = "1.4.0", features = ["serde", "v4"] } uuid = { version = "1.5.0", features = ["serde", "v4"] }
async-trait = "0.1.71" async-trait = "0.1.74"
captcha = "0.0.9" captcha = "0.0.9"
anyhow = { version = "1.0.71", features = [ anyhow = { version = "1.0.75", features = [
"backtrace", "backtrace",
] } # backtrace is on by default on nightly, but not stable rust ] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.0" diesel_ltree = "0.3.0"
typed-builder = "0.15.0" typed-builder = "0.15.2"
serial_test = "2.0.0" serial_test = "2.0.0"
tokio = { version = "1.29.1", features = ["full"] } tokio = { version = "1.33.0", features = ["full"] }
regex = "1.9.0" regex = "1.10.2"
once_cell = "1.18.0" once_cell = "1.18.0"
diesel-derive-newtype = "2.1.0" diesel-derive-newtype = "2.1.0"
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] } diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = "0.25.0" strum = "0.25.0"
strum_macros = "0.25.1" strum_macros = "0.25.3"
itertools = "0.11.0" itertools = "0.11.0"
futures = "0.3.28" futures = "0.3.28"
http = "0.2.9" http = "0.2.9"
@ -125,12 +125,12 @@ rosetta-i18n = "0.1.3"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] } opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.19.0" } tracing-opentelemetry = { version = "0.19.0" }
ts-rs = { version = "7.0.0", features = ["serde-compat", "chrono-impl"] } 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" futures-util = "0.3.28"
tokio-postgres = "0.7.8" tokio-postgres = "0.7.10"
tokio-postgres-rustls = "0.10.0" tokio-postgres-rustls = "0.10.0"
urlencoding = "2.1.3" urlencoding = "2.1.3"
enum-map = "2.6" enum-map = "2.7"
[dependencies] [dependencies]
lemmy_api = { workspace = true } lemmy_api = { workspace = true }
@ -162,7 +162,7 @@ tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true }
console-subscriber = { version = "0.1.10", optional = true } console-subscriber = { version = "0.1.10", optional = true }
opentelemetry-otlp = { version = "0.12.0", 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 tokio.workspace = true
actix-cors = "0.6.4" actix-cors = "0.6.4"
rustls = { workspace = true } rustls = { workspace = true }
@ -173,5 +173,5 @@ chrono = { workspace = true }
prometheus = { version = "0.13.3", features = ["process"], optional = true } prometheus = { version = "0.13.3", features = ["process"], optional = true }
actix-web-prom = { version = "0.6.0", optional = true } actix-web-prom = { version = "0.6.0", optional = true }
serial_test = { workspace = 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" 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" "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": { "devDependencies": {
"@types/jest": "^29.5.1", "@types/jest": "^29.5.6",
"@types/node": "^20.8.6", "@types/node": "^20.8.7",
"@typescript-eslint/eslint-plugin": "^6.7.5", "@typescript-eslint/eslint-plugin": "^6.8.0",
"@typescript-eslint/parser": "^6.7.5", "@typescript-eslint/parser": "^6.8.0",
"eslint": "^8.51.0", "eslint": "^8.52.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.1",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.19.0-rc.12", "lemmy-js-client": "0.19.0-rc.12",

View File

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

View File

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

View File

@ -34,7 +34,7 @@ chrono = { workspace = true }
url = { workspace = true } url = { workspace = true }
wav = "1.0.0" wav = "1.0.0"
sitemap-rs = "0.2.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" actix-web-httpauth = "0.8.1"
[dev-dependencies] [dev-dependencies]

View File

@ -21,7 +21,7 @@ use lemmy_db_schema::{
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType}, error::{LemmyError, LemmyErrorType},
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id}, 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() ..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) Person::update(&mut context.pool(), person_id, &person_form)
.await .await
.with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; .ok();
if let Some(discussion_languages) = data.discussion_languages.clone() { if let Some(discussion_languages) = data.discussion_languages.clone() {
LocalUserLanguage::update(&mut context.pool(), discussion_languages, local_user_id).await?; LocalUserLanguage::update(&mut context.pool(), discussion_languages, local_user_id).await?;
@ -128,7 +130,11 @@ pub async fn save_user_settings(
..Default::default() ..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())) Ok(Json(SuccessResponse::default()))
} }

View File

@ -35,6 +35,9 @@ pub async fn leave_admin(
local_user_view.local_user.id, local_user_view.local_user.id,
&LocalUserUpdateForm { &LocalUserUpdateForm {
admin: Some(false), 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() ..Default::default()
}, },
) )

View File

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

View File

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

View File

@ -18,7 +18,7 @@ pub async fn get_private_message(
let limit = data.limit; let limit = data.limit;
let unread_only = data.unread_only.unwrap_or_default(); let unread_only = data.unread_only.unwrap_or_default();
let creator_id = data.creator_id; let creator_id = data.creator_id;
let mut messages = PrivateMessageQuery { let messages = PrivateMessageQuery {
page, page,
limit, limit,
unread_only, unread_only,
@ -27,14 +27,6 @@ pub async fn get_private_message(
.list(&mut context.pool(), person_id) .list(&mut context.pool(), person_id)
.await?; .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 { Ok(Json(PrivateMessagesResponse {
private_messages: messages, private_messages: messages,
})) }))

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@ use diesel::{
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions}; use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions};
use lemmy_db_schema::{ use lemmy_db_schema::{
aliases,
newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId}, newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId},
schema::{ schema::{
comment, comment,
@ -90,6 +91,17 @@ fn queries<'a>() -> Queries<
.and(community_moderator::person_id.eq(person_id_join)), .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 = ( let selection = (
@ -99,6 +111,10 @@ fn queries<'a>() -> Queries<
community::all_columns, community::all_columns,
comment_aggregates::all_columns, comment_aggregates::all_columns,
community_person_ban::id.nullable().is_not_null(), community_person_ban::id.nullable().is_not_null(),
aliases::community_moderator1
.field(community_moderator::id)
.nullable()
.is_not_null(),
CommunityFollower::select_subscribed_type(), CommunityFollower::select_subscribed_type(),
comment_saved::id.nullable().is_not_null(), comment_saved::id.nullable().is_not_null(),
person_block::id.nullable().is_not_null(), person_block::id.nullable().is_not_null(),
@ -338,7 +354,7 @@ mod tests {
source::{ source::{
actor_language::LocalUserLanguage, actor_language::LocalUserLanguage,
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm},
community::{Community, CommunityInsertForm}, community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
instance::Instance, instance::Instance,
language::Language, language::Language,
local_user::{LocalUser, LocalUserInsertForm}, local_user::{LocalUser, LocalUserInsertForm},
@ -346,7 +362,7 @@ mod tests {
person_block::{PersonBlock, PersonBlockForm}, person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm}, post::{Post, PostInsertForm},
}, },
traits::{Blockable, Crud, Likeable}, traits::{Blockable, Crud, Joinable, Likeable},
utils::build_db_pool_for_tests, utils::build_db_pool_for_tests,
SubscribedType, SubscribedType,
}; };
@ -779,6 +795,30 @@ mod tests {
cleanup(data, pool).await; 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<'_>) { async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
CommentLike::remove( CommentLike::remove(
pool, pool,
@ -814,6 +854,7 @@ mod tests {
.unwrap(); .unwrap();
CommentView { CommentView {
creator_banned_from_community: false, creator_banned_from_community: false,
creator_is_moderator: false,
my_vote: None, my_vote: None,
subscribed: SubscribedType::NotSubscribed, subscribed: SubscribedType::NotSubscribed,
saved: false, saved: false,

View File

@ -107,6 +107,13 @@ fn queries<'a>() -> Queries<
.and(community_person_ban::person_id.eq(post_aggregates::creator_id)), .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| { let is_saved = |person_id| {
exists( exists(
@ -226,6 +233,7 @@ fn queries<'a>() -> Queries<
person::all_columns, person::all_columns,
community::all_columns, community::all_columns,
is_creator_banned_from_community, is_creator_banned_from_community,
creator_is_moderator,
post_aggregates::all_columns, post_aggregates::all_columns,
subscribed_type_selection, subscribed_type_selection,
is_saved_selection, is_saved_selection,
@ -542,16 +550,10 @@ impl PostView {
my_person_id: Option<PersonId>, my_person_id: Option<PersonId>,
is_mod_or_admin: bool, is_mod_or_admin: bool,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut res = queries() let res = queries()
.read(pool, (post_id, my_person_id, is_mod_or_admin)) .read(pool, (post_id, my_person_id, is_mod_or_admin))
.await?; .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) Ok(res)
} }
} }
@ -711,7 +713,7 @@ mod tests {
newtypes::LanguageId, newtypes::LanguageId,
source::{ source::{
actor_language::LocalUserLanguage, actor_language::LocalUserLanguage,
community::{Community, CommunityInsertForm}, community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
community_block::{CommunityBlock, CommunityBlockForm}, community_block::{CommunityBlock, CommunityBlockForm},
instance::Instance, instance::Instance,
instance_block::{InstanceBlock, InstanceBlockForm}, instance_block::{InstanceBlock, InstanceBlockForm},
@ -721,7 +723,7 @@ mod tests {
person_block::{PersonBlock, PersonBlockForm}, person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm}, post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
}, },
traits::{Blockable, Crud, Likeable}, traits::{Blockable, Crud, Joinable, Likeable},
utils::{build_db_pool_for_tests, DbPool}, utils::{build_db_pool_for_tests, DbPool},
SortType, SortType,
SubscribedType, SubscribedType,
@ -877,7 +879,7 @@ mod tests {
assert_eq!(1, read_post_listing.len()); assert_eq!(1, read_post_listing.len());
assert_eq!(expected_post_listing_with_user, read_post_listing[0]); 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!( assert_eq!(
expected_post_listing_with_user, expected_post_listing_with_user,
post_listing_single_with_person post_listing_single_with_person
@ -1069,6 +1071,36 @@ mod tests {
cleanup(data, pool).await; 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] #[tokio::test]
#[serial] #[serial]
async fn post_listing_person_language() { async fn post_listing_person_language() {
@ -1402,6 +1434,7 @@ mod tests {
last_refreshed_at: inserted_person.last_refreshed_at, last_refreshed_at: inserted_person.last_refreshed_at,
}, },
creator_banned_from_community: false, creator_banned_from_community: false,
creator_is_moderator: false,
community: Community { community: Community {
id: inserted_community.id, id: inserted_community.id,
name: inserted_community.name.clone(), name: inserted_community.name.clone(),

View File

@ -12,7 +12,7 @@ use diesel_async::RunQueryDsl;
use lemmy_db_schema::{ use lemmy_db_schema::{
aliases, aliases,
newtypes::{PersonId, PrivateMessageId}, newtypes::{PersonId, PrivateMessageId},
schema::{person, private_message}, schema::{person, person_block, private_message},
utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
}; };
use tracing::debug; use tracing::debug;
@ -27,6 +27,13 @@ fn queries<'a>() -> Queries<
.inner_join( .inner_join(
aliases::person1.on(private_message::recipient_id.eq(aliases::person1.field(person::id))), 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 = ( let selection = (
@ -45,7 +52,10 @@ fn queries<'a>() -> Queries<
let list = move |mut conn: DbConn<'a>, let list = move |mut conn: DbConn<'a>,
(options, recipient_id): (PrivateMessageQuery, PersonId)| async move { (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 its unread, I only want the ones to me
if options.unread_only { if options.unread_only {
@ -106,6 +116,15 @@ impl PrivateMessageView {
use diesel::dsl::count; use diesel::dsl::count;
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
private_message::table 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::read.eq(false))
.filter(private_message::recipient_id.eq(my_person_id)) .filter(private_message::recipient_id.eq(my_person_id))
.filter(private_message::deleted.eq(false)) .filter(private_message::deleted.eq(false))
@ -138,14 +157,15 @@ mod tests {
#![allow(clippy::unwrap_used)] #![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)] #![allow(clippy::indexing_slicing)]
use crate::private_message_view::PrivateMessageQuery; use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView};
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
instance::Instance, instance::Instance,
person::{Person, PersonInsertForm}, person::{Person, PersonInsertForm},
person_block::{PersonBlock, PersonBlockForm},
private_message::{PrivateMessage, PrivateMessageInsertForm}, private_message::{PrivateMessage, PrivateMessageInsertForm},
}, },
traits::Crud, traits::{Blockable, Crud},
utils::build_db_pool_for_tests, utils::build_db_pool_for_tests,
}; };
use serial_test::serial; use serial_test::serial;
@ -280,5 +300,39 @@ mod tests {
assert_eq!(timmy_sara_unread_messages.len(), 1); 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].creator.id, sara.id);
assert_eq!(timmy_sara_unread_messages[0].recipient.id, timmy.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 community: Community,
pub counts: CommentAggregates, pub counts: CommentAggregates,
pub creator_banned_from_community: bool, pub creator_banned_from_community: bool,
pub creator_is_moderator: bool,
pub subscribed: SubscribedType, pub subscribed: SubscribedType,
pub saved: bool, pub saved: bool,
pub creator_blocked: bool, pub creator_blocked: bool,
@ -107,6 +108,7 @@ pub struct PostView {
pub creator: Person, pub creator: Person,
pub community: Community, pub community: Community,
pub creator_banned_from_community: bool, pub creator_banned_from_community: bool,
pub creator_is_moderator: bool,
pub counts: PostAggregates, pub counts: PostAggregates,
pub subscribed: SubscribedType, pub subscribed: SubscribedType,
pub saved: bool, pub saved: bool,

View File

@ -31,3 +31,7 @@ ts-rs = { workspace = true, optional = true }
chrono.workspace = true chrono.workspace = true
strum = { workspace = true } strum = { workspace = true }
strum_macros = { 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 comment_reply::table
.inner_join(comment::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::recipient_id.eq(my_person_id))
.filter(comment_reply::read.eq(false)) .filter(comment_reply::read.eq(false))
.filter(comment::deleted.eq(false)) .filter(comment::deleted.eq(false))

View File

@ -52,6 +52,7 @@ fn queries<'a>(
query query
.inner_join(person_aggregates::table) .inner_join(person_aggregates::table)
.left_join(local_user::table) .left_join(local_user::table)
.filter(person::deleted.eq(false))
.select((person::all_columns, person_aggregates::all_columns)) .select((person::all_columns, person_aggregates::all_columns))
}; };
@ -151,3 +152,165 @@ impl PersonQuery {
queries().list(pool, ListMode::Query(self)).await 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"] } tokio = { workspace = true, features = ["full"] }
tracing.workspace = true tracing.workspace = true
async-trait = "0.1.71" async-trait = "0.1.74"
bytes = "1.4.0" bytes = "1.5.0"
enum_delegate = "0.2.0" enum_delegate = "0.2.0"
moka = { version = "0.11.2", features = ["future"] } moka = { version = "0.11.3", features = ["future"] }
openssl = "0.10.55" openssl = "0.10.57"
reqwest-middleware = "0.2.2" reqwest-middleware = "0.2.4"
reqwest-tracing = "0.4.5" reqwest-tracing = "0.4.6"
tokio-util = "0.7.8" tokio-util = "0.7.9"
tracing-subscriber = "0.3.17" tracing-subscriber = "0.3.17"

View File

@ -31,4 +31,4 @@ once_cell = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
urlencoding = { 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 actix_web::{error::ErrorBadRequest, web, Error, HttpRequest, HttpResponse, Result};
use anyhow::anyhow; use anyhow::anyhow;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lemmy_api_common::context::LemmyContext; use lemmy_api_common::{context::LemmyContext, utils::check_private_instance};
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{community::Community, person::Person}, source::{community::Community, person::Person},
traits::ApubActor, traits::ApubActor,
@ -132,6 +132,8 @@ async fn get_feed_data(
) -> Result<HttpResponse, LemmyError> { ) -> Result<HttpResponse, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool()).await?;
check_private_instance(&None, &site_view.local_site)?;
let posts = PostQuery { let posts = PostQuery {
listing_type: (Some(listing_type)), listing_type: (Some(listing_type)),
sort: (Some(sort_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 site_view = SiteView::read_local(&mut context.pool()).await?;
let person = Person::read_from_name(&mut context.pool(), user_name, false).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 { let posts = PostQuery {
listing_type: (Some(ListingType::All)), listing_type: (Some(ListingType::All)),
sort: (Some(*sort_type)), sort: (Some(*sort_type)),
@ -269,6 +273,8 @@ async fn get_feed_community(
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool()).await?;
let community = Community::read_from_name(&mut context.pool(), community_name, false).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 { let posts = PostQuery {
sort: (Some(*sort_type)), sort: (Some(*sort_type)),
community_id: (Some(community.id)), 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 site_view = SiteView::read_local(&mut context.pool()).await?;
let local_user = local_user_view_from_jwt(jwt, context).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 { let posts = PostQuery {
listing_type: (Some(ListingType::Subscribed)), listing_type: (Some(ListingType::Subscribed)),
local_user: (Some(&local_user)), local_user: (Some(&local_user)),
@ -343,6 +351,8 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> Result<ChannelBuil
let sort = CommentSortType::New; let sort = CommentSortType::New;
check_private_instance(&Some(local_user.clone()), &site_view.local_site)?;
let replies = CommentReplyQuery { let replies = CommentReplyQuery {
recipient_id: (Some(person_id)), recipient_id: (Some(person_id)),
my_person_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 serde::{Deserialize, Serialize};
use std::time::Duration; 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 cfg
.app_data(web::Data::new(client))
.service( .service(
web::resource("/pictrs/image") web::resource("/pictrs/image")
.wrap(rate_limit.image()) .wrap(rate_limit.image())
@ -90,13 +95,14 @@ async fn upload(
body: web::Payload, body: web::Payload,
// require login // require login
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: web::Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
// TODO: check rate limit here // TODO: check rate limit here
let pictrs_config = context.settings().pictrs_config()?; let pictrs_config = context.settings().pictrs_config()?;
let image_url = format!("{}image", pictrs_config.url); 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 { if let Some(addr) = req.head().peer_addr {
client_req = client_req.header("X-Forwarded-For", addr.to_string()) client_req = client_req.header("X-Forwarded-For", addr.to_string())
@ -130,6 +136,7 @@ async fn full_res(
filename: web::Path<String>, filename: web::Path<String>,
web::Query(params): web::Query<PictrsParams>, web::Query(params): web::Query<PictrsParams>,
req: HttpRequest, req: HttpRequest,
client: web::Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
@ -160,7 +167,7 @@ async fn full_res(
url url
}; };
image(url, req, context.client()).await image(url, req, &client).await
} }
async fn image( async fn image(
@ -196,6 +203,7 @@ async fn image(
async fn delete( async fn delete(
components: web::Path<(String, String)>, components: web::Path<(String, String)>,
req: HttpRequest, req: HttpRequest,
client: web::Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>, context: web::Data<LemmyContext>,
// require login // require login
_local_user_view: LocalUserView, _local_user_view: LocalUserView,
@ -205,7 +213,7 @@ async fn delete(
let pictrs_config = context.settings().pictrs_config()?; let pictrs_config = context.settings().pictrs_config()?;
let url = format!("{}image/delete/{}/{}", pictrs_config.url, &token, &file); 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 { if let Some(addr) = req.head().peer_addr {
client_req = client_req.header("X-Forwarded-For", addr.to_string()); 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", "/nodeinfo/2.0.json",
web::get().to(node_info).wrap(cache_1hour()), web::get().to(node_info).wrap(cache_1hour()),
) )
.service(web::redirect("/version", "/nodeinfo/2.0.json"))
.route( .route(
"/.well-known/nodeinfo", "/.well-known/nodeinfo",
web::get().to(node_info_well_known).wrap(cache_3days()), web::get().to(node_info_well_known).wrap(cache_3days()),

View File

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

View File

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

View File

@ -4,18 +4,20 @@ use once_cell::sync::Lazy;
use regex::{Regex, RegexBuilder}; use regex::{Regex, RegexBuilder};
use url::Url; 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> = static VALID_POST_TITLE_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r".*\S{3,200}.*").expect("compile 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(|| { 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 // taken from https://en.wikipedia.org/wiki/UTM_parameters
static CLEAN_URL_PARAMS_REGEX: Lazy<Regex> = Lazy::new(|| { 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$") Regex::new(r"^utm_source|utm_medium|utm_campaign|utm_term|utm_content|gclid|gclsrc|dclid|fbclid$")
.expect("compile regex") .expect("compile regex")
}); });
const ALLOWED_POST_URL_SCHEMES: [&str; 3] = ["http", "https", "magnet"];
const BODY_MAX_LENGTH: usize = 10000; const BODY_MAX_LENGTH: usize = 10000;
const POST_BODY_MAX_LENGTH: usize = 50000; 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<()> { pub fn is_valid_actor_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> {
let check = name.chars().count() <= actor_name_max_length static VALID_ACTOR_NAME_REGEX_EN: Lazy<Regex> =
&& VALID_ACTOR_NAME_REGEX.is_match(name) Lazy::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex"));
&& !has_newline(name); static VALID_ACTOR_NAME_REGEX_AR: Lazy<Regex> =
if !check { 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()) Err(LemmyErrorType::InvalidName.into())
} else { } else {
Ok(()) 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 // Can't do a regex here, reverse lookarounds not supported
pub fn is_valid_display_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> { pub fn is_valid_display_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> {
let check = !name.contains(FORBIDDEN_DISPLAY_CHARS) let check = !name.starts_with('@')
&& !name.starts_with('@') && !name.starts_with(FORBIDDEN_DISPLAY_CHARS)
&& name.chars().count() >= 3
&& name.chars().count() <= actor_name_max_length && name.chars().count() <= actor_name_max_length
&& !has_newline(name); && !has_newline(name)
&& has_3_permitted_display_chars(name);
if !check { if !check {
Err(LemmyErrorType::InvalidDisplayName.into()) Err(LemmyErrorType::InvalidDisplayName.into())
} else { } else {
@ -247,7 +278,7 @@ pub fn check_site_visibility_valid(
pub fn check_url_scheme(url: &Option<Url>) -> LemmyResult<()> { pub fn check_url_scheme(url: &Option<Url>) -> LemmyResult<()> {
if let Some(url) = url { if let Some(url) = url {
if url.scheme() != "http" && url.scheme() != "https" { if !ALLOWED_POST_URL_SCHEMES.contains(&url.scheme()) {
Err(LemmyErrorType::InvalidUrlScheme.into()) Err(LemmyErrorType::InvalidUrlScheme.into())
} else { } else {
Ok(()) Ok(())
@ -309,8 +340,18 @@ mod tests {
let actor_name_max_length = 20; let actor_name_max_length = 20;
assert!(is_valid_actor_name("Hello_98", actor_name_max_length).is_ok()); 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("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()); 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()); assert!(is_valid_actor_name("a", actor_name_max_length).is_err());
// empty
assert!(is_valid_actor_name("", actor_name_max_length).is_err()); assert!(is_valid_actor_name("", actor_name_max_length).is_err());
} }
@ -319,6 +360,13 @@ mod tests {
let actor_name_max_length = 20; 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_ok());
assert!(is_valid_display_name("@hello there", actor_name_max_length).is_err()); 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 // Make sure zero-space with an @ doesn't work
assert!( assert!(
@ -336,9 +384,11 @@ mod tests {
#[test] #[test]
fn test_valid_matrix_id() { fn test_valid_matrix_id() {
assert!(is_valid_matrix_id("@dess:matrix.org").is_ok()); assert!(is_valid_matrix_id("@dess:matrix.org").is_ok());
assert!(is_valid_matrix_id("@dess:matrix.org:443").is_ok());
assert!(is_valid_matrix_id("dess:matrix.org").is_err()); assert!(is_valid_matrix_id("dess:matrix.org").is_err());
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());
assert!(is_valid_matrix_id("@dess:matrix.org t").is_err());
} }
#[test] #[test]
@ -472,7 +522,11 @@ mod tests {
assert!(check_url_scheme(&None).is_ok()); 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("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("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("ftp://example.com").unwrap())).is_err());
assert!(check_url_scheme(&Some(Url::parse("javascript:void").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: lemmy-ui:
# use "image" to pull down an already compiled lemmy-ui. make sure to comment out "build". # 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. # 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". # use "build" to build your local lemmy ui image for development. make sure to comment out "image".
# run: docker compose up --build # run: docker compose up --build

View File

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

View File

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

View File

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