Merge remote-tracking branch 'upstream/main' into migration-runner

This commit is contained in:
Dull Bananas 2024-11-16 22:30:17 -07:00
commit 674a12e62a
103 changed files with 1138 additions and 847 deletions

View file

@ -138,17 +138,6 @@ steps:
- psql -c "CREATE USER lemmy WITH PASSWORD 'password' SUPERUSER;"
when: *slow_check_paths
check_db_perf_tool:
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
commands:
# same as scripts/db_perf.sh but without creating a new database server
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
when: *slow_check_paths
cargo_clippy:
image: *rust_image
environment:
@ -209,6 +198,17 @@ steps:
when:
- event: pull_request
check_db_perf_tool:
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
commands:
# same as scripts/db_perf.sh but without creating a new database server
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
when: *slow_check_paths
run_federation_tests:
image: node:22-bookworm-slim
environment:
@ -278,14 +278,14 @@ steps:
when:
- event: tag
notify_on_failure:
notify_on_build:
image: alpine:3
commands:
- apk add curl
- "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
- "curl -d'Lemmy CI build ${CI_PIPELINE_STATUS}: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
when:
- event: [pull_request, tag]
status: failure
status: [failure, success]
notify_on_tag_deploy:
image: alpine:3

View file

@ -79,6 +79,8 @@ unused_self = "deny"
unwrap_used = "deny"
unimplemented = "deny"
unused_async = "deny"
map_err_ignore = "deny"
expect_used = "deny"
[workspace.dependencies]
lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" }
@ -123,7 +125,10 @@ reqwest-tracing = "0.5.3"
clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.1"
chrono = { version = "0.4.38", features = ["serde"], default-features = false }
chrono = { version = "0.4.38", features = [
"serde",
"now",
], default-features = false }
serde_json = { version = "1.0.121", features = ["preserve_order"] }
base64 = "0.22.1"
uuid = { version = "1.10.0", features = ["serde", "v4"] }

View file

@ -22,16 +22,16 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^22.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"eslint": "^9.9.0",
"@types/node": "^22.9.0",
"@typescript-eslint/eslint-plugin": "^8.13.0",
"@typescript-eslint/parser": "^8.13.0",
"eslint": "^9.14.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.20.0-private-community.9",
"lemmy-js-client": "0.20.0-alpha.18",
"prettier": "^3.2.5",
"ts-jest": "^29.1.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.1.0"
"typescript-eslint": "^8.13.0"
}
}

View file

@ -12,38 +12,38 @@ importers:
specifier: ^29.5.12
version: 29.5.14
'@types/node':
specifier: ^22.3.0
version: 22.8.6
specifier: ^22.9.0
version: 22.9.0
'@typescript-eslint/eslint-plugin':
specifier: ^8.1.0
version: 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
specifier: ^8.13.0
version: 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/parser':
specifier: ^8.1.0
version: 8.12.2(eslint@9.13.0)(typescript@5.6.3)
specifier: ^8.13.0
version: 8.13.0(eslint@9.14.0)(typescript@5.6.3)
eslint:
specifier: ^9.9.0
version: 9.13.0
specifier: ^9.14.0
version: 9.14.0
eslint-plugin-prettier:
specifier: ^5.1.3
version: 5.2.1(eslint@9.13.0)(prettier@3.3.3)
version: 5.2.1(eslint@9.14.0)(prettier@3.3.3)
jest:
specifier: ^29.5.0
version: 29.7.0(@types/node@22.8.6)
version: 29.7.0(@types/node@22.9.0)
lemmy-js-client:
specifier: 0.20.0-private-community.9
version: 0.20.0-private-community.9
specifier: 0.20.0-alpha.18
version: 0.20.0-alpha.18
prettier:
specifier: ^3.2.5
version: 3.3.3
ts-jest:
specifier: ^29.1.0
version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.8.6))(typescript@5.6.3)
version: 29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3)
typescript:
specifier: ^5.5.4
version: 5.6.3
typescript-eslint:
specifier: ^8.1.0
version: 8.12.2(eslint@9.13.0)(typescript@5.6.3)
specifier: ^8.13.0
version: 8.13.0(eslint@9.14.0)(typescript@5.6.3)
packages:
@ -240,8 +240,8 @@ packages:
resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/js@9.13.0':
resolution: {integrity: sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==}
'@eslint/js@9.14.0':
resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.4':
@ -268,6 +268,10 @@ packages:
resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
engines: {node: '>=18.18'}
'@humanwhocodes/retry@0.4.1':
resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==}
engines: {node: '>=18.18'}
'@istanbuljs/load-nyc-config@1.1.0':
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
engines: {node: '>=8'}
@ -418,8 +422,8 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@22.8.6':
resolution: {integrity: sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==}
'@types/node@22.9.0':
resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==}
'@types/stack-utils@2.0.3':
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
@ -430,8 +434,8 @@ packages:
'@types/yargs@17.0.32':
resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==}
'@typescript-eslint/eslint-plugin@8.12.2':
resolution: {integrity: sha512-gQxbxM8mcxBwaEmWdtLCIGLfixBMHhQjBqR8sVWNTPpcj45WlYL2IObS/DNMLH1DBP0n8qz+aiiLTGfopPEebw==}
'@typescript-eslint/eslint-plugin@8.13.0':
resolution: {integrity: sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0
@ -441,8 +445,8 @@ packages:
typescript:
optional: true
'@typescript-eslint/parser@8.12.2':
resolution: {integrity: sha512-MrvlXNfGPLH3Z+r7Tk+Z5moZAc0dzdVjTgUgwsdGweH7lydysQsnSww3nAmsq8blFuRD5VRlAr9YdEFw3e6PBw==}
'@typescript-eslint/parser@8.13.0':
resolution: {integrity: sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@ -451,12 +455,12 @@ packages:
typescript:
optional: true
'@typescript-eslint/scope-manager@8.12.2':
resolution: {integrity: sha512-gPLpLtrj9aMHOvxJkSbDBmbRuYdtiEbnvO25bCMza3DhMjTQw0u7Y1M+YR5JPbMsXXnSPuCf5hfq0nEkQDL/JQ==}
'@typescript-eslint/scope-manager@8.13.0':
resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/type-utils@8.12.2':
resolution: {integrity: sha512-bwuU4TAogPI+1q/IJSKuD4shBLc/d2vGcRT588q+jzayQyjVK2X6v/fbR4InY2U2sgf8MEvVCqEWUzYzgBNcGQ==}
'@typescript-eslint/type-utils@8.13.0':
resolution: {integrity: sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
@ -464,12 +468,12 @@ packages:
typescript:
optional: true
'@typescript-eslint/types@8.12.2':
resolution: {integrity: sha512-VwDwMF1SZ7wPBUZwmMdnDJ6sIFk4K4s+ALKLP6aIQsISkPv8jhiw65sAK6SuWODN/ix+m+HgbYDkH+zLjrzvOA==}
'@typescript-eslint/types@8.13.0':
resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.12.2':
resolution: {integrity: sha512-mME5MDwGe30Pq9zKPvyduyU86PH7aixwqYR2grTglAdB+AN8xXQ1vFGpYaUSJ5o5P/5znsSBeNcs5g5/2aQwow==}
'@typescript-eslint/typescript-estree@8.13.0':
resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
@ -477,14 +481,14 @@ packages:
typescript:
optional: true
'@typescript-eslint/utils@8.12.2':
resolution: {integrity: sha512-UTTuDIX3fkfAz6iSVa5rTuSfWIYZ6ATtEocQ/umkRSyC9O919lbZ8dcH7mysshrCdrAM03skJOEYaBugxN+M6A==}
'@typescript-eslint/utils@8.13.0':
resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
'@typescript-eslint/visitor-keys@8.12.2':
resolution: {integrity: sha512-PChz8UaKQAVNHghsHcPyx1OMHoFRUEA7rJSK/mDhdq85bk+PLsUHUBqTQTFt18VJZbmxBovM65fezlheQRsSDA==}
'@typescript-eslint/visitor-keys@8.13.0':
resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
acorn-jsx@5.3.2:
@ -649,6 +653,10 @@ packages:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
cross-spawn@7.0.5:
resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==}
engines: {node: '>= 8'}
debug@4.3.7:
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
engines: {node: '>=6.0'}
@ -737,8 +745,8 @@ packages:
resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@9.13.0:
resolution: {integrity: sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==}
eslint@9.14.0:
resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
@ -1159,8 +1167,8 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
lemmy-js-client@0.20.0-private-community.9:
resolution: {integrity: sha512-iuFezswCzIco5U5Q4Eo8HAWVE65pDW2zeO+fYLEyFl30SLw9a3gqJkip2vbDfVvoAjDXyUskZKddf1Nnj8mVcg==}
lemmy-js-client@0.20.0-alpha.18:
resolution: {integrity: sha512-oZy8DboTWfUar4mPWpi7SYrOEjTBJxkvd1e6QaVwoA5UhqQV1WhxEYbzrpi/gXnEokaVQ0i5sjtL/Y2PHMO3MQ==}
leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -1533,8 +1541,8 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
typescript-eslint@8.12.2:
resolution: {integrity: sha512-UbuVUWSrHVR03q9CWx+JDHeO6B/Hr9p4U5lRH++5tq/EbFq1faYZe50ZSBePptgfIKLEti0aPQ3hFgnPVcd8ZQ==}
typescript-eslint@8.13.0:
resolution: {integrity: sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '*'
@ -1808,9 +1816,9 @@ snapshots:
'@bcoe/v8-coverage@0.2.3': {}
'@eslint-community/eslint-utils@4.4.1(eslint@9.13.0)':
'@eslint-community/eslint-utils@4.4.1(eslint@9.14.0)':
dependencies:
eslint: 9.13.0
eslint: 9.14.0
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.1': {}
@ -1839,7 +1847,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@eslint/js@9.13.0': {}
'@eslint/js@9.14.0': {}
'@eslint/object-schema@2.1.4': {}
@ -1858,6 +1866,8 @@ snapshots:
'@humanwhocodes/retry@0.3.1': {}
'@humanwhocodes/retry@0.4.1': {}
'@istanbuljs/load-nyc-config@1.1.0':
dependencies:
camelcase: 5.3.1
@ -1871,7 +1881,7 @@ snapshots:
'@jest/console@29.7.0':
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
jest-message-util: 29.7.0
jest-util: 29.7.0
@ -1884,14 +1894,14 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.9.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@22.8.6)
jest-config: 29.7.0(@types/node@22.9.0)
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@ -1916,7 +1926,7 @@ snapshots:
dependencies:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-mock: 29.7.0
'@jest/expect-utils@29.7.0':
@ -1934,7 +1944,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@sinonjs/fake-timers': 10.3.0
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-message-util: 29.7.0
jest-mock: 29.7.0
jest-util: 29.7.0
@ -1956,7 +1966,7 @@ snapshots:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.22
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
collect-v8-coverage: 1.0.2
exit: 0.1.2
@ -2026,7 +2036,7 @@ snapshots:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 22.8.6
'@types/node': 22.9.0
'@types/yargs': 17.0.32
chalk: 4.1.2
@ -2096,7 +2106,7 @@ snapshots:
'@types/graceful-fs@4.1.9':
dependencies:
'@types/node': 22.8.6
'@types/node': 22.9.0
'@types/istanbul-lib-coverage@2.0.6': {}
@ -2115,7 +2125,7 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/node@22.8.6':
'@types/node@22.9.0':
dependencies:
undici-types: 6.19.8
@ -2127,15 +2137,15 @@ snapshots:
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/eslint-plugin@8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)':
dependencies:
'@eslint-community/regexpp': 4.12.1
'@typescript-eslint/parser': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/scope-manager': 8.12.2
'@typescript-eslint/type-utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.12.2
eslint: 9.13.0
'@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/scope-manager': 8.13.0
'@typescript-eslint/type-utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.13.0
eslint: 9.14.0
graphemer: 1.4.0
ignore: 5.3.2
natural-compare: 1.4.0
@ -2145,28 +2155,28 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.12.2
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.12.2
'@typescript-eslint/scope-manager': 8.13.0
'@typescript-eslint/types': 8.13.0
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
'@typescript-eslint/visitor-keys': 8.13.0
debug: 4.3.7
eslint: 9.13.0
eslint: 9.14.0
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.12.2':
'@typescript-eslint/scope-manager@8.13.0':
dependencies:
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/visitor-keys': 8.12.2
'@typescript-eslint/types': 8.13.0
'@typescript-eslint/visitor-keys': 8.13.0
'@typescript-eslint/type-utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/type-utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
dependencies:
'@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3)
'@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
'@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
debug: 4.3.7
ts-api-utils: 1.4.0(typescript@5.6.3)
optionalDependencies:
@ -2175,12 +2185,12 @@ snapshots:
- eslint
- supports-color
'@typescript-eslint/types@8.12.2': {}
'@typescript-eslint/types@8.13.0': {}
'@typescript-eslint/typescript-estree@8.12.2(typescript@5.6.3)':
'@typescript-eslint/typescript-estree@8.13.0(typescript@5.6.3)':
dependencies:
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/visitor-keys': 8.12.2
'@typescript-eslint/types': 8.13.0
'@typescript-eslint/visitor-keys': 8.13.0
debug: 4.3.7
fast-glob: 3.3.2
is-glob: 4.0.3
@ -2192,20 +2202,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.12.2(eslint@9.13.0)(typescript@5.6.3)':
'@typescript-eslint/utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)':
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0)
'@typescript-eslint/scope-manager': 8.12.2
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/typescript-estree': 8.12.2(typescript@5.6.3)
eslint: 9.13.0
'@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0)
'@typescript-eslint/scope-manager': 8.13.0
'@typescript-eslint/types': 8.13.0
'@typescript-eslint/typescript-estree': 8.13.0(typescript@5.6.3)
eslint: 9.14.0
transitivePeerDependencies:
- supports-color
- typescript
'@typescript-eslint/visitor-keys@8.12.2':
'@typescript-eslint/visitor-keys@8.13.0':
dependencies:
'@typescript-eslint/types': 8.12.2
'@typescript-eslint/types': 8.13.0
eslint-visitor-keys: 3.4.3
acorn-jsx@5.3.2(acorn@8.14.0):
@ -2373,13 +2383,13 @@ snapshots:
convert-source-map@2.0.0: {}
create-jest@29.7.0(@types/node@22.8.6):
create-jest@29.7.0(@types/node@22.9.0):
dependencies:
'@jest/types': 29.6.3
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@22.8.6)
jest-config: 29.7.0(@types/node@22.9.0)
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@ -2394,6 +2404,12 @@ snapshots:
shebang-command: 2.0.0
which: 2.0.2
cross-spawn@7.0.5:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
debug@4.3.7:
dependencies:
ms: 2.1.3
@ -2428,9 +2444,9 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-plugin-prettier@5.2.1(eslint@9.13.0)(prettier@3.3.3):
eslint-plugin-prettier@5.2.1(eslint@9.14.0)(prettier@3.3.3):
dependencies:
eslint: 9.13.0
eslint: 9.14.0
prettier: 3.3.3
prettier-linter-helpers: 1.0.0
synckit: 0.9.1
@ -2444,23 +2460,23 @@ snapshots:
eslint-visitor-keys@4.2.0: {}
eslint@9.13.0:
eslint@9.14.0:
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.13.0)
'@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0)
'@eslint-community/regexpp': 4.12.1
'@eslint/config-array': 0.18.0
'@eslint/core': 0.7.0
'@eslint/eslintrc': 3.1.0
'@eslint/js': 9.13.0
'@eslint/js': 9.14.0
'@eslint/plugin-kit': 0.2.2
'@humanfs/node': 0.16.6
'@humanwhocodes/module-importer': 1.0.1
'@humanwhocodes/retry': 0.3.1
'@humanwhocodes/retry': 0.4.1
'@types/estree': 1.0.6
'@types/json-schema': 7.0.15
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
cross-spawn: 7.0.5
debug: 4.3.7
escape-string-regexp: 4.0.0
eslint-scope: 8.2.0
@ -2736,7 +2752,7 @@ snapshots:
'@jest/expect': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
co: 4.6.0
dedent: 1.5.1
@ -2756,16 +2772,16 @@ snapshots:
- babel-plugin-macros
- supports-color
jest-cli@29.7.0(@types/node@22.8.6):
jest-cli@29.7.0(@types/node@22.9.0):
dependencies:
'@jest/core': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@22.8.6)
create-jest: 29.7.0(@types/node@22.9.0)
exit: 0.1.2
import-local: 3.1.0
jest-config: 29.7.0(@types/node@22.8.6)
jest-config: 29.7.0(@types/node@22.9.0)
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@ -2775,7 +2791,7 @@ snapshots:
- supports-color
- ts-node
jest-config@29.7.0(@types/node@22.8.6):
jest-config@29.7.0(@types/node@22.9.0):
dependencies:
'@babel/core': 7.23.9
'@jest/test-sequencer': 29.7.0
@ -2800,7 +2816,7 @@ snapshots:
slash: 3.0.0
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.8.6
'@types/node': 22.9.0
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@ -2829,7 +2845,7 @@ snapshots:
'@jest/environment': 29.7.0
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-mock: 29.7.0
jest-util: 29.7.0
@ -2839,7 +2855,7 @@ snapshots:
dependencies:
'@jest/types': 29.6.3
'@types/graceful-fs': 4.1.9
'@types/node': 22.8.6
'@types/node': 22.9.0
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
@ -2878,7 +2894,7 @@ snapshots:
jest-mock@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-util: 29.7.0
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@ -2913,7 +2929,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.11
@ -2941,7 +2957,7 @@ snapshots:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
cjs-module-lexer: 1.2.3
collect-v8-coverage: 1.0.2
@ -2987,7 +3003,7 @@ snapshots:
jest-util@29.7.0:
dependencies:
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@ -3006,7 +3022,7 @@ snapshots:
dependencies:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 22.8.6
'@types/node': 22.9.0
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@ -3015,17 +3031,17 @@ snapshots:
jest-worker@29.7.0:
dependencies:
'@types/node': 22.8.6
'@types/node': 22.9.0
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
jest@29.7.0(@types/node@22.8.6):
jest@29.7.0(@types/node@22.9.0):
dependencies:
'@jest/core': 29.7.0
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0(@types/node@22.8.6)
jest-cli: 29.7.0(@types/node@22.9.0)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -3061,7 +3077,7 @@ snapshots:
kleur@3.0.3: {}
lemmy-js-client@0.20.0-private-community.9: {}
lemmy-js-client@0.20.0-alpha.18: {}
leven@3.1.0: {}
@ -3342,12 +3358,12 @@ snapshots:
dependencies:
typescript: 5.6.3
ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.8.6))(typescript@5.6.3):
ts-jest@29.2.5(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@22.9.0))(typescript@5.6.3):
dependencies:
bs-logger: 0.2.6
ejs: 3.1.10
fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@22.8.6)
jest: 29.7.0(@types/node@22.9.0)
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
@ -3371,11 +3387,11 @@ snapshots:
type-fest@0.21.3: {}
typescript-eslint@8.12.2(eslint@9.13.0)(typescript@5.6.3):
typescript-eslint@8.13.0(eslint@9.14.0)(typescript@5.6.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.12.2(@typescript-eslint/parser@8.12.2(eslint@9.13.0)(typescript@5.6.3))(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/parser': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.12.2(eslint@9.13.0)(typescript@5.6.3)
'@typescript-eslint/eslint-plugin': 8.13.0(@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/parser': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
'@typescript-eslint/utils': 8.13.0(eslint@9.14.0)(typescript@5.6.3)
optionalDependencies:
typescript: 5.6.3
transitivePeerDependencies:

View file

@ -9,7 +9,6 @@ import {
CreatePrivateMessageReport,
DeleteImage,
EditCommunity,
GetCommunityPendingFollowsCount,
GetCommunityPendingFollowsCountResponse,
GetReplies,
GetRepliesResponse,
@ -988,7 +987,7 @@ export function getCommentParentId(comment: Comment): number | undefined {
if (split.length > 1) {
return Number(split[split.length - 2]);
} else {
console.log(`Failed to extract comment parent id from ${comment.path}`);
console.error(`Failed to extract comment parent id from ${comment.path}`);
return undefined;
}
}
@ -1006,7 +1005,7 @@ export async function waitUntil<T>(
result = await fetcher();
if (checker(result)) return result;
} catch (error) {
//console.error(error);
console.error(error);
}
await delay(
delaySeconds[Math.min(retry - 1, delaySeconds.length - 1)] * 1000,

View file

@ -122,5 +122,5 @@
}
# Sets a response Access-Control-Allow-Origin CORS header
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
cors_origin: "*"
cors_origin: "lemmy.tld"
}

View file

@ -145,7 +145,7 @@ fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> LemmyResult<T
let sec = Secret::Raw(secret.as_bytes().to_vec());
let sec_bytes = sec
.to_bytes()
.map_err(|_| LemmyErrorType::CouldntParseTotpSecret)?;
.with_lemmy_type(LemmyErrorType::CouldntParseTotpSecret)?;
TOTP::new(
totp_rs::Algorithm::SHA1,

View file

@ -37,7 +37,7 @@ pub async fn add_admin(
// Make sure that the person_id added is local
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
.await
.map_err(|_| LemmyErrorType::ObjectNotLocal)?;
.with_lemmy_type(LemmyErrorType::ObjectNotLocal)?;
LocalUser::update(
&mut context.pool(),

View file

@ -10,7 +10,7 @@ use lemmy_db_schema::source::{
login_token::LoginToken,
password_reset_request::PasswordResetRequest,
};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn change_password_after_reset(
@ -32,9 +32,7 @@ pub async fn change_password_after_reset(
// Update the user with the new password
let password = data.password.clone();
LocalUser::update_password(&mut context.pool(), local_user_id, &password)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)?;
LocalUser::update_password(&mut context.pool(), local_user_id, &password).await?;
LoginToken::invalidate_all(&mut context.pool(), local_user_id).await?;

View file

@ -12,6 +12,7 @@ use captcha::{gen, Difficulty};
use lemmy_api_common::{
context::LemmyContext,
person::{CaptchaResponse, GetCaptchaResponse},
LemmyErrorType,
};
use lemmy_db_schema::source::{
captcha_answer::{CaptchaAnswer, CaptchaAnswerForm},
@ -37,7 +38,9 @@ pub async fn get_captcha(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
let answer = captcha.chars_as_string();
let png = captcha.as_base64().expect("failed to generate captcha");
let png = captcha
.as_base64()
.ok_or(LemmyErrorType::CouldntCreateImageCaptcha)?;
let wav = captcha_as_wav_base64(&captcha)?;

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
SuccessResponse,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn reset_password(
@ -17,7 +17,7 @@ pub async fn reset_password(
let email = data.email.to_lowercase();
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
.await
.map_err(|_| LemmyErrorType::IncorrectLogin)?;
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
check_email_verified(&local_user_view, &site_view)?;

View file

@ -143,6 +143,7 @@ pub async fn save_user_settings(
enable_animated_images: data.enable_animated_images,
enable_private_messages: data.enable_private_messages,
collapse_bot_comments: data.collapse_bot_comments,
auto_mark_fetched_posts_as_read: data.auto_mark_fetched_posts_as_read,
..Default::default()
};

View file

@ -1,34 +1,39 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::HidePost, SuccessResponse};
use lemmy_api_common::{
context::LemmyContext,
post::{HidePost, PostResponse},
};
use lemmy_db_schema::source::post::PostHide;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
use std::collections::HashSet;
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn hide_post(
data: Json<HidePost>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let post_ids = HashSet::from_iter(data.post_ids.clone());
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
) -> LemmyResult<Json<PostResponse>> {
let person_id = local_user_view.person.id;
let post_id = data.post_id;
// Mark the post as hidden / unhidden
if data.hide {
PostHide::hide(&mut context.pool(), post_ids, person_id)
PostHide::hide(&mut context.pool(), post_id, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
} else {
PostHide::unhide(&mut context.pool(), post_ids, person_id)
PostHide::unhide(&mut context.pool(), post_id, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntHidePost)?;
}
Ok(Json(SuccessResponse::default()))
let post_view = PostView::read(
&mut context.pool(),
post_id,
Some(&local_user_view.local_user),
false,
)
.await?;
Ok(Json(PostResponse { post_view }))
}

View file

@ -5,18 +5,12 @@ use lemmy_api_common::{
context::LemmyContext,
post::{CreatePostLike, PostResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_bot_account,
check_community_user_action,
check_local_vote_mode,
mark_post_as_read,
VoteItem,
},
utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
};
use lemmy_db_schema::{
source::{
local_site::LocalSite,
post::{PostLike, PostLikeForm},
post::{PostLike, PostLikeForm, PostRead, PostReadForm},
},
traits::Likeable,
};
@ -53,11 +47,7 @@ pub async fn like_post(
)
.await?;
let like_form = PostLikeForm {
post_id: data.post_id,
person_id: local_user_view.person.id,
score: data.score,
};
let like_form = PostLikeForm::new(data.post_id, local_user_view.person.id, data.score);
// Remove any likes first
let person_id = local_user_view.person.id;
@ -72,7 +62,9 @@ pub async fn like_post(
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
}
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
// Mark Post Read
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment {

View file

@ -0,0 +1,24 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::MarkManyPostsAsRead, SuccessResponse};
use lemmy_db_schema::source::post::PostRead;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
#[tracing::instrument(skip(context))]
pub async fn mark_posts_as_read(
data: Json<MarkManyPostsAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let post_ids = &data.post_ids;
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
let person_id = local_user_view.person.id;
// Mark the posts as read
PostRead::mark_many_as_read(&mut context.pool(), post_ids, person_id).await?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -1,34 +1,35 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse};
use lemmy_db_schema::source::post::PostRead;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS};
use std::collections::HashSet;
use lemmy_api_common::{
context::LemmyContext,
post::{MarkPostAsRead, PostResponse},
};
use lemmy_db_schema::source::post::{PostRead, PostReadForm};
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn mark_post_as_read(
data: Json<MarkPostAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let post_ids = HashSet::from_iter(data.post_ids.clone());
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
) -> LemmyResult<Json<PostResponse>> {
let person_id = local_user_view.person.id;
let post_id = data.post_id;
// Mark the post as read / unread
let form = PostReadForm::new(post_id, person_id);
if data.read {
PostRead::mark_as_read(&mut context.pool(), post_ids, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
PostRead::mark_as_read(&mut context.pool(), &form).await?;
} else {
PostRead::mark_as_unread(&mut context.pool(), post_ids, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
PostRead::mark_as_unread(&mut context.pool(), &form).await?;
}
let post_view = PostView::read(
&mut context.pool(),
post_id,
Some(&local_user_view.local_user),
false,
)
.await?;
Ok(Json(SuccessResponse::default()))
Ok(Json(PostResponse { post_view }))
}

View file

@ -4,5 +4,6 @@ pub mod hide;
pub mod like;
pub mod list_post_likes;
pub mod lock;
pub mod mark_many_read;
pub mod mark_read;
pub mod save;

View file

@ -2,10 +2,9 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
post::{PostResponse, SavePost},
utils::mark_post_as_read,
};
use lemmy_db_schema::{
source::post::{PostSaved, PostSavedForm},
source::post::{PostRead, PostReadForm, PostSaved, PostSavedForm},
traits::Saveable,
};
use lemmy_db_views::structs::{LocalUserView, PostView};
@ -17,10 +16,7 @@ pub async fn save_post(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let post_saved_form = PostSavedForm {
post_id: data.post_id,
person_id: local_user_view.person.id,
};
let post_saved_form = PostSavedForm::new(data.post_id, local_user_view.person.id);
if data.save {
PostSaved::save(&mut context.pool(), &post_saved_form)
@ -42,7 +38,8 @@ pub async fn save_post(
)
.await?;
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
Ok(Json(PostResponse { post_view }))
}

View file

@ -55,6 +55,7 @@ impl LemmyContext {
/// Initialize a context for use in tests which blocks federation network calls.
///
/// Do not use this in production code.
#[allow(clippy::expect_used)]
pub async fn init_test_federation_config() -> FederationConfig<LemmyContext> {
// call this to run migrations
let pool = build_db_pool_for_tests();

View file

@ -178,6 +178,9 @@ pub struct SaveUserSettings {
pub show_downvotes: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
pub show_upvote_percentage: Option<bool>,
/// Whether to automatically mark fetched posts as read.
#[cfg_attr(feature = "full", ts(optional))]
pub auto_mark_fetched_posts_as_read: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]

View file

@ -108,6 +108,9 @@ pub struct GetPosts {
/// If true, then show the nsfw posts (even if your user setting is to hide them)
#[cfg_attr(feature = "full", ts(optional))]
pub show_nsfw: Option<bool>,
/// Whether to automatically mark fetched posts as read.
#[cfg_attr(feature = "full", ts(optional))]
pub mark_as_read: Option<bool>,
#[cfg_attr(feature = "full", ts(optional))]
/// If true, then only show posts with no comments
pub no_comments_only: Option<bool>,
@ -193,17 +196,26 @@ pub struct RemovePost {
#[cfg_attr(feature = "full", ts(export))]
/// Mark a post as read.
pub struct MarkPostAsRead {
pub post_ids: Vec<PostId>,
pub post_id: PostId,
pub read: bool,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Mark several posts as read.
pub struct MarkManyPostsAsRead {
pub post_ids: Vec<PostId>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Hide a post from list views
pub struct HidePost {
pub post_ids: Vec<PostId>,
pub post_id: PostId,
pub hide: bool,
}

View file

@ -18,7 +18,7 @@ use lemmy_db_schema::{
},
};
use lemmy_utils::{
error::{LemmyError, LemmyErrorType, LemmyResult},
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
settings::structs::{PictrsImageMode, Settings},
REQWEST_TIMEOUT,
VERSION,
@ -61,7 +61,8 @@ pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResu
// server may ignore this and still respond with the full response
.header(RANGE, format!("bytes=0-{}", bytes_to_fetch - 1)) /* -1 because inclusive */
.send()
.await?;
.await?
.error_for_status()?;
let content_type: Option<Mime> = response
.headers()
@ -308,7 +309,8 @@ pub async fn purge_image_from_pictrs(image_url: &Url, context: &LemmyContext) ->
.timeout(REQWEST_TIMEOUT)
.header("x-api-token", pictrs_api_key)
.send()
.await?;
.await?
.error_for_status()?;
let response: PictrsPurgeResponse = response.json().await.map_err(LemmyError::from)?;
@ -333,8 +335,8 @@ pub async fn delete_image_from_pictrs(
.delete(&url)
.timeout(REQWEST_TIMEOUT)
.send()
.await
.map_err(LemmyError::from)?;
.await?
.error_for_status()?;
Ok(())
}
@ -366,6 +368,7 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
.timeout(REQWEST_TIMEOUT)
.send()
.await?
.error_for_status()?
.json::<PictrsResponse>()
.await?;
@ -406,16 +409,14 @@ pub async fn fetch_pictrs_proxied_image_details(
// Pictrs needs you to fetch the proxied image before you can fetch the details
let proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}");
let res = context
context
.client()
.get(&proxy_url)
.timeout(REQWEST_TIMEOUT)
.send()
.await?
.status();
if !res.is_success() {
Err(LemmyErrorType::NotAnImageType)?
}
.error_for_status()
.with_lemmy_type(LemmyErrorType::NotAnImageType)?;
let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}");
@ -425,6 +426,7 @@ pub async fn fetch_pictrs_proxied_image_details(
.timeout(REQWEST_TIMEOUT)
.send()
.await?
.error_for_status()?
.json()
.await?;
@ -521,7 +523,7 @@ mod tests {
// root relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='/image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
let metadata = extract_opengraph_data(html_bytes, &url)?;
assert_eq!(
metadata.image,
Some(Url::parse("https://example.com/image.jpg")?.into())
@ -529,7 +531,7 @@ mod tests {
// base relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
let metadata = extract_opengraph_data(html_bytes, &url)?;
assert_eq!(
metadata.image,
Some(Url::parse("https://example.com/one/image.jpg")?.into())
@ -537,7 +539,7 @@ mod tests {
// absolute url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='https://cdn.host.com/image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
let metadata = extract_opengraph_data(html_bytes, &url)?;
assert_eq!(
metadata.image,
Some(Url::parse("https://cdn.host.com/image.jpg")?.into())
@ -545,7 +547,7 @@ mod tests {
// protocol relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='//example.com/image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
let metadata = extract_opengraph_data(html_bytes, &url)?;
assert_eq!(
metadata.image,
Some(Url::parse("https://example.com/image.jpg")?.into())

View file

@ -514,6 +514,7 @@ pub struct ReadableFederationState {
next_retry: Option<DateTime<Utc>>,
}
#[allow(clippy::expect_used)]
impl From<FederationQueueState> for ReadableFederationState {
fn from(internal_state: FederationQueueState) -> Self {
ReadableFederationState {

View file

@ -28,7 +28,7 @@ use lemmy_db_schema::{
password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm},
person_block::PersonBlock,
post::{Post, PostLike, PostRead},
post::{Post, PostLike},
registration_application::RegistrationApplication,
site::Site,
},
@ -65,7 +65,7 @@ use lemmy_utils::{
use moka::future::Cache;
use regex::{escape, Regex, RegexSet};
use rosetta_i18n::{Language, LanguageId};
use std::{collections::HashSet, sync::LazyLock};
use std::sync::LazyLock;
use tracing::warn;
use url::{ParseError, Url};
use urlencoding::encode;
@ -141,19 +141,6 @@ pub fn is_top_mod(
}
}
/// Marks a post as read for a given person.
#[tracing::instrument(skip_all)]
pub async fn mark_post_as_read(
person_id: PersonId,
post_id: PostId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
PostRead::mark_as_read(pool, HashSet::from([post_id]), person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
Ok(())
}
/// Updates the read comment count for a post. Usually done when reading or creating a new comment.
#[tracing::instrument(skip_all)]
pub async fn update_read_comments(
@ -455,7 +442,11 @@ pub async fn send_password_reset_email(
// Generate a random token
let token = uuid::Uuid::new_v4().to_string();
let email = &user.local_user.email.clone().expect("email");
let email = &user
.local_user
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language(user);
let subject = &lang.password_reset_subject(&user.person.name);
let protocol_and_hostname = settings.get_protocol_and_hostname();
@ -505,6 +496,7 @@ pub fn get_interface_language_from_settings(user: &LocalUserView) -> Lang {
lang_str_to_lang(&user.local_user.interface_language)
}
#[allow(clippy::expect_used)]
fn lang_str_to_lang(lang: &str) -> Lang {
let lang_id = LanguageId::new(lang);
Lang::from_language_id(&lang_id).unwrap_or_else(|| {
@ -531,11 +523,11 @@ pub fn local_site_rate_limit_to_rate_limit_config(
})
}
pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option<Regex> {
pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option<LemmyResult<Regex>> {
build_slur_regex(local_site.slur_filter_regex.as_deref())
}
pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Regex> {
pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<LemmyResult<Regex>> {
local_site
.as_ref()
.map(local_site_to_slur_regex)
@ -570,7 +562,11 @@ pub async fn send_application_approved_email(
user: &LocalUserView,
settings: &Settings,
) -> LemmyResult<()> {
let email = &user.local_user.email.clone().expect("email");
let email = &user
.local_user
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language(user);
let subject = lang.registration_approved_subject(&user.person.actor_id);
let body = lang.registration_approved_body(&settings.hostname);
@ -592,7 +588,11 @@ pub async fn send_new_applicant_email_to_admins(
);
for admin in &admins {
let email = &admin.local_user.email.clone().expect("email");
let email = &admin
.local_user
.email
.clone()
.ok_or(LemmyErrorType::EmailRequired)?;
let lang = get_interface_language_from_settings(admin);
let subject = lang.new_application_subject(&settings.hostname, applicant_username);
let body = lang.new_application_body(applications_link);
@ -614,11 +614,13 @@ pub async fn send_new_report_email_to_admins(
let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),);
for admin in &admins {
let email = &admin.local_user.email.clone().expect("email");
let lang = get_interface_language_from_settings(admin);
let subject = lang.new_report_subject(&settings.hostname, reported_username, reporter_username);
let body = lang.new_report_body(reports_link);
send_email(&subject, email, &admin.person.name, &body, settings).await?;
if let Some(email) = &admin.local_user.email {
let lang = get_interface_language_from_settings(admin);
let subject =
lang.new_report_subject(&settings.hostname, reported_username, reporter_username);
let body = lang.new_report_body(reports_link);
send_email(&subject, email, &admin.person.name, &body, settings).await?;
}
}
Ok(())
}
@ -1043,7 +1045,7 @@ pub fn check_conflicting_like_filters(
pub async fn process_markdown(
text: &str,
slur_regex: &Option<Regex>,
slur_regex: &Option<LemmyResult<Regex>>,
url_blocklist: &RegexSet,
context: &LemmyContext,
) -> LemmyResult<String> {
@ -1075,7 +1077,7 @@ pub async fn process_markdown(
pub async fn process_markdown_opt(
text: &Option<String>,
slur_regex: &Option<Regex>,
slur_regex: &Option<LemmyResult<Regex>>,
url_blocklist: &RegexSet,
context: &LemmyContext,
) -> LemmyResult<Option<String>> {

View file

@ -16,9 +16,8 @@ use lemmy_api_common::{
},
};
use lemmy_db_schema::{
impls::actor_language::default_post_language,
impls::actor_language::validate_post_language,
source::{
actor_language::CommunityLanguage,
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm},
comment_reply::{CommentReply, CommentReplyUpdateForm},
local_site::LocalSite,
@ -93,21 +92,13 @@ pub async fn create_comment(
check_comment_depth(parent)?;
}
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => lid,
None => {
default_post_language(
&mut context.pool(),
community_id,
local_user_view.local_user.id,
)
.await?
}
};
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id)
.await?;
let language_id = validate_post_language(
&mut context.pool(),
data.language_id,
community_id,
local_user_view.local_user.id,
)
.await?;
let comment_form = CommentInsertForm {
language_id: Some(language_id),

View file

@ -1,5 +1,6 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
build_response::{build_comment_response, send_local_notifs},
comment::{CommentResponse, EditComment},
@ -13,13 +14,12 @@ use lemmy_api_common::{
},
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
source::{
actor_language::CommunityLanguage,
comment::{Comment, CommentUpdateForm},
local_site::LocalSite,
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::{
@ -55,14 +55,13 @@ pub async fn update_comment(
Err(LemmyErrorType::NoCommentEditAllowed)?
}
if let Some(language_id) = data.language_id {
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_comment.community.id,
)
.await?;
}
let language_id = validate_post_language(
&mut context.pool(),
data.language_id,
orig_comment.community.id,
local_user_view.local_user.id,
)
.await?;
let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?;
@ -74,8 +73,8 @@ pub async fn update_comment(
let comment_id = data.comment_id;
let form = CommentUpdateForm {
content,
language_id: data.language_id,
updated: Some(Some(naive_now())),
language_id: Some(language_id),
updated: Some(Some(Utc::now())),
..Default::default()
};
let updated_comment = Comment::update(&mut context.pool(), comment_id, &form)

View file

@ -1,6 +1,7 @@
use super::check_community_visibility_allowed;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
build_response::build_community_response,
community::{CommunityResponse, EditCommunity},
@ -22,7 +23,7 @@ use lemmy_db_schema::{
local_site::LocalSite,
},
traits::Crud,
utils::{diesel_string_update, diesel_url_update, naive_now},
utils::{diesel_string_update, diesel_url_update},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{
@ -95,7 +96,7 @@ pub async fn update_community(
nsfw: data.nsfw,
posting_restricted_to_mods: data.posting_restricted_to_mods,
visibility: data.visibility,
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
..Default::default()
};

View file

@ -1,10 +1,11 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{context::LemmyContext, oauth_provider::EditOAuthProvider, utils::is_admin};
use lemmy_db_schema::{
source::oauth_provider::{OAuthProvider, OAuthProviderUpdateForm},
traits::Crud,
utils::{diesel_required_string_update, diesel_required_url_update, naive_now},
utils::{diesel_required_string_update, diesel_required_url_update},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
@ -32,7 +33,7 @@ pub async fn update_oauth_provider(
auto_verify_email: data.auto_verify_email,
account_linking_enabled: data.account_linking_enabled,
enabled: data.enabled,
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
};
let update_result =

View file

@ -12,17 +12,15 @@ use lemmy_api_common::{
get_url_blocklist,
honeypot_check,
local_site_to_slur_regex,
mark_post_as_read,
process_markdown_opt,
},
};
use lemmy_db_schema::{
impls::actor_language::default_post_language,
impls::actor_language::validate_post_language,
source::{
actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite,
post::{Post, PostInsertForm, PostLike, PostLikeForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostRead, PostReadForm},
},
traits::{Crud, Likeable},
utils::diesel_url_create,
@ -98,23 +96,13 @@ pub async fn create_post(
.await?;
}
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => lid,
None => {
default_post_language(
&mut context.pool(),
community.id,
local_user_view.local_user.id,
)
.await?
}
};
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community.id)
.await?;
let language_id = validate_post_language(
&mut context.pool(),
data.language_id,
data.community_id,
local_user_view.local_user.id,
)
.await?;
let scheduled_publish_time =
convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?;
@ -154,17 +142,14 @@ pub async fn create_post(
// They like their own post by default
let person_id = local_user_view.person.id;
let post_id = inserted_post.id;
let like_form = PostLikeForm {
post_id,
person_id,
score: 1,
};
let like_form = PostLikeForm::new(post_id, person_id, 1);
PostLike::like(&mut context.pool(), &like_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
build_post_response(&context, community_id, local_user_view, post_id).await
}

View file

@ -2,10 +2,13 @@ use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{GetPost, GetPostResponse},
utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read, update_read_comments},
utils::{check_private_instance, is_mod_or_admin_opt, update_read_comments},
};
use lemmy_db_schema::{
source::{comment::Comment, post::Post},
source::{
comment::Comment,
post::{Post, PostRead, PostReadForm},
},
traits::Crud,
};
use lemmy_db_views::{
@ -62,7 +65,8 @@ pub async fn get_post(
let post_id = post_view.post.id;
if let Some(person_id) = person_id {
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let read_form = PostReadForm::new(post_id, person_id);
PostRead::mark_as_read(&mut context.pool(), &read_form).await?;
update_read_comments(
person_id,

View file

@ -1,6 +1,7 @@
use super::{convert_published_time, create::send_webmention};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
build_response::build_post_response,
context::LemmyContext,
@ -15,14 +16,14 @@ use lemmy_api_common::{
},
};
use lemmy_db_schema::{
impls::actor_language::validate_post_language,
source::{
actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite,
post::{Post, PostUpdateForm},
},
traits::Crud,
utils::{diesel_string_update, diesel_url_update, naive_now},
utils::{diesel_string_update, diesel_url_update},
};
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{
@ -101,14 +102,13 @@ pub async fn update_post(
Err(LemmyErrorType::NoPostEditAllowed)?
}
if let Some(language_id) = data.language_id {
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_post.community.id,
)
.await?;
}
let language_id = validate_post_language(
&mut context.pool(),
data.language_id,
orig_post.post.community_id,
local_user_view.local_user.id,
)
.await?;
// handle changes to scheduled_publish_time
let scheduled_publish_time = match (
@ -131,8 +131,8 @@ pub async fn update_post(
body,
alt_text,
nsfw: data.nsfw,
language_id: data.language_id,
updated: Some(Some(naive_now())),
language_id: Some(language_id),
updated: Some(Some(Utc::now())),
scheduled_publish_time,
..Default::default()
};

View file

@ -1,5 +1,6 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
private_message::{EditPrivateMessage, PrivateMessageResponse},
@ -12,7 +13,6 @@ use lemmy_db_schema::{
private_message::{PrivateMessage, PrivateMessageUpdateForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
use lemmy_utils::{
@ -47,7 +47,7 @@ pub async fn update_private_message(
private_message_id,
&PrivateMessageUpdateForm {
content: Some(content),
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
..Default::default()
},
)

View file

@ -2,6 +2,7 @@ use super::not_zero;
use crate::site::{application_question_check, site_default_post_listing_type_check};
use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
site::{CreateSite, SiteResponse},
@ -23,7 +24,7 @@ use lemmy_db_schema::{
site::{Site, SiteUpdateForm},
},
traits::Crud,
utils::{diesel_string_update, diesel_url_create, naive_now},
utils::{diesel_string_update, diesel_url_create},
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{
@ -75,7 +76,7 @@ pub async fn create_site(
icon: Some(icon),
banner: Some(banner),
actor_id: Some(actor_id),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
inbox_url,
private_key: Some(Some(keypair.private_key)),
public_key: Some(keypair.public_key),
@ -102,7 +103,7 @@ pub async fn create_site(
legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
actor_name_max_length: data.actor_name_max_length,
federation_enabled: data.federation_enabled,
@ -161,7 +162,7 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
.slur_filter_regex
.as_deref()
.or(local_site.slur_filter_regex.as_deref()),
)?;
);
site_name_length_check(&create_site.name)?;
check_slurs(&create_site.name, &slur_regex)?;

View file

@ -2,6 +2,7 @@ use super::not_zero;
use crate::site::{application_question_check, site_default_post_listing_type_check};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
request::replace_image,
@ -27,7 +28,7 @@ use lemmy_db_schema::{
site::{Site, SiteUpdateForm},
},
traits::Crud,
utils::{diesel_string_update, diesel_url_update, naive_now},
utils::{diesel_string_update, diesel_url_update},
RegistrationMode,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
@ -88,7 +89,7 @@ pub async fn update_site(
icon,
banner,
content_warning: diesel_string_update(data.content_warning.as_deref()),
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
..Default::default()
};
@ -111,7 +112,7 @@ pub async fn update_site(
legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())),
updated: Some(Some(Utc::now())),
slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
actor_name_max_length: data.actor_name_max_length,
federation_enabled: data.federation_enabled,
@ -210,7 +211,7 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm
.slur_filter_regex
.as_deref()
.or(local_site.slur_filter_regex.as_deref()),
)?;
);
if let Some(name) = &edit_site.name {
// The name doesn't need to be updated, but if provided it cannot be blanked out...

View file

@ -1,5 +1,6 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use chrono::Utc;
use lemmy_api_common::{
context::LemmyContext,
tagline::{TaglineResponse, UpdateTagline},
@ -11,7 +12,6 @@ use lemmy_db_schema::{
tagline::{Tagline, TaglineUpdateForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
@ -33,7 +33,7 @@ pub async fn update_tagline(
let tagline_form = TaglineUpdateForm {
content,
updated: naive_now(),
updated: Utc::now(),
};
let tagline = Tagline::update(&mut context.pool(), data.id, &tagline_form).await?;

View file

@ -148,14 +148,15 @@ pub async fn register(
let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?;
if local_site.site_setup && require_registration_application {
// Create the registration application
let form = RegistrationApplicationInsertForm {
local_user_id: inserted_local_user.id,
// We already made sure answer was not null above
answer: data.answer.clone().expect("must have an answer"),
};
if let Some(answer) = data.answer.clone() {
// Create the registration application
let form = RegistrationApplicationInsertForm {
local_user_id: inserted_local_user.id,
answer,
};
RegistrationApplication::create(&mut context.pool(), &form).await?;
RegistrationApplication::create(&mut context.pool(), &form).await?;
}
}
// Email the admins, only if email verification is not required
@ -304,7 +305,7 @@ pub async fn authenticate_with_oauth(
OAuthAccount::create(&mut context.pool(), &oauth_account_form)
.await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
local_user = user_view.local_user.clone();
} else {
@ -365,7 +366,7 @@ pub async fn authenticate_with_oauth(
OAuthAccount::create(&mut context.pool(), &oauth_account_form)
.await
.map_err(|_| LemmyErrorType::IncorrectLogin)?;
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?;
// prevent sign in until application is accepted
if local_site.site_setup
@ -373,17 +374,19 @@ pub async fn authenticate_with_oauth(
&& !local_user.accepted_application
&& !local_user.admin
{
// Create the registration application
RegistrationApplication::create(
&mut context.pool(),
&RegistrationApplicationInsertForm {
local_user_id: local_user.id,
answer: data.answer.clone().expect("must have an answer"),
},
)
.await?;
if let Some(answer) = data.answer.clone() {
// Create the registration application
RegistrationApplication::create(
&mut context.pool(),
&RegistrationApplicationInsertForm {
local_user_id: local_user.id,
answer,
},
)
.await?;
login_response.registration_created = true;
login_response.registration_created = true;
}
}
// Check email is verified when required
@ -483,7 +486,7 @@ async fn send_verification_email_if_required(
&local_user
.email
.clone()
.expect("invalid verification email"),
.ok_or(LemmyErrorType::EmailRequired)?,
&mut context.pool(),
context.settings(),
)
@ -524,18 +527,16 @@ async fn oauth_request_access_token(
("client_secret", &oauth_provider.client_secret),
])
.send()
.await;
let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
if !response.status().is_success() {
Err(LemmyErrorType::OauthLoginFailed)?;
}
.await
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?
.error_for_status()
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
// Extract the access token
let token_response = response
.json::<TokenResponse>()
.await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
Ok(token_response)
}
@ -552,18 +553,16 @@ async fn oidc_get_user_info(
.header("Accept", "application/json")
.bearer_auth(access_token)
.send()
.await;
let response = response.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
if !response.status().is_success() {
Err(LemmyErrorType::OauthLoginFailed)?;
}
.await
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?
.error_for_status()
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
// Extract the OAUTH user_id claim from the returned user_info
let user_info = response
.json::<serde_json::Value>()
.await
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
Ok(user_info)
}
@ -571,7 +570,7 @@ async fn oidc_get_user_info(
fn read_user_info(user_info: &serde_json::Value, key: &str) -> LemmyResult<String> {
if let Some(value) = user_info.get(key) {
let result = serde_json::from_value::<String>(value.clone())
.map_err(|_| LemmyErrorType::OauthLoginFailed)?;
.with_lemmy_type(LemmyErrorType::OauthLoginFailed)?;
return Ok(result);
}
Err(LemmyErrorType::OauthLoginFailed)?

View file

@ -130,7 +130,7 @@ impl AnnounceActivity {
actor: c.actor.clone().into_inner(),
other: serde_json::to_value(c.object)?
.as_object()
.expect("is object")
.ok_or(FederationError::Unreachable)?
.clone(),
};
let announce_compat = AnnounceActivity::new(announcable_page, community, context)?;

View file

@ -70,7 +70,8 @@ impl Report {
let object_creator = Person::read(&mut context.pool(), object_creator_id).await?;
let object_creator_site: Option<ApubSite> =
Site::read_from_instance_id(&mut context.pool(), object_creator.instance_id)
.await?
.await
.ok()
.map(Into::into);
if let Some(inbox) = object_creator_site.map(|s| s.shared_inbox_or_inbox()) {
inboxes.add_inbox(inbox);

View file

@ -17,6 +17,7 @@ use activitypub_federation::{
kinds::activity::UpdateType,
traits::{ActivityHandler, Actor, Object},
};
use chrono::Utc;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
@ -25,7 +26,6 @@ use lemmy_db_schema::{
person::Person,
},
traits::Crud,
utils::naive_now,
};
use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url;
@ -103,7 +103,7 @@ impl ActivityHandler for UpdateCommunity {
nsfw: Some(self.object.sensitive.unwrap_or(false)),
actor_id: Some(self.object.id.into()),
public_key: Some(self.object.public_key.public_key_pem),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
icon: Some(self.object.icon.map(|i| i.url.into())),
banner: Some(self.object.image.map(|i| i.url.into())),
followers_url: self.object.followers.map(Into::into),

View file

@ -118,11 +118,7 @@ impl ActivityHandler for CreateOrUpdatePage {
let post = ApubPost::from_json(self.object, context).await?;
// author likes their own post by default
let like_form = PostLikeForm {
post_id: post.id,
person_id: post.creator_id,
score: 1,
};
let like_form = PostLikeForm::new(post.id, post.creator_id, 1);
PostLike::like(&mut context.pool(), &like_form).await?;
// Calculate initial hot_rank for post

View file

@ -79,11 +79,7 @@ async fn vote_post(
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let post_id = post.id;
let like_form = PostLikeForm {
post_id: post.id,
person_id: actor.id,
score: vote_type.into(),
};
let like_form = PostLikeForm::new(post.id, actor.id, vote_type.into());
let person_id = actor.id;
PostLike::remove(&mut context.pool(), person_id, post_id).await?;
PostLike::like(&mut context.pool(), &like_form).await?;

View file

@ -10,7 +10,10 @@ use lemmy_api_common::{
post::{GetPosts, GetPostsResponse},
utils::{check_conflicting_like_filters, check_private_instance},
};
use lemmy_db_schema::source::community::Community;
use lemmy_db_schema::{
newtypes::PostId,
source::{community::Community, post::PostRead},
};
use lemmy_db_views::{
post_view::PostQuery,
structs::{LocalUserView, PaginationCursor, SiteView},
@ -90,6 +93,17 @@ pub async fn list_posts(
.await
.with_lemmy_type(LemmyErrorType::CouldntGetPosts)?;
// If in their user settings (or as part of the API request), auto-mark fetched posts as read
if let Some(local_user) = local_user {
if data
.mark_as_read
.unwrap_or(local_user.auto_mark_fetched_posts_as_read)
{
let post_ids = posts.iter().map(|p| p.post.id).collect::<Vec<PostId>>();
PostRead::mark_many_as_read(&mut context.pool(), &post_ids, local_user.person_id).await?;
}
}
// if this page wasn't empty, then there is a next page after the last post on this page
let next_page = posts.last().map(PaginationCursor::after_post);
Ok(Json(GetPostsResponse { posts, next_page }))

View file

@ -200,10 +200,7 @@ pub async fn import_settings(
&context,
|(saved, context)| async move {
let post = saved.dereference(&context).await?;
let form = PostSavedForm {
person_id,
post_id: post.id,
};
let form = PostSavedForm::new(post.id, person_id);
PostSaved::save(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
},

View file

@ -5,7 +5,7 @@ use activitypub_federation::{
};
use diesel::NotFound;
use itertools::Itertools;
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{context::LemmyContext, LemmyErrorType};
use lemmy_db_schema::traits::ApubActor;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyResult};
@ -42,7 +42,7 @@ where
let (name, domain) = identifier
.splitn(2, '@')
.collect_tuple()
.expect("invalid query");
.ok_or(LemmyErrorType::InvalidUrl)?;
let actor = DbActor::read_from_name_and_domain(&mut context.pool(), name, domain)
.await
.ok()

View file

@ -18,7 +18,7 @@ use lemmy_db_schema::{
CommunityVisibility,
};
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::{FederationError, LemmyErrorType, LemmyResult};
use lemmy_utils::error::{FederationError, LemmyErrorExt, LemmyErrorType, LemmyResult};
use serde::{Deserialize, Serialize};
use std::{ops::Deref, time::Duration};
use tokio::time::timeout;
@ -46,7 +46,7 @@ pub async fn shared_inbox(
// consider the activity broken and move on.
timeout(INCOMING_ACTIVITY_TIMEOUT, receive_fut)
.await
.map_err(|_| FederationError::InboxTimeout)?
.with_lemmy_type(FederationError::InboxTimeout.into())?
}
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
@ -109,7 +109,7 @@ pub(crate) async fn get_activity(
.into();
let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id)
.await
.map_err(|_| FederationError::CouldntFindActivity)?;
.with_lemmy_type(FederationError::CouldntFindActivity.into())?;
let sensitive = activity.sensitive;
if sensitive {

View file

@ -50,7 +50,8 @@ impl UrlVerifier for VerifyUrlData {
async fn verify(&self, url: &Url) -> Result<(), ActivityPubError> {
let local_site_data = local_site_data_cached(&mut (&self.0).into())
.await
.expect("read local site data");
.map_err(|e| ActivityPubError::Other(format!("Cant read local site data: {e}")))?;
use FederationError::*;
check_apub_id_valid(url, &local_site_data).map_err(|err| match err {
LemmyError {
@ -176,10 +177,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
.domain()
.ok_or(FederationError::UrlWithoutDomain)?
.to_string();
let local_instance = context
.settings()
.get_hostname_without_port()
.expect("local hostname is valid");
let local_instance = context.settings().get_hostname_without_port()?;
if domain == local_instance {
return Ok(());
}
@ -196,10 +194,7 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
.iter()
.map(|i| i.domain.clone())
.collect::<Vec<String>>();
let local_instance = context
.settings()
.get_hostname_without_port()
.expect("local hostname is valid");
let local_instance = context.settings().get_hostname_without_port()?;
allowed_and_local.push(local_instance);
let domain = apub_id

View file

@ -30,7 +30,6 @@ use lemmy_db_schema::{
post::Post,
},
traits::Crud,
utils::naive_now,
};
use lemmy_utils::{
error::{FederationError, LemmyError, LemmyResult},
@ -204,7 +203,7 @@ impl Object for ApubComment {
language_id,
};
let parent_comment_path = parent_comment.map(|t| t.0.path);
let timestamp: DateTime<Utc> = note.updated.or(note.published).unwrap_or_else(naive_now);
let timestamp: DateTime<Utc> = note.updated.or(note.published).unwrap_or_else(Utc::now);
let comment = Comment::insert_apub(
&mut context.pool(),
Some(timestamp),

View file

@ -38,7 +38,6 @@ use lemmy_db_schema::{
local_site::LocalSite,
},
traits::{ApubActor, Crud},
utils::naive_now,
CommunityVisibility,
};
use lemmy_db_views_actor::structs::CommunityFollowerView;
@ -166,7 +165,7 @@ impl Object for ApubCommunity {
nsfw: Some(group.sensitive.unwrap_or(false)),
actor_id: Some(group.id.into()),
local: Some(false),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
icon,
banner,
sidebar,
@ -193,11 +192,17 @@ impl Object for ApubCommunity {
let languages =
LanguageTag::to_language_id_multiple(group.language, &mut context.pool()).await?;
let timestamp = group.updated.or(group.published).unwrap_or_else(naive_now);
let community = Community::insert_apub(&mut context.pool(), timestamp, &form).await?;
let timestamp = group.updated.or(group.published).unwrap_or_else(Utc::now);
let community: ApubCommunity = Community::insert_apub(&mut context.pool(), timestamp, &form)
.await?
.into();
CommunityLanguage::update(&mut context.pool(), languages, community.id).await?;
let community: ApubCommunity = community.into();
// Need to fetch mods synchronously, otherwise fetching a post in community with
// `posting_restricted_to_mods` can fail if mods havent been fetched yet.
if let Some(moderators) = group.attributed_to {
moderators.dereference(&community, context).await.ok();
}
// These collections are not necessary for Lemmy to work, so ignore errors.
let community_ = community.clone();
@ -210,9 +215,6 @@ impl Object for ApubCommunity {
if let Some(featured) = group.featured {
featured.dereference(&community_, &context_).await.ok();
}
if let Some(moderators) = group.attributed_to {
moderators.dereference(&community_, &context_).await.ok();
}
Ok(())
});

View file

@ -39,7 +39,6 @@ use lemmy_db_schema::{
site::{Site, SiteInsertForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_utils::{
error::{FederationError, LemmyError, LemmyResult},
@ -163,7 +162,7 @@ impl Object for ApubSite {
banner,
description: apub.summary,
actor_id: Some(apub.id.clone().into()),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
inbox_url: Some(apub.inbox.clone().into()),
public_key: Some(apub.public_key.public_key_pem.clone()),
private_key: None,

View file

@ -35,7 +35,6 @@ use lemmy_db_schema::{
person::{Person as DbPerson, PersonInsertForm, PersonUpdateForm},
},
traits::{ApubActor, Crud},
utils::naive_now,
};
use lemmy_utils::{
error::{LemmyError, LemmyResult},
@ -176,7 +175,7 @@ impl Object for ApubPerson {
bot_account: Some(person.kind == UserTypes::Service),
private_key: None,
public_key: person.public_key.public_key_pem,
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
inbox_url: Some(
person
.endpoints

View file

@ -35,7 +35,6 @@ use lemmy_db_schema::{
post::{Post, PostInsertForm, PostUpdateForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views_actor::structs::CommunityModeratorView;
use lemmy_utils::{
@ -260,7 +259,7 @@ impl Object for ApubPost {
..PostInsertForm::new(name, creator.id, community.id)
};
let timestamp = page.updated.or(page.published).unwrap_or_else(naive_now);
let timestamp = page.updated.or(page.published).unwrap_or_else(Utc::now);
let post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?;
let post_ = post.clone();
let context_ = context.reset_request_count();

View file

@ -31,7 +31,6 @@ use lemmy_db_schema::{
private_message::{PrivateMessage, PrivateMessageInsertForm},
},
traits::Crud,
utils::naive_now,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{
@ -161,7 +160,7 @@ impl Object for ApubPrivateMessage {
ap_id: Some(note.id.into()),
local: Some(false),
};
let timestamp = note.updated.or(note.published).unwrap_or_else(naive_now);
let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now);
let pm = PrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?;
Ok(pm.into())
}

View file

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

View file

@ -65,11 +65,7 @@ mod tests {
);
let inserted_post = Post::create(pool, &new_post).await?;
let post_like = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1);
let _inserted_post_like = PostLike::like(pool, &post_like).await?;
let comment_form = CommentInsertForm::new(

View file

@ -113,11 +113,7 @@ mod tests {
let inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
let post_like = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let post_like = PostLikeForm::new(inserted_post.id, inserted_person.id, 1);
PostLike::like(pool, &post_like).await?;
@ -129,11 +125,7 @@ mod tests {
assert_eq!(0, post_aggs_before_delete.downvotes);
// Add a post dislike from the other person
let post_dislike = PostLikeForm {
post_id: inserted_post.id,
person_id: another_inserted_person.id,
score: -1,
};
let post_dislike = PostLikeForm::new(inserted_post.id, another_inserted_person.id, -1);
PostLike::like(pool, &post_dislike).await?;

View file

@ -197,7 +197,7 @@ impl SiteLanguage {
impl CommunityLanguage {
/// Returns true if the given language is one of configured languages for given community
pub async fn is_allowed_community_language(
async fn is_allowed_community_language(
pool: &mut DbPool<'_>,
for_language_id: LanguageId,
for_community_id: CommunityId,
@ -319,29 +319,38 @@ impl CommunityLanguage {
}
}
pub async fn default_post_language(
pub async fn validate_post_language(
pool: &mut DbPool<'_>,
language_id: Option<LanguageId>,
community_id: CommunityId,
local_user_id: LocalUserId,
) -> Result<LanguageId, Error> {
) -> LemmyResult<LanguageId> {
use crate::schema::{community_language::dsl as cl, local_user_language::dsl as ul};
let conn = &mut get_conn(pool).await?;
let mut intersection = ul::local_user_language
.inner_join(cl::community_language.on(ul::language_id.eq(cl::language_id)))
.filter(ul::local_user_id.eq(local_user_id))
.filter(cl::community_id.eq(community_id))
.select(cl::language_id)
.get_results::<LanguageId>(conn)
.await?;
let language_id = match language_id {
None | Some(LanguageId(0)) => {
let mut intersection = ul::local_user_language
.inner_join(cl::community_language.on(ul::language_id.eq(cl::language_id)))
.filter(ul::local_user_id.eq(local_user_id))
.filter(cl::community_id.eq(community_id))
.select(cl::language_id)
.get_results::<LanguageId>(conn)
.await?;
if intersection.len() == 1 {
Ok(intersection.pop().unwrap_or(UNDETERMINED_ID))
} else if intersection.len() == 2 && intersection.contains(&UNDETERMINED_ID) {
intersection.retain(|i| i != &UNDETERMINED_ID);
Ok(intersection.pop().unwrap_or(UNDETERMINED_ID))
} else {
Ok(UNDETERMINED_ID)
}
if intersection.len() == 1 {
intersection.pop().unwrap_or(UNDETERMINED_ID)
} else if intersection.len() == 2 && intersection.contains(&UNDETERMINED_ID) {
intersection.retain(|i| i != &UNDETERMINED_ID);
intersection.pop().unwrap_or(UNDETERMINED_ID)
} else {
UNDETERMINED_ID
}
}
Some(lid) => lid,
};
CommunityLanguage::is_allowed_community_language(pool, language_id, community_id).await?;
Ok(language_id)
}
/// If no language is given, set all languages
@ -363,6 +372,7 @@ async fn convert_update_languages(
}
/// If all languages are returned, return empty vec instead
#[allow(clippy::expect_used)]
async fn convert_read_languages(
conn: &mut AsyncPgConnection,
language_ids: Vec<LanguageId>,
@ -501,7 +511,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_user_languages() -> Result<(), Error> {
async fn test_user_languages() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
@ -534,7 +544,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_community_languages() -> Result<(), Error> {
async fn test_community_languages() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
@ -590,7 +600,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_default_post_language() -> Result<(), Error> {
async fn test_validate_post_language() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await?;
@ -613,8 +623,11 @@ mod tests {
LocalUserLanguage::update(pool, test_langs2, local_user.id).await?;
// no overlap in user/community languages, so defaults to undetermined
let def1 = default_post_language(pool, community.id, local_user.id).await?;
assert_eq!(UNDETERMINED_ID, def1);
let def1 = validate_post_language(pool, None, community.id, local_user.id).await;
assert_eq!(
Some(LemmyErrorType::LanguageNotAllowed),
def1.err().map(|e| e.error_type)
);
let ru = Language::read_id_from_code(pool, "ru").await?;
let test_langs3 = vec![
@ -626,7 +639,7 @@ mod tests {
LocalUserLanguage::update(pool, test_langs3, local_user.id).await?;
// this time, both have ru as common lang
let def2 = default_post_language(pool, community.id, local_user.id).await?;
let def2 = validate_post_language(pool, None, community.id, local_user.id).await?;
assert_eq!(ru, def2);
Person::delete(pool, person.id).await?;

View file

@ -57,11 +57,12 @@ mod tests {
source::captcha_answer::{CaptchaAnswer, CaptchaAnswerForm, CheckCaptchaAnswer},
utils::build_db_pool_for_tests,
};
use lemmy_utils::error::LemmyResult;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_captcha_happy_path() {
async fn test_captcha_happy_path() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
@ -71,8 +72,7 @@ mod tests {
answer: "XYZ".to_string(),
},
)
.await
.expect("should not fail to insert captcha");
.await?;
let result = CaptchaAnswer::check_captcha(
pool,
@ -84,11 +84,12 @@ mod tests {
.await;
assert!(result.is_ok());
Ok(())
}
#[tokio::test]
#[serial]
async fn test_captcha_repeat_answer_fails() {
async fn test_captcha_repeat_answer_fails() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
@ -98,8 +99,7 @@ mod tests {
answer: "XYZ".to_string(),
},
)
.await
.expect("should not fail to insert captcha");
.await?;
let _result = CaptchaAnswer::check_captcha(
pool,
@ -120,5 +120,7 @@ mod tests {
.await;
assert!(result_repeat.is_err());
Ok(())
}
}

View file

@ -12,15 +12,7 @@ use crate::{
CommentUpdateForm,
},
traits::{Crud, Likeable, Saveable},
utils::{
functions::coalesce,
get_conn,
naive_now,
now,
uplete,
DbPool,
DELETED_REPLACEMENT_TEXT,
},
utils::{functions::coalesce, get_conn, now, uplete, DbPool, DELETED_REPLACEMENT_TEXT},
};
use chrono::{DateTime, Utc};
use diesel::{
@ -46,7 +38,7 @@ impl Comment {
.set((
comment::content.eq(DELETED_REPLACEMENT_TEXT),
comment::deleted.eq(true),
comment::updated.eq(naive_now()),
comment::updated.eq(Utc::now()),
))
.get_results::<Self>(conn)
.await
@ -61,7 +53,7 @@ impl Comment {
diesel::update(comment::table.filter(comment::creator_id.eq(for_creator_id)))
.set((
comment::removed.eq(removed),
comment::updated.eq(naive_now()),
comment::updated.eq(Utc::now()),
))
.get_results::<Self>(conn)
.await

View file

@ -6,8 +6,9 @@ use crate::{
},
source::comment_report::{CommentReport, CommentReportForm},
traits::Reportable,
utils::{get_conn, naive_now, DbPool},
utils::{get_conn, DbPool},
};
use chrono::Utc;
use diesel::{
dsl::{insert_into, update},
result::Error,
@ -51,7 +52,7 @@ impl Reportable for CommentReport {
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
updated.eq(Utc::now()),
))
.execute(conn)
.await
@ -67,7 +68,7 @@ impl Reportable for CommentReport {
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
updated.eq(Utc::now()),
))
.execute(conn)
.await
@ -88,7 +89,7 @@ impl Reportable for CommentReport {
.set((
resolved.eq(false),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
updated.eq(Utc::now()),
))
.execute(conn)
.await

View file

@ -16,11 +16,11 @@ use crate::{
utils::{
functions::{coalesce, lower},
get_conn,
naive_now,
now,
DbPool,
},
};
use chrono::Utc;
use diesel::{
dsl::{count_star, insert_into},
result::Error,
@ -52,7 +52,7 @@ impl Instance {
None => {
// Instance not in database yet, insert it
let form = InstanceForm {
updated: Some(naive_now()),
updated: Some(Utc::now()),
..InstanceForm::new(domain_)
};
insert_into(instance::table)

View file

@ -26,19 +26,19 @@ use diesel::{
QueryDsl,
};
use diesel_async::RunQueryDsl;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
impl LocalUser {
pub async fn create(
pool: &mut DbPool<'_>,
form: &LocalUserInsertForm,
languages: Vec<LanguageId>,
) -> Result<LocalUser, Error> {
) -> LemmyResult<LocalUser> {
let conn = &mut get_conn(pool).await?;
let mut form_with_encrypted_password = form.clone();
if let Some(password_encrypted) = &form.password_encrypted {
let password_hash = hash(password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
let password_hash = hash(password_encrypted, DEFAULT_COST)?;
form_with_encrypted_password.password_encrypted = Some(password_hash);
}
@ -84,14 +84,15 @@ impl LocalUser {
pool: &mut DbPool<'_>,
local_user_id: LocalUserId,
new_password: &str,
) -> Result<Self, Error> {
) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
let password_hash = hash(new_password, DEFAULT_COST).expect("Couldn't hash password");
let password_hash = hash(new_password, DEFAULT_COST)?;
diesel::update(local_user::table.find(local_user_id))
.set((local_user::password_encrypted.eq(password_hash),))
.get_result::<Self>(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdateUser)
}
pub async fn set_all_users_email_verified(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {

View file

@ -10,8 +10,9 @@ use crate::{
PersonUpdateForm,
},
traits::{ApubActor, Crud, Followable},
utils::{action_query, functions::lower, get_conn, naive_now, now, uplete, DbPool},
utils::{action_query, functions::lower, get_conn, now, uplete, DbPool},
};
use chrono::Utc;
use diesel::{
dsl::{insert_into, not},
expression::SelectableHelper,
@ -93,7 +94,7 @@ impl Person {
person::bio.eq::<Option<String>>(None),
person::matrix_user_id.eq::<Option<String>>(None),
person::deleted.eq(true),
person::updated.eq(naive_now()),
person::updated.eq(Utc::now()),
))
.get_result::<Self>(conn)
.await

View file

@ -1,5 +1,5 @@
use crate::{
diesel::{BoolExpressionMethods, OptionalExtension},
diesel::{BoolExpressionMethods, NullableExpressionMethods, OptionalExtension},
newtypes::{CommunityId, DbUrl, PersonId, PostId},
schema::{community, person, post, post_actions},
source::post::{
@ -19,7 +19,6 @@ use crate::{
utils::{
functions::coalesce,
get_conn,
naive_now,
now,
uplete,
DbPool,
@ -37,12 +36,11 @@ use diesel::{
result::Error,
DecoratableTarget,
ExpressionMethods,
NullableExpressionMethods,
QueryDsl,
TextExpressionMethods,
};
use diesel_async::RunQueryDsl;
use std::collections::HashSet;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[async_trait]
impl Crud for Post {
@ -117,9 +115,7 @@ impl Post {
.filter(post::local.eq(true))
.filter(post::deleted.eq(false))
.filter(post::removed.eq(false))
.filter(
post::published.ge(Utc::now().naive_utc() - SITEMAP_DAYS.expect("TimeDelta out of bounds")),
)
.filter(post::published.ge(Utc::now().naive_utc() - SITEMAP_DAYS))
.order(post::published.desc())
.limit(SITEMAP_LIMIT)
.load::<(DbUrl, chrono::DateTime<Utc>)>(conn)
@ -138,7 +134,7 @@ impl Post {
post::url.eq(Option::<&str>::None),
post::body.eq(DELETED_REPLACEMENT_TEXT),
post::deleted.eq(true),
post::updated.eq(naive_now()),
post::updated.eq(Utc::now()),
))
.get_results::<Self>(conn)
.await
@ -160,7 +156,7 @@ impl Post {
}
update
.set((post::removed.eq(removed), post::updated.eq(naive_now())))
.set((post::removed.eq(removed), post::updated.eq(Utc::now())))
.get_results::<Self>(conn)
.await
}
@ -281,7 +277,6 @@ impl Likeable for PostLike {
type IdType = PostId;
async fn like(pool: &mut DbPool<'_>, post_like_form: &PostLikeForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let post_like_form = (post_like_form, post_actions::liked.eq(now().nullable()));
insert_into(post_actions::table)
.values(post_like_form)
.on_conflict((post_actions::post_id, post_actions::person_id))
@ -310,7 +305,6 @@ impl Saveable for PostSaved {
type Form = PostSavedForm;
async fn save(pool: &mut DbPool<'_>, post_saved_form: &PostSavedForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let post_saved_form = (post_saved_form, post_actions::saved.eq(now().nullable()));
insert_into(post_actions::table)
.values(post_saved_form)
.on_conflict((post_actions::post_id, post_actions::person_id))
@ -335,20 +329,39 @@ impl Saveable for PostSaved {
impl PostRead {
pub async fn mark_as_read(
pool: &mut DbPool<'_>,
post_ids: HashSet<PostId>,
post_read_form: &PostReadForm,
) -> LemmyResult<usize> {
Self::mark_many_as_read(pool, &[post_read_form.post_id], post_read_form.person_id).await
}
pub async fn mark_as_unread(
pool: &mut DbPool<'_>,
post_read_form: &PostReadForm,
) -> Result<uplete::Count, Error> {
let conn = &mut get_conn(pool).await?;
uplete::new(
post_actions::table
.filter(post_actions::post_id.eq(post_read_form.post_id))
.filter(post_actions::person_id.eq(post_read_form.person_id)),
)
.set_null(post_actions::read)
.get_result(conn)
.await
}
pub async fn mark_many_as_read(
pool: &mut DbPool<'_>,
post_ids: &[PostId],
person_id: PersonId,
) -> Result<usize, Error> {
) -> LemmyResult<usize> {
let conn = &mut get_conn(pool).await?;
let forms = post_ids
.into_iter()
.map(|post_id| {
(
PostReadForm { post_id, person_id },
post_actions::read.eq(now().nullable()),
)
})
.iter()
.map(|post_id| (PostReadForm::new(*post_id, person_id)))
.collect::<Vec<_>>();
insert_into(post_actions::table)
.values(forms)
.on_conflict((post_actions::person_id, post_actions::post_id))
@ -356,62 +369,38 @@ impl PostRead {
.set(post_actions::read.eq(now().nullable()))
.execute(conn)
.await
}
pub async fn mark_as_unread(
pool: &mut DbPool<'_>,
post_id_: HashSet<PostId>,
person_id_: PersonId,
) -> Result<uplete::Count, Error> {
let conn = &mut get_conn(pool).await?;
uplete::new(
post_actions::table
.filter(post_actions::post_id.eq_any(post_id_))
.filter(post_actions::person_id.eq(person_id_)),
)
.set_null(post_actions::read)
.get_result(conn)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)
}
}
impl PostHide {
pub async fn hide(
pool: &mut DbPool<'_>,
post_ids: HashSet<PostId>,
post_id: PostId,
person_id: PersonId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
let forms = post_ids
.into_iter()
.map(|post_id| {
(
PostHideForm { post_id, person_id },
post_actions::hidden.eq(now().nullable()),
)
})
.collect::<Vec<_>>();
let form = &PostHideForm::new(post_id, person_id);
insert_into(post_actions::table)
.values(forms)
.values(form)
.on_conflict((post_actions::person_id, post_actions::post_id))
.do_update()
.set(post_actions::hidden.eq(now().nullable()))
.set(form)
.execute(conn)
.await
}
pub async fn unhide(
pool: &mut DbPool<'_>,
post_id_: HashSet<PostId>,
post_id_: PostId,
person_id_: PersonId,
) -> Result<uplete::Count, Error> {
let conn = &mut get_conn(pool).await?;
uplete::new(
post_actions::table
.filter(post_actions::post_id.eq_any(post_id_))
.filter(post_actions::post_id.eq(post_id_))
.filter(post_actions::person_id.eq(person_id_)),
)
.set_null(post_actions::hidden)
@ -434,6 +423,7 @@ mod tests {
PostLike,
PostLikeForm,
PostRead,
PostReadForm,
PostSaved,
PostSavedForm,
PostUpdateForm,
@ -446,7 +436,6 @@ mod tests {
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
use std::collections::HashSet;
use url::Url;
#[tokio::test]
@ -518,11 +507,7 @@ mod tests {
};
// Post Like
let post_like_form = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let post_like_form = PostLikeForm::new(inserted_post.id, inserted_person.id, 1);
let inserted_post_like = PostLike::like(pool, &post_like_form).await?;
@ -534,10 +519,7 @@ mod tests {
};
// Post Save
let post_saved_form = PostSavedForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
};
let post_saved_form = PostSavedForm::new(inserted_post.id, inserted_person.id);
let inserted_post_saved = PostSaved::save(pool, &post_saved_form).await?;
@ -547,14 +529,11 @@ mod tests {
published: inserted_post_saved.published,
};
// Post Read
let marked_as_read = PostRead::mark_as_read(
pool,
HashSet::from([inserted_post.id, inserted_post2.id]),
inserted_person.id,
)
.await?;
assert_eq!(2, marked_as_read);
// Mark 2 posts as read
let post_read_form_1 = PostReadForm::new(inserted_post.id, inserted_person.id);
PostRead::mark_as_read(pool, &post_read_form_1).await?;
let post_read_form_2 = PostReadForm::new(inserted_post2.id, inserted_person.id);
PostRead::mark_as_read(pool, &post_read_form_2).await?;
let read_post = Post::read(pool, inserted_post.id).await?;
@ -572,17 +551,19 @@ mod tests {
assert_eq!(uplete::Count::only_updated(1), like_removed);
let saved_removed = PostSaved::unsave(pool, &post_saved_form).await?;
assert_eq!(uplete::Count::only_updated(1), saved_removed);
let read_removed = PostRead::mark_as_unread(
pool,
HashSet::from([inserted_post.id, inserted_post2.id]),
inserted_person.id,
)
.await?;
assert_eq!(uplete::Count::only_deleted(2), read_removed);
let read_remove_form_1 = PostReadForm::new(inserted_post.id, inserted_person.id);
let read_removed_1 = PostRead::mark_as_unread(pool, &read_remove_form_1).await?;
assert_eq!(uplete::Count::only_deleted(1), read_removed_1);
let read_remove_form_2 = PostReadForm::new(inserted_post2.id, inserted_person.id);
let read_removed_2 = PostRead::mark_as_unread(pool, &read_remove_form_2).await?;
assert_eq!(uplete::Count::only_deleted(1), read_removed_2);
let num_deleted = Post::delete(pool, inserted_post.id).await?
+ Post::delete(pool, inserted_post2.id).await?
+ Post::delete(pool, inserted_scheduled_post.id).await?;
assert_eq!(3, num_deleted);
Community::delete(pool, inserted_community.id).await?;
Person::delete(pool, inserted_person.id).await?;

View file

@ -6,8 +6,9 @@ use crate::{
},
source::post_report::{PostReport, PostReportForm},
traits::Reportable,
utils::{get_conn, naive_now, DbPool},
utils::{get_conn, DbPool},
};
use chrono::Utc;
use diesel::{
dsl::{insert_into, update},
result::Error,
@ -40,7 +41,7 @@ impl Reportable for PostReport {
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
updated.eq(Utc::now()),
))
.execute(conn)
.await
@ -56,7 +57,7 @@ impl Reportable for PostReport {
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
updated.eq(Utc::now()),
))
.execute(conn)
.await
@ -72,7 +73,7 @@ impl Reportable for PostReport {
.set((
resolved.eq(false),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
updated.eq(Utc::now()),
))
.execute(conn)
.await

View file

@ -3,8 +3,9 @@ use crate::{
schema::private_message_report::dsl::{private_message_report, resolved, resolver_id, updated},
source::private_message_report::{PrivateMessageReport, PrivateMessageReportForm},
traits::Reportable,
utils::{get_conn, naive_now, DbPool},
utils::{get_conn, DbPool},
};
use chrono::Utc;
use diesel::{
dsl::{insert_into, update},
result::Error,
@ -40,7 +41,7 @@ impl Reportable for PrivateMessageReport {
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
updated.eq(Utc::now()),
))
.execute(conn)
.await
@ -65,7 +66,7 @@ impl Reportable for PrivateMessageReport {
.set((
resolved.eq(false),
resolver_id.eq(by_resolver_id),
updated.eq(naive_now()),
updated.eq(Utc::now()),
))
.execute(conn)
.await

View file

@ -10,7 +10,7 @@ use crate::{
};
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use url::Url;
#[async_trait]
@ -65,13 +65,13 @@ impl Site {
pub async fn read_from_instance_id(
pool: &mut DbPool<'_>,
_instance_id: InstanceId,
) -> Result<Option<Self>, Error> {
) -> LemmyResult<Self> {
let conn = &mut get_conn(pool).await?;
site::table
.filter(site::instance_id.eq(_instance_id))
.first(conn)
.await
.optional()
.with_lemmy_type(LemmyErrorType::NotFound)
}
pub async fn read_from_apub_id(
pool: &mut DbPool<'_>,

View file

@ -455,6 +455,7 @@ diesel::table! {
enable_private_messages -> Bool,
collapse_bot_comments -> Bool,
default_comment_sort_type -> CommentSortTypeEnum,
auto_mark_fetched_posts_as_read -> Bool,
}
}

View file

@ -84,7 +84,7 @@ pub struct CommentInsertForm {
pub struct CommentUpdateForm {
pub content: Option<String>,
pub removed: Option<bool>,
// Don't use a default naive_now here, because the create function does a lot of comment updates
// Don't use a default Utc::now here, because the create function does a lot of comment updates
pub updated: Option<Option<DateTime<Utc>>>,
pub deleted: Option<bool>,
pub ap_id: Option<DbUrl>,

View file

@ -68,6 +68,8 @@ pub struct LocalUser {
/// Whether to auto-collapse bot comments.
pub collapse_bot_comments: bool,
pub default_comment_sort_type: CommentSortType,
/// Whether to automatically mark fetched posts as read.
pub auto_mark_fetched_posts_as_read: bool,
}
#[derive(Clone, derive_new::new)]
@ -124,6 +126,8 @@ pub struct LocalUserInsertForm {
pub collapse_bot_comments: Option<bool>,
#[new(default)]
pub default_comment_sort_type: Option<CommentSortType>,
#[new(default)]
pub auto_mark_fetched_posts_as_read: Option<bool>,
}
#[derive(Clone, Default)]
@ -155,4 +159,5 @@ pub struct LocalUserUpdateForm {
pub enable_private_messages: Option<bool>,
pub collapse_bot_comments: Option<bool>,
pub default_comment_sort_type: Option<CommentSortType>,
pub auto_mark_fetched_posts_as_read: Option<bool>,
}

View file

@ -47,6 +47,7 @@ pub mod tagline;
/// This is necessary so they can be successfully deserialized from API responses, even though the
/// value is not sent by Lemmy. Necessary for crates which rely on Rust API such as
/// lemmy-stats-crawler.
#[allow(clippy::expect_used)]
fn placeholder_apub_url() -> DbUrl {
DbUrl(Box::new(
Url::parse("http://example.com").expect("parse placeholder url"),

View file

@ -165,7 +165,7 @@ pub struct PostLike {
pub published: DateTime<Utc>,
}
#[derive(Clone)]
#[derive(Clone, derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = post_actions))]
pub struct PostLikeForm {
@ -173,6 +173,8 @@ pub struct PostLikeForm {
pub person_id: PersonId,
#[cfg_attr(feature = "full", diesel(column_name = like_score))]
pub score: i16,
#[new(value = "Utc::now()")]
pub liked: DateTime<Utc>,
}
#[derive(PartialEq, Eq, Debug)]
@ -192,11 +194,14 @@ pub struct PostSaved {
pub published: DateTime<Utc>,
}
#[derive(derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = post_actions))]
pub struct PostSavedForm {
pub post_id: PostId,
pub person_id: PersonId,
#[new(value = "Utc::now()")]
pub saved: DateTime<Utc>,
}
#[derive(PartialEq, Eq, Debug)]
@ -216,11 +221,14 @@ pub struct PostRead {
pub published: DateTime<Utc>,
}
#[derive(derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = post_actions))]
pub(crate) struct PostReadForm {
pub struct PostReadForm {
pub post_id: PostId,
pub person_id: PersonId,
#[new(value = "Utc::now()")]
pub read: DateTime<Utc>,
}
#[derive(PartialEq, Eq, Debug)]
@ -240,9 +248,12 @@ pub struct PostHide {
pub published: DateTime<Utc>,
}
#[derive(derive_new::new)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = post_actions))]
pub(crate) struct PostHideForm {
pub struct PostHideForm {
pub post_id: PostId,
pub person_id: PersonId,
#[new(value = "Utc::now()")]
pub hidden: DateTime<Utc>,
}

View file

@ -1,7 +1,7 @@
pub mod uplete;
use crate::{newtypes::DbUrl, schema_setup, CommentSortType, PostSortType};
use chrono::{DateTime, TimeDelta, Utc};
use chrono::TimeDelta;
use deadpool::Runtime;
use diesel::{
dsl,
@ -68,7 +68,7 @@ use url::Url;
const FETCH_LIMIT_DEFAULT: i64 = 10;
pub const FETCH_LIMIT_MAX: i64 = 50;
pub const SITEMAP_LIMIT: i64 = 50000;
pub const SITEMAP_DAYS: Option<TimeDelta> = TimeDelta::try_days(31);
pub const SITEMAP_DAYS: TimeDelta = TimeDelta::days(31);
pub const RANK_DEFAULT: f64 = 0.0001;
/// Some connection options to speed up queries
@ -360,8 +360,8 @@ pub fn diesel_url_create(opt: Option<&str>) -> LemmyResult<Option<DbUrl>> {
}
/// Sets a few additional config options necessary for starting lemmy
fn build_config_options_uri_segment(config: &str) -> String {
let mut url = Url::parse(config).expect("Couldn't parse postgres connection URI");
fn build_config_options_uri_segment(config: &str) -> LemmyResult<String> {
let mut url = Url::parse(config)?;
// Set `lemmy.protocol_and_hostname` so triggers can use it
let lemmy_protocol_and_hostname_option =
@ -377,7 +377,7 @@ fn build_config_options_uri_segment(config: &str) -> String {
.join(" ");
url.set_query(Some(&format!("options={options_segments}")));
url.into()
Ok(url.into())
}
fn establish_connection(config: &str) -> BoxFuture<ConnectionResult<AsyncPgConnection>> {
@ -385,8 +385,11 @@ fn establish_connection(config: &str) -> BoxFuture<ConnectionResult<AsyncPgConne
/// Use a once_lock to create the postgres connection config, since this config never changes
static POSTGRES_CONFIG_WITH_OPTIONS: OnceLock<String> = OnceLock::new();
let config =
POSTGRES_CONFIG_WITH_OPTIONS.get_or_init(|| build_config_options_uri_segment(config));
let config = POSTGRES_CONFIG_WITH_OPTIONS.get_or_init(|| {
build_config_options_uri_segment(config)
.inspect_err(|e| error!("Couldn't parse postgres connection URI: {e}"))
.unwrap_or_default()
});
// We only support TLS with sslmode=require currently
let conn = if config.contains("sslmode=require") {
@ -495,14 +498,11 @@ pub fn build_db_pool() -> LemmyResult<ActualDbPool> {
Ok(pool)
}
#[allow(clippy::expect_used)]
pub fn build_db_pool_for_tests() -> ActualDbPool {
build_db_pool().expect("db pool missing")
}
pub fn naive_now() -> DateTime<Utc> {
Utc::now()
}
pub fn post_to_comment_sort_type(sort: PostSortType) -> CommentSortType {
use PostSortType::*;
match sort {
@ -515,6 +515,7 @@ pub fn post_to_comment_sort_type(sort: PostSortType) -> CommentSortType {
}
}
#[allow(clippy::expect_used)]
static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
.expect("compile email regex")

View file

@ -20,7 +20,7 @@ use lemmy_db_schema::{
ReadFn,
},
};
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult};
use std::future::{ready, Ready};
enum ReadBy<'a> {
@ -146,7 +146,7 @@ impl LocalUserView {
name: &str,
bio: &str,
admin: bool,
) -> Result<Self, Error> {
) -> LemmyResult<Self> {
let instance_id = Instance::read_or_create(pool, "example.com".to_string())
.await?
.id;
@ -163,7 +163,9 @@ impl LocalUserView {
};
let local_user = LocalUser::create(pool, &user_form, vec![]).await?;
LocalUserView::read(pool, local_user.id).await
LocalUserView::read(pool, local_user.id)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
}

View file

@ -501,6 +501,7 @@ pub struct PostQuery<'a> {
}
impl<'a> PostQuery<'a> {
#[allow(clippy::expect_used)]
async fn prefetch_upper_bound_for_page_before(
&self,
site: &Site,
@ -641,6 +642,7 @@ mod tests {
PostLike,
PostLikeForm,
PostRead,
PostReadForm,
PostSaved,
PostSavedForm,
PostUpdateForm,
@ -656,10 +658,7 @@ mod tests {
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
use std::{
collections::HashSet,
time::{Duration, Instant},
};
use std::time::{Duration, Instant};
use url::Url;
const POST_WITH_ANOTHER_TITLE: &str = "Another title";
@ -997,11 +996,8 @@ mod tests {
let pool = &mut pool.into();
let mut data = init_data(pool).await?;
let post_like_form = PostLikeForm {
post_id: data.inserted_post.id,
person_id: data.local_user_view.person.id,
score: 1,
};
let post_like_form =
PostLikeForm::new(data.inserted_post.id, data.local_user_view.person.id, 1);
let inserted_post_like = PostLike::like(pool, &post_like_form).await?;
@ -1057,18 +1053,12 @@ mod tests {
// Like both the bot post, and your own
// The liked_only should not show your own post
let post_like_form = PostLikeForm {
post_id: data.inserted_post.id,
person_id: data.local_user_view.person.id,
score: 1,
};
let post_like_form =
PostLikeForm::new(data.inserted_post.id, data.local_user_view.person.id, 1);
PostLike::like(pool, &post_like_form).await?;
let bot_post_like_form = PostLikeForm {
post_id: data.inserted_bot_post.id,
person_id: data.local_user_view.person.id,
score: 1,
};
let bot_post_like_form =
PostLikeForm::new(data.inserted_bot_post.id, data.local_user_view.person.id, 1);
PostLike::like(pool, &bot_post_like_form).await?;
// Read the liked only
@ -1106,10 +1096,8 @@ mod tests {
// Save only the bot post
// The saved_only should only show the bot post
let post_save_form = PostSavedForm {
post_id: data.inserted_bot_post.id,
person_id: data.local_user_view.person.id,
};
let post_save_form =
PostSavedForm::new(data.inserted_bot_post.id, data.local_user_view.person.id);
PostSaved::save(pool, &post_save_form).await?;
// Read the saved only
@ -1524,12 +1512,8 @@ mod tests {
data.local_user_view.local_user.show_read_posts = false;
// Mark a post as read
PostRead::mark_as_read(
pool,
HashSet::from([data.inserted_bot_post.id]),
data.local_user_view.person.id,
)
.await?;
let read_form = PostReadForm::new(data.inserted_bot_post.id, data.local_user_view.person.id);
PostRead::mark_as_read(pool, &read_form).await?;
// Make sure you don't see the read post in the results
let post_listings_hide_read = data.default_post_query().list(&data.site, pool).await?;
@ -1568,7 +1552,7 @@ mod tests {
// Mark a post as hidden
PostHide::hide(
pool,
HashSet::from([data.inserted_bot_post.id]),
data.inserted_bot_post.id,
data.local_user_view.person.id,
)
.await?;

View file

@ -242,6 +242,7 @@ mod tests {
enable_animated_images: inserted_sara_local_user.enable_animated_images,
enable_private_messages: inserted_sara_local_user.enable_private_messages,
collapse_bot_comments: inserted_sara_local_user.collapse_bot_comments,
auto_mark_fetched_posts_as_read: false,
},
creator: Person {
id: inserted_sara_person.id,

View file

@ -134,19 +134,11 @@ mod tests {
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
// Timmy upvotes his own post
let timmy_post_vote_form = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_timmy.id,
score: 1,
};
let timmy_post_vote_form = PostLikeForm::new(inserted_post.id, inserted_timmy.id, 1);
PostLike::like(pool, &timmy_post_vote_form).await?;
// Sara downvotes timmy's post
let sara_post_vote_form = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_sara.id,
score: -1,
};
let sara_post_vote_form = PostLikeForm::new(inserted_post.id, inserted_sara.id, -1);
PostLike::like(pool, &sara_post_vote_form).await?;
let expected_post_vote_views = [

View file

@ -21,7 +21,7 @@ use lemmy_db_schema::{
CommunityVisibility,
SubscribedType,
};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
impl CommunityFollowerView {
/// return a list of local community ids and remote inboxes that at least one user of the given
@ -30,7 +30,7 @@ impl CommunityFollowerView {
pool: &mut DbPool<'_>,
instance_id: InstanceId,
published_since: chrono::DateTime<Utc>,
) -> Result<Vec<(CommunityId, DbUrl)>, Error> {
) -> LemmyResult<Vec<(CommunityId, DbUrl)>> {
let conn = &mut get_conn(pool).await?;
// In most cases this will fetch the same url many times (the shared inbox url)
// PG will only send a single copy to rust, but it has to scan through all follower rows (same
@ -51,6 +51,7 @@ impl CommunityFollowerView {
.distinct() // only need each community_id, inbox combination once
.load::<(CommunityId, DbUrl)>(conn)
.await
.with_lemmy_type(LemmyErrorType::NotFound)
}
pub async fn get_community_follower_inboxes(
pool: &mut DbPool<'_>,

View file

@ -286,7 +286,7 @@ mod tests {
CommunityVisibility,
SubscribedType,
};
use lemmy_utils::error::LemmyResult;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use serial_test::serial;
use url::Url;
@ -495,7 +495,7 @@ mod tests {
};
let communities = query.list(&data.site, pool).await?;
for (i, c) in communities.iter().enumerate().skip(1) {
let prev = communities.get(i - 1).expect("No previous community?");
let prev = communities.get(i - 1).ok_or(LemmyErrorType::NotFound)?;
assert!(c.community.title.cmp(&prev.community.title).is_ge());
}
@ -505,7 +505,7 @@ mod tests {
};
let communities = query.list(&data.site, pool).await?;
for (i, c) in communities.iter().enumerate().skip(1) {
let prev = communities.get(i - 1).expect("No previous community?");
let prev = communities.get(i - 1).ok_or(LemmyErrorType::NotFound)?;
assert!(c.community.title.cmp(&prev.community.title).is_le());
}

View file

@ -1,5 +1,4 @@
use crate::util::LEMMY_TEST_FAST_FEDERATION;
use anyhow::Result;
use async_trait::async_trait;
use chrono::{DateTime, TimeZone, Utc};
use lemmy_db_schema::{
@ -8,6 +7,7 @@ use lemmy_db_schema::{
utils::{ActualDbPool, DbPool},
};
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::LemmyResult;
use reqwest::Url;
use std::{
collections::{HashMap, HashSet},
@ -23,6 +23,7 @@ use std::{
/// currently fairly high because of the current structure of storing inboxes for every person, not
/// having a separate list of shared_inboxes, and the architecture of having every instance queue be
/// fully separate. (see https://github.com/LemmyNet/lemmy/issues/3958)
#[allow(clippy::expect_used)]
static FOLLOW_ADDITIONS_RECHECK_DELAY: LazyLock<chrono::TimeDelta> = LazyLock::new(|| {
if *LEMMY_TEST_FAST_FEDERATION {
chrono::TimeDelta::try_seconds(1).expect("TimeDelta out of bounds")
@ -33,20 +34,18 @@ static FOLLOW_ADDITIONS_RECHECK_DELAY: LazyLock<chrono::TimeDelta> = LazyLock::n
/// The same as FOLLOW_ADDITIONS_RECHECK_DELAY, but triggering when the last person on an instance
/// unfollows a specific remote community. This is expected to happen pretty rarely and updating it
/// in a timely manner is not too important.
#[allow(clippy::expect_used)]
static FOLLOW_REMOVALS_RECHECK_DELAY: LazyLock<chrono::TimeDelta> =
LazyLock::new(|| chrono::TimeDelta::try_hours(1).expect("TimeDelta out of bounds"));
#[async_trait]
pub trait DataSource: Send + Sync {
async fn read_site_from_instance_id(
&self,
instance_id: InstanceId,
) -> Result<Option<Site>, diesel::result::Error>;
async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> LemmyResult<Site>;
async fn get_instance_followed_community_inboxes(
&self,
instance_id: InstanceId,
last_fetch: DateTime<Utc>,
) -> Result<Vec<(CommunityId, DbUrl)>, diesel::result::Error>;
) -> LemmyResult<Vec<(CommunityId, DbUrl)>>;
}
pub struct DbDataSource {
pool: ActualDbPool,
@ -60,10 +59,7 @@ impl DbDataSource {
#[async_trait]
impl DataSource for DbDataSource {
async fn read_site_from_instance_id(
&self,
instance_id: InstanceId,
) -> Result<Option<Site>, diesel::result::Error> {
async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> LemmyResult<Site> {
Site::read_from_instance_id(&mut DbPool::Pool(&self.pool), instance_id).await
}
@ -71,7 +67,7 @@ impl DataSource for DbDataSource {
&self,
instance_id: InstanceId,
last_fetch: DateTime<Utc>,
) -> Result<Vec<(CommunityId, DbUrl)>, diesel::result::Error> {
) -> LemmyResult<Vec<(CommunityId, DbUrl)>> {
CommunityFollowerView::get_instance_followed_community_inboxes(
&mut DbPool::Pool(&self.pool),
instance_id,
@ -126,7 +122,7 @@ impl<T: DataSource> CommunityInboxCollector<T> {
/// most often this will return 0 values (if instance doesn't care about the activity)
/// or 1 value (the shared inbox)
/// > 1 values only happens for non-lemmy software
pub async fn get_inbox_urls(&mut self, activity: &SentActivity) -> Result<Vec<Url>> {
pub async fn get_inbox_urls(&mut self, activity: &SentActivity) -> LemmyResult<Vec<Url>> {
let mut inbox_urls: HashSet<Url> = HashSet::new();
if activity.send_all_instances {
@ -134,7 +130,8 @@ impl<T: DataSource> CommunityInboxCollector<T> {
self.site = self
.data_source
.read_site_from_instance_id(self.instance_id)
.await?;
.await
.ok();
self.site_loaded = true;
}
if let Some(site) = &self.site {
@ -168,7 +165,7 @@ impl<T: DataSource> CommunityInboxCollector<T> {
Ok(inbox_urls.into_iter().collect())
}
pub async fn update_communities(&mut self) -> Result<()> {
pub async fn update_communities(&mut self) -> LemmyResult<()> {
if (Utc::now() - self.last_full_communities_fetch) > *FOLLOW_REMOVALS_RECHECK_DELAY {
tracing::debug!("{}: fetching full list of communities", self.domain);
// process removals every hour
@ -201,7 +198,7 @@ impl<T: DataSource> CommunityInboxCollector<T> {
&mut self,
instance_id: InstanceId,
last_fetch: DateTime<Utc>,
) -> Result<(HashMap<CommunityId, HashSet<Url>>, DateTime<Utc>)> {
) -> LemmyResult<(HashMap<CommunityId, HashSet<Url>>, DateTime<Utc>)> {
// update to time before fetch to ensure overlap. subtract some time to ensure overlap even if
// published date is not exact
let new_last_fetch = Utc::now() - *FOLLOW_ADDITIONS_RECHECK_DELAY / 2;
@ -236,12 +233,12 @@ mod tests {
DataSource {}
#[async_trait]
impl DataSource for DataSource {
async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> Result<Option<Site>, diesel::result::Error>;
async fn read_site_from_instance_id(&self, instance_id: InstanceId) -> LemmyResult<Site>;
async fn get_instance_followed_community_inboxes(
&self,
instance_id: InstanceId,
last_fetch: DateTime<Utc>,
) -> Result<Vec<(CommunityId, DbUrl)>, diesel::result::Error>;
) -> LemmyResult<Vec<(CommunityId, DbUrl)>>;
}
}
@ -299,7 +296,7 @@ mod tests {
collector
.data_source
.expect_read_site_from_instance_id()
.return_once(move |_| Ok(Some(site)));
.return_once(move |_| Ok(site));
let activity = SentActivity {
id: ActivityId(1),
@ -333,14 +330,8 @@ mod tests {
.expect_get_instance_followed_community_inboxes()
.return_once(move |_, _| {
Ok(vec![
(
community_id,
Url::parse(url1).map_err(|_| diesel::NotFound)?.into(),
),
(
community_id,
Url::parse(url2).map_err(|_| diesel::NotFound)?.into(),
),
(community_id, Url::parse(url1)?.into()),
(community_id, Url::parse(url2)?.into()),
])
});
@ -428,20 +419,13 @@ mod tests {
collector
.data_source
.expect_read_site_from_instance_id()
.return_once(move |_| Ok(Some(site)));
.return_once(move |_| Ok(site));
let subdomain_inbox = "https://follower.example.com/inbox";
collector
.data_source
.expect_get_instance_followed_community_inboxes()
.return_once(move |_, _| {
Ok(vec![(
community_id,
Url::parse(subdomain_inbox)
.map_err(|_| diesel::NotFound)?
.into(),
)])
});
.return_once(move |_, _| Ok(vec![(community_id, Url::parse(subdomain_inbox)?.into())]));
collector.update_communities().await?;
let user1_inbox = Url::parse("https://example.com/user1/inbox")?;
@ -472,6 +456,7 @@ mod tests {
Ok(())
}
#[allow(clippy::expect_used)]
#[tokio::test]
async fn test_update_communities() -> LemmyResult<()> {
let mut collector = setup_collector();
@ -493,26 +478,11 @@ mod tests {
.returning(move |_, last_fetch| {
if last_fetch == Utc.timestamp_nanos(0) {
Ok(vec![
(
community_id1,
Url::parse(user1_inbox_str)
.map_err(|_| diesel::NotFound)?
.into(),
),
(
community_id2,
Url::parse(user2_inbox_str)
.map_err(|_| diesel::NotFound)?
.into(),
),
(community_id1, Url::parse(user1_inbox_str)?.into()),
(community_id2, Url::parse(user2_inbox_str)?.into()),
])
} else {
Ok(vec![(
community_id3,
Url::parse(user3_inbox_str)
.map_err(|_| diesel::NotFound)?
.into(),
)])
Ok(vec![(community_id3, Url::parse(user3_inbox_str)?.into())])
}
});
@ -566,7 +536,7 @@ mod tests {
collector
.data_source
.expect_read_site_from_instance_id()
.return_once(move |_| Ok(Some(site)));
.return_once(move |_| Ok(site));
collector
.data_source

View file

@ -22,8 +22,9 @@ use lemmy_db_schema::{
federation_queue_state::FederationQueueState,
instance::{Instance, InstanceForm},
},
utils::{naive_now, ActualDbPool, DbPool},
utils::{ActualDbPool, DbPool},
};
use lemmy_utils::error::LemmyResult;
use std::{collections::BinaryHeap, ops::Add, time::Duration};
use tokio::{
sync::mpsc::{self, UnboundedSender},
@ -86,7 +87,7 @@ impl InstanceWorker {
federation_worker_config: FederationWorkerConfig,
stop: CancellationToken,
stats_sender: UnboundedSender<FederationQueueStateWithDomain>,
) -> Result<(), anyhow::Error> {
) -> LemmyResult<()> {
let pool = config.to_request_data().inner_pool().clone();
let state = FederationQueueState::load(&mut DbPool::Pool(&pool), instance.id).await?;
let (report_send_result, receive_send_result) =
@ -116,7 +117,7 @@ impl InstanceWorker {
/// loop fetch new activities from db and send them to the inboxes of the given instances
/// this worker only returns if (a) there is an internal error or (b) the cancellation token is
/// cancelled (graceful exit)
async fn loop_until_stopped(&mut self) -> Result<()> {
async fn loop_until_stopped(&mut self) -> LemmyResult<()> {
self.initial_fail_sleep().await?;
let (mut last_sent_id, mut newest_id) = self.get_latest_ids().await?;
@ -149,12 +150,15 @@ impl InstanceWorker {
});
// compare to next id based on incrementing
if expected_next_id != Some(next_id_to_send.0) {
anyhow::bail!(
"{}: next id to send is not as expected: {:?} != {:?}",
self.instance.domain,
expected_next_id,
next_id_to_send
)
return Err(
anyhow::anyhow!(
"{}: next id to send is not as expected: {:?} != {:?}",
self.instance.domain,
expected_next_id,
next_id_to_send
)
.into(),
);
}
}
@ -291,7 +295,7 @@ impl InstanceWorker {
self.instance.updated = Some(Utc::now());
let form = InstanceForm {
updated: Some(naive_now()),
updated: Some(Utc::now()),
..InstanceForm::new(self.instance.domain.clone())
};
Instance::update(&mut self.pool(), self.instance.id, form).await?;
@ -331,7 +335,7 @@ impl InstanceWorker {
self.state.last_successful_published_time = next.published;
}
let save_state_every = chrono::Duration::from_std(SAVE_STATE_EVERY_TIME).expect("not negative");
let save_state_every = chrono::Duration::from_std(SAVE_STATE_EVERY_TIME)?;
if force_write || (Utc::now() - self.last_state_insert) > save_state_every {
self.save_and_send_state().await?;
}
@ -341,7 +345,7 @@ impl InstanceWorker {
/// we collect the relevant inboxes in the main instance worker task, and only spawn the send task
/// if we have inboxes to send to this limits CPU usage and reduces overhead for the (many)
/// cases where we don't have any inboxes
async fn spawn_send_if_needed(&mut self, activity_id: ActivityId) -> Result<()> {
async fn spawn_send_if_needed(&mut self, activity_id: ActivityId) -> LemmyResult<()> {
let Some(ele) = get_activity_cached(&mut self.pool(), activity_id)
.await
.context("failed reading activity from db")?
@ -357,11 +361,7 @@ impl InstanceWorker {
return Ok(());
};
let activity = &ele.0;
let inbox_urls = self
.inbox_collector
.get_inbox_urls(activity)
.await
.context("failed figuring out inbox urls")?;
let inbox_urls = self.inbox_collector.get_inbox_urls(activity).await?;
if inbox_urls.is_empty() {
// this is the case when the activity is not relevant to this receiving instance (e.g. no user
// subscribed to the relevant community)

View file

@ -312,12 +312,16 @@ where
}
// TODO: remove these conversions after actix-web upgrades to http 1.0
#[allow(clippy::expect_used)]
fn convert_status(status: http::StatusCode) -> StatusCode {
StatusCode::from_u16(status.as_u16()).expect("status can be converted")
}
#[allow(clippy::expect_used)]
fn convert_method(method: &Method) -> http::Method {
http::Method::from_bytes(method.as_str().as_bytes()).expect("method can be converted")
}
fn convert_header<'a>(name: &'a http::HeaderName, value: &'a HeaderValue) -> (&'a str, &'a [u8]) {
(name.as_str(), value.as_bytes())
}

View file

@ -3,13 +3,16 @@ use activitypub_federation::{
fetch::webfinger::{extract_webfinger_name, Webfinger, WebfingerLink, WEBFINGER_CONTENT_TYPE},
};
use actix_web::{web, web::Query, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{context::LemmyContext, LemmyErrorType};
use lemmy_db_schema::{
source::{community::Community, person::Person},
traits::ApubActor,
CommunityVisibility,
};
use lemmy_utils::{cache_header::cache_3days, error::LemmyResult};
use lemmy_utils::{
cache_header::cache_3days,
error::{LemmyErrorExt, LemmyResult},
};
use serde::Deserialize;
use std::collections::HashMap;
use url::Url;
@ -41,7 +44,7 @@ async fn get_webfinger_response(
let links = if name == context.settings().hostname {
// webfinger response for instance actor (required for mastodon authorized fetch)
let url = Url::parse(&context.settings().get_protocol_and_hostname())?;
vec![webfinger_link_for_actor(Some(url), "none", &context)]
vec![webfinger_link_for_actor(Some(url), "none", &context)?]
} else {
// webfinger response for user/community
let user_id: Option<Url> = Person::read_from_name(&mut context.pool(), name, false)
@ -65,8 +68,8 @@ async fn get_webfinger_response(
// Mastodon seems to prioritize the last webfinger item in case of duplicates. Put
// community last so that it gets prioritized. For Lemmy the order doesn't matter.
vec![
webfinger_link_for_actor(user_id, "Person", &context),
webfinger_link_for_actor(community_id, "Group", &context),
webfinger_link_for_actor(user_id, "Person", &context)?,
webfinger_link_for_actor(community_id, "Group", &context)?,
]
}
.into_iter()
@ -94,11 +97,11 @@ fn webfinger_link_for_actor(
url: Option<Url>,
kind: &str,
context: &LemmyContext,
) -> Vec<WebfingerLink> {
) -> LemmyResult<Vec<WebfingerLink>> {
if let Some(url) = url {
let type_key = "https://www.w3.org/ns/activitystreams#type"
.parse()
.expect("parse url");
.with_lemmy_type(LemmyErrorType::InvalidUrl)?;
let mut vec = vec![
WebfingerLink {
@ -128,8 +131,8 @@ fn webfinger_link_for_actor(
..Default::default()
});
}
vec
Ok(vec)
} else {
vec![]
Ok(vec![])
}
}

View file

@ -33,7 +33,7 @@ pub async fn send_email(
let email_and_port = email_config.smtp_server.split(':').collect::<Vec<&str>>();
let email = *email_and_port
.first()
.ok_or(LemmyErrorType::MissingAnEmail)?;
.ok_or(LemmyErrorType::EmailRequired)?;
let port = email_and_port
.get(1)
.ok_or(LemmyErrorType::EmailSmtpServerNeedsAPort)?
@ -45,16 +45,20 @@ pub async fn send_email(
// use usize::MAX as the line wrap length, since lettre handles the wrapping for us
let plain_text = html2text::from_read(html.as_bytes(), usize::MAX);
let smtp_from_address = &email_config.smtp_from_address;
let email = Message::builder()
.from(
email_config
.smtp_from_address
smtp_from_address
.parse()
.expect("email from address isn't valid"),
.with_lemmy_type(LemmyErrorType::InvalidEmailAddress(
smtp_from_address.into(),
))?,
)
.to(Mailbox::new(
Some(to_username.to_string()),
Address::from_str(to_email).expect("email to address isn't valid"),
Address::from_str(to_email)
.with_lemmy_type(LemmyErrorType::InvalidEmailAddress(to_email.into()))?,
))
.message_id(Some(format!("<{}@{}>", Uuid::new_v4(), settings.hostname)))
.subject(subject)
@ -62,7 +66,7 @@ pub async fn send_email(
plain_text,
html.to_string(),
))
.expect("email built incorrectly");
.with_lemmy_type(LemmyErrorType::EmailSendFailed)?;
// don't worry about 'dangeous'. it's just that leaving it at the default configuration
// is bad.

View file

@ -73,7 +73,7 @@ pub enum LemmyErrorType {
NoEmailSetup,
LocalSiteNotSetup,
EmailSmtpServerNeedsAPort,
MissingAnEmail,
InvalidEmailAddress(String),
RateLimitError,
InvalidName,
InvalidDisplayName,
@ -129,6 +129,7 @@ pub enum LemmyErrorType {
InvalidRegex,
CaptchaIncorrect,
CouldntCreateAudioCaptcha,
CouldntCreateImageCaptcha,
InvalidUrlScheme,
CouldntSendWebmention,
ContradictingFilters,
@ -185,6 +186,7 @@ pub enum FederationError {
CantDeleteSite,
ObjectIsNotPublic,
ObjectIsNotPrivate,
Unreachable,
}
cfg_if! {
@ -276,6 +278,12 @@ cfg_if! {
}
}
impl From<FederationError> for LemmyErrorType {
fn from(error: FederationError) -> Self {
LemmyErrorType::FederationError { error: Some(error) }
}
}
pub trait LemmyErrorExt<T, E: Into<anyhow::Error>> {
fn with_lemmy_type(self, error_type: LemmyErrorType) -> LemmyResult<T>;
}

View file

@ -29,6 +29,7 @@ pub struct RateLimitCell {
state: Arc<Mutex<RateLimitState>>,
}
#[allow(clippy::expect_used)]
impl RateLimitCell {
pub fn new(rate_limit_config: EnumMap<ActionType, BucketConfig>) -> Self {
let state = Arc::new(Mutex::new(RateLimitState::new(rate_limit_config)));
@ -133,6 +134,7 @@ pub struct RateLimitedMiddleware<S> {
service: Rc<S>,
}
#[allow(clippy::expect_used)]
impl RateLimitChecker {
/// Returns true if the request passed the rate limit, false if it failed and should be rejected.
pub fn check(self, ip_addr: IpAddr) -> bool {

View file

@ -18,6 +18,7 @@ pub struct InstantSecs {
secs: u32,
}
#[allow(clippy::expect_used)]
impl InstantSecs {
pub fn now() -> Self {
InstantSecs {

View file

@ -10,6 +10,7 @@ where
}
#[tracing::instrument(skip_all)]
#[allow(clippy::expect_used)]
async fn retry_custom<F, Fut, T>(f: F) -> Result<T, reqwest_middleware::Error>
where
F: Fn() -> Fut,

View file

@ -11,19 +11,20 @@ pub fn jsonify_plain_text_errors<BODY>(
return Ok(ErrorHandlerResponse::Response(res.map_into_left_body()));
}
// We're assuming that any LemmyError is already in JSON format, so we don't need to do anything
if maybe_error
.expect("http responses with 400-599 statuses should have an error object")
.as_error::<LemmyError>()
.is_some()
{
return Ok(ErrorHandlerResponse::Response(res.map_into_left_body()));
if let Some(maybe_error) = maybe_error {
if maybe_error.as_error::<LemmyError>().is_some() {
return Ok(ErrorHandlerResponse::Response(res.map_into_left_body()));
}
}
let (req, res) = res.into_parts();
let error = res
.error()
.expect("expected an error object in the response");
let response = HttpResponse::build(res.status()).json(LemmyErrorType::Unknown(error.to_string()));
let (req, res_parts) = res.into_parts();
let lemmy_err_type = if let Some(error) = res_parts.error() {
LemmyErrorType::Unknown(error.to_string())
} else {
LemmyErrorType::Unknown("couldnt build json".into())
};
let response = HttpResponse::build(res_parts.status()).json(lemmy_err_type);
let service_response = ServiceResponse::new(req, response);
Ok(ErrorHandlerResponse::Response(

View file

@ -3,6 +3,7 @@ use anyhow::{anyhow, Context};
use deser_hjson::from_str;
use regex::Regex;
use std::{env, fs, io::Error, sync::LazyLock};
use url::Url;
use urlencoding::encode;
pub mod structs;
@ -11,6 +12,7 @@ use structs::{DatabaseConnection, PictrsConfig, PictrsImageMode, Settings};
static DEFAULT_CONFIG_FILE: &str = "config/config.hjson";
#[allow(clippy::expect_used)]
pub static SETTINGS: LazyLock<Settings> = LazyLock::new(|| {
if env::var("LEMMY_INITIALIZE_WITH_DEFAULT_SETTINGS").is_ok() {
println!(
@ -23,6 +25,7 @@ pub static SETTINGS: LazyLock<Settings> = LazyLock::new(|| {
}
});
#[allow(clippy::expect_used)]
static WEBFINGER_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(&format!(
"^acct:([a-zA-Z0-9_]{{3,}})@{}$",
@ -128,3 +131,9 @@ impl PictrsConfig {
}
}
}
#[allow(clippy::expect_used)]
/// Necessary to avoid URL expect failures
fn pictrs_placeholder_url() -> Url {
Url::parse("http://localhost:8080").expect("parse pictrs url")
}

View file

@ -1,3 +1,4 @@
use super::pictrs_placeholder_url;
use doku::Document;
use serde::{Deserialize, Serialize};
use smart_default::SmartDefault;
@ -52,7 +53,7 @@ pub struct Settings {
/// Sets a response Access-Control-Allow-Origin CORS header
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
#[default(None)]
#[doku(example = "*")]
#[doku(example = "lemmy.tld")]
cors_origin: Option<String>,
}
@ -68,7 +69,7 @@ impl Settings {
#[serde(default, deny_unknown_fields)]
pub struct PictrsConfig {
/// Address where pictrs is available (for image hosting)
#[default(Url::parse("http://localhost:8080").expect("parse pictrs url"))]
#[default(pictrs_placeholder_url())]
#[doku(example = "http://localhost:8080")]
pub url: Url,

View file

@ -58,11 +58,13 @@ fn find_urls<T: NodeValue + UrlAndTitle>(src: &str) -> Vec<(usize, usize)> {
let mut links_offsets = vec![];
ast.walk(|node, _depth| {
if let Some(image) = node.cast::<T>() {
let (_, node_offset) = node.srcmap.expect("srcmap is none").get_byte_offsets();
let start_offset = node_offset - image.url_len() - 1 - image.title_len();
let end_offset = node_offset - 1;
if let Some(srcmap) = node.srcmap {
let (_, node_offset) = srcmap.get_byte_offsets();
let start_offset = node_offset - image.url_len() - 1 - image.title_len();
let end_offset = node_offset - 1;
links_offsets.push((start_offset, end_offset));
links_offsets.push((start_offset, end_offset));
}
}
});
links_offsets

View file

@ -2,6 +2,7 @@ use itertools::Itertools;
use regex::Regex;
use std::sync::LazyLock;
#[allow(clippy::expect_used)]
static MENTIONS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"@(?P<name>[\w.]+)@(?P<domain>[a-zA-Z0-9._:-]+)").expect("compile regex")
});

View file

@ -1,8 +1,8 @@
use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use regex::{Regex, RegexBuilder};
pub fn remove_slurs(test: &str, slur_regex: &Option<Regex>) -> String {
if let Some(slur_regex) = slur_regex {
pub fn remove_slurs(test: &str, slur_regex: &Option<LemmyResult<Regex>>) -> String {
if let Some(Ok(slur_regex)) = slur_regex {
slur_regex.replace_all(test, "*removed*").to_string()
} else {
test.to_string()
@ -11,9 +11,9 @@ pub fn remove_slurs(test: &str, slur_regex: &Option<Regex>) -> String {
pub(crate) fn slur_check<'a>(
test: &'a str,
slur_regex: &'a Option<Regex>,
slur_regex: &'a Option<LemmyResult<Regex>>,
) -> Result<(), Vec<&'a str>> {
if let Some(slur_regex) = slur_regex {
if let Some(Ok(slur_regex)) = slur_regex {
let mut matches: Vec<&str> = slur_regex.find_iter(test).map(|mat| mat.as_str()).collect();
// Unique
@ -30,16 +30,16 @@ pub(crate) fn slur_check<'a>(
}
}
pub fn build_slur_regex(regex_str: Option<&str>) -> Option<Regex> {
pub fn build_slur_regex(regex_str: Option<&str>) -> Option<LemmyResult<Regex>> {
regex_str.map(|slurs| {
RegexBuilder::new(slurs)
.case_insensitive(true)
.build()
.expect("compile regex")
.with_lemmy_type(LemmyErrorType::InvalidRegex)
})
}
pub fn check_slurs(text: &str, slur_regex: &Option<Regex>) -> LemmyResult<()> {
pub fn check_slurs(text: &str, slur_regex: &Option<LemmyResult<Regex>>) -> LemmyResult<()> {
if let Err(slurs) = slur_check(text, slur_regex) {
Err(anyhow::anyhow!("{}", slurs_vec_to_str(&slurs))).with_lemmy_type(LemmyErrorType::Slurs)
} else {
@ -47,7 +47,10 @@ pub fn check_slurs(text: &str, slur_regex: &Option<Regex>) -> LemmyResult<()> {
}
}
pub fn check_slurs_opt(text: &Option<String>, slur_regex: &Option<Regex>) -> LemmyResult<()> {
pub fn check_slurs_opt(
text: &Option<String>,
slur_regex: &Option<LemmyResult<Regex>>,
) -> LemmyResult<()> {
match text {
Some(t) => check_slurs(t, slur_regex),
None => Ok(()),
@ -64,7 +67,7 @@ pub(crate) fn slurs_vec_to_str(slurs: &[&str]) -> String {
mod test {
use crate::{
error::LemmyResult,
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str},
};
use pretty_assertions::assert_eq;
@ -72,7 +75,7 @@ mod test {
#[test]
fn test_slur_filter() -> LemmyResult<()> {
let slur_regex = Some(RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build()?);
let slur_regex = Some(RegexBuilder::new(r"(fag(g|got|tard)?\b|cock\s?sucker(s|ing)?|ni((g{2,}|q)+|[gq]{2,})[e3r]+(s|z)?|mudslime?s?|kikes?|\bspi(c|k)s?\b|\bchinks?|gooks?|bitch(es|ing|y)?|whor(es?|ing)|\btr(a|@)nn?(y|ies?)|\b(b|re|r)tard(ed)?s?)").case_insensitive(true).build().with_lemmy_type(LemmyErrorType::InvalidRegex));
let test =
"faggot test kike tranny cocksucker retardeds. Capitalized Niggerz. This is a bunch of other safe text.";
let slur_free = "No slurs here";

View file

@ -6,11 +6,13 @@ use std::sync::LazyLock;
use url::{ParseError, Url};
// From here: https://github.com/vector-im/element-android/blob/develop/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt#L35
#[allow(clippy::expect_used)]
static VALID_MATRIX_ID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
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
#[allow(clippy::expect_used)]
static URL_CLEANER: LazyLock<UrlCleaner> =
LazyLock::new(|| UrlCleaner::from_embedded_rules().expect("compile clearurls"));
const ALLOWED_POST_URL_SCHEMES: [&str; 3] = ["http", "https", "magnet"];
@ -85,26 +87,20 @@ fn has_newline(name: &str) -> bool {
}
pub fn is_valid_actor_name(name: &str, actor_name_max_length: usize) -> LemmyResult<()> {
static VALID_ACTOR_NAME_REGEX_EN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_]{3,}$").expect("compile regex"));
static VALID_ACTOR_NAME_REGEX_AR: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[\p{Arabic}0-9_]{3,}$").expect("compile regex"));
static VALID_ACTOR_NAME_REGEX_RU: LazyLock<Regex> =
LazyLock::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);
#[allow(clippy::expect_used)]
static VALID_ACTOR_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(?:[a-zA-Z0-9_]+|[0-9_\p{Arabic}]+|[0-9_\p{Cyrillic}]+)$").expect("compile regex")
});
if !check || !lang_check {
Err(LemmyErrorType::InvalidName.into())
} else {
min_length_check(name, 3, LemmyErrorType::InvalidName)?;
max_length_check(name, actor_name_max_length, LemmyErrorType::InvalidName)?;
if VALID_ACTOR_NAME_REGEX.is_match(name) {
Ok(())
} else {
Err(LemmyErrorType::InvalidName.into())
}
}
@ -225,33 +221,32 @@ fn min_length_check(item: &str, min_length: usize, min_msg: LemmyErrorType) -> L
}
/// Attempts to build a regex and check it for common errors before inserting into the DB.
pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> LemmyResult<Option<Regex>> {
regex_str_opt.map_or_else(
|| Ok(None::<Regex>),
|regex_str| {
if regex_str.is_empty() {
// If the proposed regex is empty, return as having no regex at all; this is the same
// behavior that happens downstream before the write to the database.
return Ok(None::<Regex>);
}
RegexBuilder::new(regex_str)
.case_insensitive(true)
.build()
.with_lemmy_type(LemmyErrorType::InvalidRegex)
.and_then(|regex| {
// NOTE: It is difficult to know, in the universe of user-crafted regex, which ones
// may match against any string text. To keep it simple, we'll match the regex
// against an innocuous string - a single number - which should help catch a regex
// that accidentally matches against all strings.
if regex.is_match("1") {
Err(LemmyErrorType::PermissiveRegex.into())
} else {
Ok(Some(regex))
}
})
},
)
pub fn build_and_check_regex(regex_str_opt: &Option<&str>) -> Option<LemmyResult<Regex>> {
if let Some(regex) = regex_str_opt {
if regex.is_empty() {
None
} else {
Some(
RegexBuilder::new(regex)
.case_insensitive(true)
.build()
.with_lemmy_type(LemmyErrorType::InvalidRegex)
.and_then(|regex| {
// NOTE: It is difficult to know, in the universe of user-crafted regex, which ones
// may match against any string text. To keep it simple, we'll match the regex
// against an innocuous string - a single number - which should help catch a regex
// that accidentally matches against all strings.
if regex.is_match("1") {
Err(LemmyErrorType::PermissiveRegex.into())
} else {
Ok(regex)
}
}),
)
}
} else {
None
}
}
/// Cleans a url of tracking parameters.
@ -334,7 +329,7 @@ pub fn build_url_str_without_scheme(url_str: &str) -> LemmyResult<String> {
// Set the scheme to http, then remove the http:// part
url
.set_scheme("http")
.map_err(|_| LemmyErrorType::InvalidUrl)?;
.map_err(|_e| LemmyErrorType::InvalidUrl)?;
let mut out = url
.to_string()
@ -379,11 +374,14 @@ mod tests {
use pretty_assertions::assert_eq;
use url::Url;
const URL_WITH_TRACKING: &str = "https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123";
const URL_TRACKING_REMOVED: &str = "https://example.com/path/123?user+name=random+user&id=123";
#[test]
fn test_clean_url_params() -> LemmyResult<()> {
let url = Url::parse("https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123")?;
let url = Url::parse(URL_WITH_TRACKING)?;
let cleaned = clean_url(&url);
let expected = Url::parse("https://example.com/path/123?user+name=random+user&id=123")?;
let expected = Url::parse(URL_TRACKING_REMOVED)?;
assert_eq!(expected.to_string(), cleaned.to_string());
let url = Url::parse("https://example.com/path/123")?;
@ -395,9 +393,9 @@ mod tests {
#[test]
fn test_clean_body() -> LemmyResult<()> {
let text = "[a link](https://example.com/path/123?utm_content=buffercf3b2&utm_medium=social&user+name=random+user&id=123)";
let cleaned = clean_urls_in_text(text);
let expected = "[a link](https://example.com/path/123?user+name=random+user&id=123)";
let text = format!("[a link]({URL_WITH_TRACKING})");
let cleaned = clean_urls_in_text(&text);
let expected = format!("[a link]({URL_TRACKING_REMOVED})");
assert_eq!(expected.to_string(), cleaned.to_string());
let text = "[a link](https://example.com/path/123)";
@ -438,6 +436,15 @@ mod tests {
assert!(is_valid_actor_name("a", actor_name_max_length).is_err());
// empty
assert!(is_valid_actor_name("", actor_name_max_length).is_err());
// newline
assert!(is_valid_actor_name(
r"Line1
Line3",
actor_name_max_length
)
.is_err());
assert!(is_valid_actor_name("Line1\nLine3", actor_name_max_length).is_err());
}
#[test]
@ -560,13 +567,27 @@ mod tests {
#[test]
fn test_valid_slur_regex() {
let valid_regexes = [&None, &Some(""), &Some("(foo|bar)")];
let valid_regex = Some("(foo|bar)");
let result = build_and_check_regex(&valid_regex);
assert!(
result.is_some_and(|x| x.is_ok()),
"Testing regex: {:?}",
valid_regex
);
}
valid_regexes.iter().for_each(|regex| {
let result = build_and_check_regex(regex);
#[test]
fn test_missing_slur_regex() {
let missing_regex = None;
let result = build_and_check_regex(&missing_regex);
assert!(result.is_none());
}
assert!(result.is_ok(), "Testing regex: {:?}", regex);
});
#[test]
fn test_empty_slur_regex() {
let empty = Some("");
let result = build_and_check_regex(&empty);
assert!(result.is_none());
}
#[test]
@ -582,9 +603,9 @@ mod tests {
.for_each(|(regex_str, expected_err)| {
let result = build_and_check_regex(regex_str);
assert!(result.is_err());
assert!(result.as_ref().is_some_and(Result::is_err));
assert!(
result.is_err_and(|e| e.error_type.eq(&expected_err.clone())),
result.is_some_and(|x| x.is_err_and(|e| e.error_type.eq(&expected_err.clone()))),
"Testing regex {:?}, expected error {}",
regex_str,
expected_err

View file

@ -0,0 +1,3 @@
ALTER TABLE local_user
DROP COLUMN auto_mark_fetched_posts_as_read;

View file

@ -0,0 +1,3 @@
ALTER TABLE local_user
ADD COLUMN auto_mark_fetched_posts_as_read boolean DEFAULT FALSE NOT NULL;

View file

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

View file

@ -0,0 +1,2 @@
DROP FUNCTION community_aggregates_activity, site_aggregates_activity CASCADE;

View file

@ -60,6 +60,7 @@ use lemmy_api::{
like::like_post,
list_post_likes::list_post_likes,
lock::lock_post,
mark_many_read::mark_posts_as_read,
mark_read::mark_post_as_read,
save::save_post,
},
@ -239,6 +240,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
.route("/delete", web::post().to(delete_post))
.route("/remove", web::post().to(remove_post))
.route("/mark_as_read", web::post().to(mark_post_as_read))
.route("/mark_many_as_read", web::post().to(mark_posts_as_read))
.route("/hide", web::post().to(hide_post))
.route("/lock", web::post().to(lock_post))
.route("/feature", web::post().to(feature_post))

View file

@ -1,5 +1,6 @@
// This is for db migrations that require code
use activitypub_federation::http_signatures::generate_actor_keypair;
use chrono::Utc;
use diesel::{
sql_types::{Nullable, Text},
ExpressionMethods,
@ -26,9 +27,12 @@ use lemmy_db_schema::{
site::{Site, SiteInsertForm, SiteUpdateForm},
},
traits::Crud,
utils::{get_conn, naive_now, DbPool},
utils::{get_conn, DbPool},
};
use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
settings::structs::Settings,
};
use lemmy_utils::{error::LemmyResult, settings::structs::Settings};
use tracing::info;
use url::Url;
@ -78,7 +82,7 @@ async fn user_updates_2020_04_02(
)?),
private_key: Some(Some(keypair.private_key)),
public_key: Some(keypair.public_key),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
..Default::default()
};
@ -118,7 +122,7 @@ async fn community_updates_2020_04_02(
actor_id: Some(community_actor_id.clone()),
private_key: Some(Some(keypair.private_key)),
public_key: Some(keypair.public_key),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
..Default::default()
};
@ -334,7 +338,7 @@ async fn instance_actor_2022_01_28(
let actor_id = Url::parse(protocol_and_hostname)?;
let site_form = SiteUpdateForm {
actor_id: Some(actor_id.clone().into()),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
inbox_url: Some(generate_inbox_url()?),
private_key: Some(Some(key_pair.private_key)),
public_key: Some(key_pair.public_key),
@ -420,7 +424,7 @@ async fn initialize_local_site_2022_10_10(
let domain = settings
.get_hostname_without_port()
.expect("must have domain");
.with_lemmy_type(LemmyErrorType::Unknown("must have domain".into()))?;
// Upsert this to the instance table
let instance = Instance::read_or_create(pool, domain).await?;
@ -465,7 +469,7 @@ async fn initialize_local_site_2022_10_10(
.unwrap_or_else(|| "New Site".to_string());
let site_form = SiteInsertForm {
actor_id: Some(site_actor_id.clone().into()),
last_refreshed_at: Some(naive_now()),
last_refreshed_at: Some(Utc::now()),
inbox_url: Some(generate_inbox_url()?),
private_key: Some(site_key_pair.private_key),
public_key: Some(site_key_pair.public_key),

Some files were not shown because too many files have changed in this diff Show more