diff --git a/Cargo.lock b/Cargo.lock index eebb1ce1a7..6151787294 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2677,6 +2677,7 @@ version = "0.19.6-beta.7" dependencies = [ "actix-web", "chrono", + "derive-new", "diesel", "diesel-async", "diesel_ltree", @@ -2700,8 +2701,8 @@ dependencies = [ "chrono", "diesel", "diesel-async", + "i-love-jesus", "lemmy_db_schema", - "lemmy_db_views", "lemmy_utils", "pretty_assertions", "serde", @@ -2719,9 +2720,14 @@ version = "0.19.6-beta.7" dependencies = [ "diesel", "diesel-async", + "i-love-jesus", "lemmy_db_schema", + "lemmy_utils", + "pretty_assertions", "serde", "serde_with", + "serial_test", + "tokio", "ts-rs", ] diff --git a/api_tests/package.json b/api_tests/package.json index 7ea21d0ba0..62f14d1943 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -6,7 +6,7 @@ "repository": "https://github.com/LemmyNet/lemmy", "author": "Dessalines", "license": "AGPL-3.0", - "packageManager": "pnpm@9.12.3", + "packageManager": "pnpm@9.15.0", "scripts": { "lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'", "fix": "prettier --write src && eslint --fix src", @@ -22,16 +22,16 @@ }, "devDependencies": { "@types/jest": "^29.5.12", - "@types/node": "^22.9.0", - "@typescript-eslint/eslint-plugin": "^8.13.0", - "@typescript-eslint/parser": "^8.13.0", - "eslint": "^9.14.0", + "@types/node": "^22.10.1", + "@typescript-eslint/eslint-plugin": "^8.18.0", + "@typescript-eslint/parser": "^8.18.0", + "eslint": "^9.16.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-api-v4.16", - "prettier": "^3.2.5", + "lemmy-js-client": "0.20.0-reports-combined.3", + "prettier": "^3.4.2", "ts-jest": "^29.1.0", - "typescript": "^5.5.4", - "typescript-eslint": "^8.13.0" + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0" } } diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 496606e6c2..4d2ebb31b1 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -12,38 +12,38 @@ importers: specifier: ^29.5.12 version: 29.5.14 '@types/node': - specifier: ^22.9.0 - version: 22.9.0 + specifier: ^22.10.1 + version: 22.10.1 '@typescript-eslint/eslint-plugin': - 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) + specifier: ^8.18.0 + version: 8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0)(typescript@5.7.2) '@typescript-eslint/parser': - specifier: ^8.13.0 - version: 8.13.0(eslint@9.14.0)(typescript@5.6.3) + specifier: ^8.18.0 + version: 8.18.0(eslint@9.16.0)(typescript@5.7.2) eslint: - specifier: ^9.14.0 - version: 9.14.0 + specifier: ^9.16.0 + version: 9.16.0 eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.2.1(eslint@9.14.0)(prettier@3.3.3) + version: 5.2.1(eslint@9.16.0)(prettier@3.4.2) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@22.9.0) + version: 29.7.0(@types/node@22.10.1) lemmy-js-client: - specifier: 0.20.0-api-v4.16 - version: 0.20.0-api-v4.16 + specifier: 0.20.0-reports-combined.3 + version: 0.20.0-reports-combined.3 prettier: - specifier: ^3.2.5 - version: 3.3.3 + specifier: ^3.4.2 + version: 3.4.2 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.9.0))(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.10.1))(typescript@5.7.2) typescript: - specifier: ^5.5.4 - version: 5.6.3 + specifier: ^5.7.2 + version: 5.7.2 typescript-eslint: - specifier: ^8.13.0 - version: 8.13.0(eslint@9.14.0)(typescript@5.6.3) + specifier: ^8.18.0 + version: 8.18.0(eslint@9.16.0)(typescript@5.7.2) packages: @@ -228,28 +228,28 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.18.0': - resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} + '@eslint/config-array@0.19.1': + resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.7.0': - resolution: {integrity: sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==} + '@eslint/core@0.9.1': + resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.1.0': - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.14.0': - resolution: {integrity: sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==} + '@eslint/js@9.16.0': + resolution: {integrity: sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.4': - resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.2': - resolution: {integrity: sha512-CXtq5nR4Su+2I47WPOlWud98Y5Lv8Kyxp2ukhgFx/eW6Blm18VXJO5WuQylPugRo8nbluoi6GvvxBLqHcvqUUw==} + '@eslint/plugin-kit@0.2.4': + resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': @@ -422,8 +422,8 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@22.9.0': - resolution: {integrity: sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==} + '@types/node@22.10.1': + resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -434,61 +434,51 @@ packages: '@types/yargs@17.0.32': resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} - '@typescript-eslint/eslint-plugin@8.13.0': - resolution: {integrity: sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==} + '@typescript-eslint/eslint-plugin@8.18.0': + resolution: {integrity: sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/parser@8.13.0': - resolution: {integrity: sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==} + '@typescript-eslint/parser@8.18.0': + resolution: {integrity: sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/scope-manager@8.13.0': - resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==} + '@typescript-eslint/scope-manager@8.18.0': + resolution: {integrity: sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.13.0': - resolution: {integrity: sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==} + '@typescript-eslint/type-utils@8.18.0': + resolution: {integrity: sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/types@8.13.0': - resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==} + '@typescript-eslint/types@8.18.0': + resolution: {integrity: sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.13.0': - resolution: {integrity: sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==} + '@typescript-eslint/typescript-estree@8.18.0': + resolution: {integrity: sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/utils@8.13.0': - resolution: {integrity: sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==} + '@typescript-eslint/utils@8.18.0': + resolution: {integrity: sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/visitor-keys@8.13.0': - resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==} + '@typescript-eslint/visitor-keys@8.18.0': + resolution: {integrity: sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -666,6 +656,15 @@ packages: supports-color: optional: true + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dedent@1.5.1: resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} peerDependencies: @@ -745,8 +744,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.14.0: - resolution: {integrity: sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==} + eslint@9.16.0: + resolution: {integrity: sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1167,8 +1166,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-api-v4.16: - resolution: {integrity: sha512-9Wn7b8YT2KnEA286+RV1B3mLmecAynvAERoC0ZZiccfSgkEvd3rG9A5X9ejiPqp+JzDZJeisO57+Ut4QHr5oTw==} + lemmy-js-client@0.20.0-reports-combined.3: + resolution: {integrity: sha512-0Z/9S41r6NM8f09Gkxerq9zYBE6UcywXfeWNxsYknkyh0ZnKbtNxjTkSxE6JpRbz7wokKFRSH9NpwgNloQY5uw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -1335,8 +1334,8 @@ packages: resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} engines: {node: '>=14'} hasBin: true @@ -1482,9 +1481,6 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -1502,6 +1498,12 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + ts-jest@29.2.5: resolution: {integrity: sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -1541,22 +1543,20 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} - typescript-eslint@8.13.0: - resolution: {integrity: sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==} + typescript-eslint@8.18.0: + resolution: {integrity: sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} update-browserslist-db@1.0.13: resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} @@ -1816,24 +1816,26 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@eslint-community/eslint-utils@4.4.1(eslint@9.14.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.16.0)': dependencies: - eslint: 9.14.0 + eslint: 9.16.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.18.0': + '@eslint/config-array@0.19.1': dependencies: - '@eslint/object-schema': 2.1.4 + '@eslint/object-schema': 2.1.5 debug: 4.3.7 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/core@0.7.0': {} + '@eslint/core@0.9.1': + dependencies: + '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.1.0': + '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 debug: 4.3.7 @@ -1847,11 +1849,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.14.0': {} + '@eslint/js@9.16.0': {} - '@eslint/object-schema@2.1.4': {} + '@eslint/object-schema@2.1.5': {} - '@eslint/plugin-kit@0.2.2': + '@eslint/plugin-kit@0.2.4': dependencies: levn: 0.4.1 @@ -1881,7 +1883,7 @@ snapshots: '@jest/console@29.7.0': dependencies: '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 chalk: 4.1.2 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -1894,14 +1896,14 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 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.9.0) + jest-config: 29.7.0(@types/node@22.10.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -1926,7 +1928,7 @@ snapshots: dependencies: '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 jest-mock: 29.7.0 '@jest/expect-utils@29.7.0': @@ -1944,7 +1946,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@sinonjs/fake-timers': 10.3.0 - '@types/node': 22.9.0 + '@types/node': 22.10.1 jest-message-util: 29.7.0 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -1966,7 +1968,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.22 - '@types/node': 22.9.0 + '@types/node': 22.10.1 chalk: 4.1.2 collect-v8-coverage: 1.0.2 exit: 0.1.2 @@ -2036,7 +2038,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.9.0 + '@types/node': 22.10.1 '@types/yargs': 17.0.32 chalk: 4.1.2 @@ -2106,7 +2108,7 @@ snapshots: '@types/graceful-fs@4.1.9': dependencies: - '@types/node': 22.9.0 + '@types/node': 22.10.1 '@types/istanbul-lib-coverage@2.0.6': {} @@ -2125,9 +2127,9 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@22.9.0': + '@types/node@22.10.1': dependencies: - undici-types: 6.19.8 + undici-types: 6.20.0 '@types/stack-utils@2.0.3': {} @@ -2137,86 +2139,82 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.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/eslint-plugin@8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0)(typescript@5.7.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@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 + '@typescript-eslint/parser': 8.18.0(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/type-utils': 8.18.0(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.18.0 + eslint: 9.16.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.4.0(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 + ts-api-utils: 1.4.0(typescript@5.7.2) + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.13.0(eslint@9.14.0)(typescript@5.6.3)': + '@typescript-eslint/parser@8.18.0(eslint@9.16.0)(typescript@5.7.2)': dependencies: - '@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 + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.18.0 debug: 4.3.7 - eslint: 9.14.0 - optionalDependencies: - typescript: 5.6.3 + eslint: 9.16.0 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.13.0': + '@typescript-eslint/scope-manager@8.18.0': dependencies: - '@typescript-eslint/types': 8.13.0 - '@typescript-eslint/visitor-keys': 8.13.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/visitor-keys': 8.18.0 - '@typescript-eslint/type-utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)': + '@typescript-eslint/type-utils@8.18.0(eslint@9.16.0)(typescript@5.7.2)': dependencies: - '@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: - typescript: 5.6.3 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.16.0)(typescript@5.7.2) + debug: 4.4.0 + eslint: 9.16.0 + ts-api-utils: 1.4.0(typescript@5.7.2) + typescript: 5.7.2 transitivePeerDependencies: - - eslint - supports-color - '@typescript-eslint/types@8.13.0': {} + '@typescript-eslint/types@8.18.0': {} - '@typescript-eslint/typescript-estree@8.13.0(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@8.18.0(typescript@5.7.2)': dependencies: - '@typescript-eslint/types': 8.13.0 - '@typescript-eslint/visitor-keys': 8.13.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/visitor-keys': 8.18.0 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.4.0(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.13.0(eslint@9.14.0)(typescript@5.6.3)': + '@typescript-eslint/utils@8.18.0(eslint@9.16.0)(typescript@5.7.2)': dependencies: - '@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 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.0) + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + eslint: 9.16.0 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@8.13.0': + '@typescript-eslint/visitor-keys@8.18.0': dependencies: - '@typescript-eslint/types': 8.13.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.18.0 + eslint-visitor-keys: 4.2.0 acorn-jsx@5.3.2(acorn@8.14.0): dependencies: @@ -2383,13 +2381,13 @@ snapshots: convert-source-map@2.0.0: {} - create-jest@29.7.0(@types/node@22.9.0): + create-jest@29.7.0(@types/node@22.10.1): 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.9.0) + jest-config: 29.7.0(@types/node@22.10.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -2414,6 +2412,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.0: + dependencies: + ms: 2.1.3 + dedent@1.5.1: {} deep-is@0.1.4: {} @@ -2444,10 +2446,10 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-prettier@5.2.1(eslint@9.14.0)(prettier@3.3.3): + eslint-plugin-prettier@5.2.1(eslint@9.16.0)(prettier@3.4.2): dependencies: - eslint: 9.14.0 - prettier: 3.3.3 + eslint: 9.16.0 + prettier: 3.4.2 prettier-linter-helpers: 1.0.0 synckit: 0.9.1 @@ -2460,15 +2462,15 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.14.0: + eslint@9.16.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.14.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.16.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.14.0 - '@eslint/plugin-kit': 0.2.2 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.9.1 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.16.0 + '@eslint/plugin-kit': 0.2.4 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.1 @@ -2496,7 +2498,6 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - text-table: 0.2.0 transitivePeerDependencies: - supports-color @@ -2752,7 +2753,7 @@ snapshots: '@jest/expect': 29.7.0 '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 chalk: 4.1.2 co: 4.6.0 dedent: 1.5.1 @@ -2772,16 +2773,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@22.9.0): + jest-cli@29.7.0(@types/node@22.10.1): 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.9.0) + create-jest: 29.7.0(@types/node@22.10.1) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.9.0) + jest-config: 29.7.0(@types/node@22.10.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -2791,7 +2792,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@22.9.0): + jest-config@29.7.0(@types/node@22.10.1): dependencies: '@babel/core': 7.23.9 '@jest/test-sequencer': 29.7.0 @@ -2816,7 +2817,7 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 22.9.0 + '@types/node': 22.10.1 transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -2845,7 +2846,7 @@ snapshots: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 jest-mock: 29.7.0 jest-util: 29.7.0 @@ -2855,7 +2856,7 @@ snapshots: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.9 - '@types/node': 22.9.0 + '@types/node': 22.10.1 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -2894,7 +2895,7 @@ snapshots: jest-mock@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): @@ -2929,7 +2930,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -2957,7 +2958,7 @@ snapshots: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 chalk: 4.1.2 cjs-module-lexer: 1.2.3 collect-v8-coverage: 1.0.2 @@ -3003,7 +3004,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -3022,7 +3023,7 @@ snapshots: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 22.9.0 + '@types/node': 22.10.1 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -3031,17 +3032,17 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 22.9.0 + '@types/node': 22.10.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@22.9.0): + jest@29.7.0(@types/node@22.10.1): dependencies: '@jest/core': 29.7.0 '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.9.0) + jest-cli: 29.7.0(@types/node@22.10.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -3077,7 +3078,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-api-v4.16: {} + lemmy-js-client@0.20.0-reports-combined.3: {} leven@3.1.0: {} @@ -3226,7 +3227,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.3.3: {} + prettier@3.4.2: {} pretty-format@29.7.0: dependencies: @@ -3344,8 +3345,6 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 - text-table@0.2.0: {} - tmpl@1.0.5: {} to-fast-properties@2.0.0: {} @@ -3354,22 +3353,26 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@1.4.0(typescript@5.6.3): + ts-api-utils@1.4.0(typescript@5.7.2): dependencies: - typescript: 5.6.3 + typescript: 5.7.2 - 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): + ts-api-utils@1.4.3(typescript@5.7.2): + dependencies: + typescript: 5.7.2 + + 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.10.1))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@22.9.0) + jest: 29.7.0(@types/node@22.10.1) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 semver: 7.6.3 - typescript: 5.6.3 + typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: '@babel/core': 7.23.9 @@ -3387,20 +3390,19 @@ snapshots: type-fest@0.21.3: {} - typescript-eslint@8.13.0(eslint@9.14.0)(typescript@5.6.3): + typescript-eslint@8.18.0(eslint@9.16.0)(typescript@5.7.2): dependencies: - '@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 + '@typescript-eslint/eslint-plugin': 8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/parser': 8.18.0(eslint@9.16.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.16.0)(typescript@5.7.2) + eslint: 9.16.0 + typescript: 5.7.2 transitivePeerDependencies: - - eslint - supports-color - typescript@5.6.3: {} + typescript@5.7.2: {} - undici-types@6.19.8: {} + undici-types@6.20.0: {} update-browserslist-db@1.0.13(browserslist@4.22.3): dependencies: diff --git a/api_tests/src/comment.spec.ts b/api_tests/src/comment.spec.ts index 5cf94aa03b..419e587698 100644 --- a/api_tests/src/comment.spec.ts +++ b/api_tests/src/comment.spec.ts @@ -22,7 +22,6 @@ import { createCommunity, registerUser, reportComment, - listCommentReports, randomString, unfollows, getComments, @@ -38,8 +37,15 @@ import { blockCommunity, delay, saveUserSettings, + listReports, } from "./shared"; -import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client"; +import { + CommentReportView, + CommentView, + CommunityView, + ReportCombinedView, + SaveUserSettings, +} from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; let postOnAlphaRes: PostResponse; @@ -796,13 +802,17 @@ test("Report a comment", async () => { let alphaReport = (await reportComment(alpha, alphaComment.id, reason)) .comment_report_view.comment_report; - let betaReport = (await waitUntil( - () => - listCommentReports(beta).then(r => - r.comment_reports.find(rep => rep.comment_report.reason === reason), - ), - e => !!e, - ))!.comment_report; + let betaReport = ( + (await waitUntil( + () => + listReports(beta).then(p => + p.reports.find(r => { + return checkCommentReportReason(r, reason); + }), + ), + e => !!e, + )!) as CommentReportView + ).comment_report; expect(betaReport).toBeDefined(); expect(betaReport.resolved).toBe(false); expect(betaReport.original_comment_text).toBe( @@ -877,3 +887,12 @@ test.skip("Fetch a deeply nested comment", async () => { expect(betaComment!.comment!.comment).toBeDefined(); expect(betaComment?.comment?.post).toBeDefined(); }); + +function checkCommentReportReason(rcv: ReportCombinedView, reason: string) { + switch (rcv.type_) { + case "Comment": + return rcv.comment_report.reason === reason; + default: + return false; + } +} diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index 2bb0920881..2d1570ea6f 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -16,7 +16,6 @@ import { followCommunity, banPersonFromCommunity, resolvePerson, - getSite, createPost, getPost, resolvePost, @@ -36,7 +35,7 @@ import { userBlockInstance, } from "./shared"; import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; -import { EditCommunity, EditSite, GetPosts } from "lemmy-js-client"; +import { EditCommunity, GetPosts } from "lemmy-js-client"; beforeAll(setupLogins); afterAll(unfollows); @@ -573,7 +572,7 @@ test("Remote mods can edit communities", async () => { communityRes.community_view.community.id, ); - await expect(alphaCommunity.community_view.community.description).toBe( + expect(alphaCommunity.community_view.community.description).toBe( "Example description", ); }); diff --git a/api_tests/src/follow.spec.ts b/api_tests/src/follow.spec.ts index 936ce26065..c447e14cd7 100644 --- a/api_tests/src/follow.spec.ts +++ b/api_tests/src/follow.spec.ts @@ -5,7 +5,6 @@ import { setupLogins, resolveBetaCommunity, followCommunity, - getSite, waitUntil, beta, betaUrl, diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index a3478081ad..4d1abbdfd9 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -18,7 +18,6 @@ import { epsilon, followCommunity, gamma, - getSite, imageFetchLimit, registerUser, resolveBetaCommunity, diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 4158bbdc7b..37381d3023 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -27,10 +27,8 @@ import { followCommunity, banPersonFromCommunity, reportPost, - listPostReports, randomString, registerUser, - getSite, unfollows, resolveCommunity, waitUntil, @@ -38,11 +36,18 @@ import { alphaUrl, loginUser, createCommunity, + listReports, getMyUser, } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; -import { EditSite, ResolveObject } from "lemmy-js-client"; +import { + EditSite, + PostReport, + PostReportView, + ReportCombinedView, + ResolveObject, +} from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -688,16 +693,17 @@ test("Report a post", async () => { expect(gammaReport).toBeDefined(); // Report was federated to community instance - let betaReport = (await waitUntil( - () => - listPostReports(beta).then(p => - p.post_reports.find( - r => - r.post_report.original_post_name === gammaReport.original_post_name, + let betaReport = ( + (await waitUntil( + () => + listReports(beta).then(p => + p.reports.find(r => { + return checkReportName(r, gammaReport); + }), ), - ), - res => !!res, - ))!.post_report; + res => !!res, + ))! as PostReportView + ).post_report; expect(betaReport).toBeDefined(); expect(betaReport.resolved).toBe(false); expect(betaReport.original_post_name).toBe(gammaReport.original_post_name); @@ -707,16 +713,25 @@ test("Report a post", async () => { await unfollowRemotes(alpha); // Report was federated to poster's instance - let alphaReport = (await waitUntil( - () => - listPostReports(alpha).then(p => - p.post_reports.find( - r => - r.post_report.original_post_name === gammaReport.original_post_name, + let alphaReport = ( + (await waitUntil( + () => + listReports(alpha).then(p => + p.reports.find(r => { + switch (r.type_) { + case "Post": + return ( + r.post_report.original_post_name === + gammaReport.original_post_name + ); + default: + return false; + } + }), ), - ), - res => !!res, - ))!.post_report; + res => !!res, + ))! as PostReportView + ).post_report; expect(alphaReport).toBeDefined(); expect(alphaReport.resolved).toBe(false); expect(alphaReport.original_post_name).toBe(gammaReport.original_post_name); @@ -817,3 +832,12 @@ test("Rewrite markdown links", async () => { `[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`, ); }); + +function checkReportName(rcv: ReportCombinedView, report: PostReport) { + switch (rcv.type_) { + case "Post": + return rcv.post_report.original_post_name === report.original_post_name; + default: + return false; + } +} diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 1ed13d9cff..a6f8bc78c2 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -1,5 +1,4 @@ import { - AdminBlockInstanceParams, ApproveCommunityPendingFollower, BlockCommunity, BlockCommunityResponse, @@ -16,6 +15,8 @@ import { LemmyHttp, ListCommunityPendingFollows, ListCommunityPendingFollowsResponse, + ListReports, + ListReportsResponse, MyUserInfo, PersonId, PostView, @@ -75,12 +76,8 @@ import { PrivateMessagesResponse } from "lemmy-js-client/dist/types/PrivateMessa import { GetPrivateMessages } from "lemmy-js-client/dist/types/GetPrivateMessages"; import { PostReportResponse } from "lemmy-js-client/dist/types/PostReportResponse"; import { CreatePostReport } from "lemmy-js-client/dist/types/CreatePostReport"; -import { ListPostReportsResponse } from "lemmy-js-client/dist/types/ListPostReportsResponse"; -import { ListPostReports } from "lemmy-js-client/dist/types/ListPostReports"; import { CommentReportResponse } from "lemmy-js-client/dist/types/CommentReportResponse"; import { CreateCommentReport } from "lemmy-js-client/dist/types/CreateCommentReport"; -import { ListCommentReportsResponse } from "lemmy-js-client/dist/types/ListCommentReportsResponse"; -import { ListCommentReports } from "lemmy-js-client/dist/types/ListCommentReports"; import { GetPostsResponse } from "lemmy-js-client/dist/types/GetPostsResponse"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; import { GetPersonDetailsResponse } from "lemmy-js-client/dist/types/GetPersonDetailsResponse"; @@ -210,7 +207,9 @@ async function allowInstance(api: LemmyHttp, instance: string) { // Ignore errors from duplicate allows (because setup gets called for each test file) try { await api.adminAllowInstance(params); - } catch {} + } catch { + // console.error(error); + } } export async function createPost( @@ -809,11 +808,11 @@ export async function reportPost( return api.createPostReport(form); } -export async function listPostReports( +export async function listReports( api: LemmyHttp, -): Promise { - let form: ListPostReports = {}; - return api.listPostReports(form); +): Promise { + let form: ListReports = {}; + return api.listReports(form); } export async function reportComment( @@ -840,13 +839,6 @@ export async function reportPrivateMessage( return api.createPrivateMessageReport(form); } -export async function listCommentReports( - api: LemmyHttp, -): Promise { - let form: ListCommentReports = {}; - return api.listCommentReports(form); -} - export function getPosts( api: LemmyHttp, listingType?: ListingType, diff --git a/crates/api/src/comment/like.rs b/crates/api/src/comment/like.rs index fbc7201024..0815b38639 100644 --- a/crates/api/src/comment/like.rs +++ b/crates/api/src/comment/like.rs @@ -5,10 +5,10 @@ use lemmy_api_common::{ comment::{CommentResponse, CreateCommentLike}, context::LemmyContext, send_activity::{ActivityChannel, SendActivityData}, - utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem}, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode}, }; use lemmy_db_schema::{ - newtypes::LocalUserId, + newtypes::{LocalUserId, PostOrCommentId}, source::{ comment::{CommentLike, CommentLikeForm}, comment_reply::CommentReply, @@ -33,7 +33,7 @@ pub async fn like_comment( check_local_vote_mode( data.score, - VoteItem::Comment(comment_id), + PostOrCommentId::Comment(comment_id), &local_site, local_user_view.person.id, &mut context.pool(), diff --git a/crates/api/src/comment/save.rs b/crates/api/src/comment/save.rs index 6efa6296da..cca6d06bc6 100644 --- a/crates/api/src/comment/save.rs +++ b/crates/api/src/comment/save.rs @@ -16,10 +16,7 @@ pub async fn save_comment( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let comment_saved_form = CommentSavedForm { - comment_id: data.comment_id, - person_id: local_user_view.person.id, - }; + let comment_saved_form = CommentSavedForm::new(data.comment_id, local_user_view.person.id); if data.save { CommentSaved::save(&mut context.pool(), &comment_saved_form) diff --git a/crates/api/src/comment_report/list.rs b/crates/api/src/comment_report/list.rs deleted file mode 100644 index d2f723819f..0000000000 --- a/crates/api/src/comment_report/list.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - comment::{ListCommentReports, ListCommentReportsResponse}, - context::LemmyContext, - utils::check_community_mod_of_any_or_admin_action, -}; -use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView}; -use lemmy_utils::error::LemmyResult; - -/// Lists comment reports for a community if an id is supplied -/// or returns all comment reports for communities a user moderates -#[tracing::instrument(skip(context))] -pub async fn list_comment_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let community_id = data.community_id; - let comment_id = data.comment_id; - let unresolved_only = data.unresolved_only.unwrap_or_default(); - - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - - let page = data.page; - let limit = data.limit; - let comment_reports = CommentReportQuery { - community_id, - comment_id, - unresolved_only, - page, - limit, - } - .list(&mut context.pool(), &local_user_view) - .await?; - - Ok(Json(ListCommentReportsResponse { comment_reports })) -} diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 6a2c94332b..aa6e370009 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -33,13 +33,11 @@ use std::io::Cursor; use totp_rs::{Secret, TOTP}; pub mod comment; -pub mod comment_report; pub mod community; pub mod local_user; pub mod post; -pub mod post_report; pub mod private_message; -pub mod private_message_report; +pub mod reports; pub mod site; pub mod sitemap; diff --git a/crates/api/src/local_user/list_saved.rs b/crates/api/src/local_user/list_saved.rs new file mode 100644 index 0000000000..5f0deff39e --- /dev/null +++ b/crates/api/src/local_user/list_saved.rs @@ -0,0 +1,40 @@ +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + person::{ListPersonSaved, ListPersonSavedResponse}, + utils::check_private_instance, +}; +use lemmy_db_views::{ + person_saved_combined_view::PersonSavedCombinedQuery, + structs::{LocalUserView, SiteView}, +}; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn list_person_saved( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let local_site = SiteView::read_local(&mut context.pool()).await?; + + check_private_instance(&Some(local_user_view.clone()), &local_site.local_site)?; + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let saved = PersonSavedCombinedQuery { + page_after, + page_back, + } + .list(&mut context.pool(), &local_user_view) + .await?; + + Ok(Json(ListPersonSavedResponse { saved })) +} diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index d3fc37a73c..0dcd24affc 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -8,6 +8,7 @@ pub mod get_captcha; pub mod list_banned; pub mod list_logins; pub mod list_media; +pub mod list_saved; pub mod login; pub mod logout; pub mod notifications; diff --git a/crates/api/src/local_user/notifications/list_inbox.rs b/crates/api/src/local_user/notifications/list_inbox.rs new file mode 100644 index 0000000000..7d6e884689 --- /dev/null +++ b/crates/api/src/local_user/notifications/list_inbox.rs @@ -0,0 +1,40 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + person::{ListInbox, ListInboxResponse}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::inbox_combined_view::InboxCombinedQuery; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn list_inbox( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let unread_only = data.unread_only; + let type_ = data.type_; + let person_id = local_user_view.person.id; + let show_bot_accounts = Some(local_user_view.local_user.show_bot_accounts); + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let inbox = InboxCombinedQuery { + type_, + unread_only, + show_bot_accounts, + page_after, + page_back, + } + .list(&mut context.pool(), person_id) + .await?; + + Ok(Json(ListInboxResponse { inbox })) +} diff --git a/crates/api/src/local_user/notifications/list_mentions.rs b/crates/api/src/local_user/notifications/list_mentions.rs deleted file mode 100644 index bf3cd8e0d7..0000000000 --- a/crates/api/src/local_user/notifications/list_mentions.rs +++ /dev/null @@ -1,36 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - person::{GetPersonMentions, GetPersonMentionsResponse}, -}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::person_mention_view::PersonMentionQuery; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn list_mentions( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let sort = data.sort; - let page = data.page; - let limit = data.limit; - let unread_only = data.unread_only.unwrap_or_default(); - let person_id = Some(local_user_view.person.id); - let show_bot_accounts = local_user_view.local_user.show_bot_accounts; - - let mentions = PersonMentionQuery { - recipient_id: person_id, - my_person_id: person_id, - sort, - unread_only, - show_bot_accounts, - page, - limit, - } - .list(&mut context.pool()) - .await?; - - Ok(Json(GetPersonMentionsResponse { mentions })) -} diff --git a/crates/api/src/local_user/notifications/list_replies.rs b/crates/api/src/local_user/notifications/list_replies.rs deleted file mode 100644 index d88595d967..0000000000 --- a/crates/api/src/local_user/notifications/list_replies.rs +++ /dev/null @@ -1,36 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - person::{GetReplies, GetRepliesResponse}, -}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::comment_reply_view::CommentReplyQuery; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn list_replies( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let sort = data.sort; - let page = data.page; - let limit = data.limit; - let unread_only = data.unread_only.unwrap_or_default(); - let person_id = Some(local_user_view.person.id); - let show_bot_accounts = local_user_view.local_user.show_bot_accounts; - - let replies = CommentReplyQuery { - recipient_id: person_id, - my_person_id: person_id, - sort, - unread_only, - show_bot_accounts, - page, - limit, - } - .list(&mut context.pool()) - .await?; - - Ok(Json(GetRepliesResponse { replies })) -} diff --git a/crates/api/src/local_user/notifications/mark_all_read.rs b/crates/api/src/local_user/notifications/mark_all_read.rs index 558d276f72..9ba0916f8a 100644 --- a/crates/api/src/local_user/notifications/mark_all_read.rs +++ b/crates/api/src/local_user/notifications/mark_all_read.rs @@ -1,8 +1,9 @@ use actix_web::web::{Data, Json}; -use lemmy_api_common::{context::LemmyContext, person::GetRepliesResponse}; +use lemmy_api_common::{context::LemmyContext, SuccessResponse}; use lemmy_db_schema::source::{ comment_reply::CommentReply, - person_mention::PersonMention, + person_comment_mention::PersonCommentMention, + person_post_mention::PersonPostMention, private_message::PrivateMessage, }; use lemmy_db_views::structs::LocalUserView; @@ -12,7 +13,7 @@ use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; pub async fn mark_all_notifications_read( context: Data, local_user_view: LocalUserView, -) -> LemmyResult> { +) -> LemmyResult> { let person_id = local_user_view.person.id; // Mark all comment_replies as read @@ -20,15 +21,20 @@ pub async fn mark_all_notifications_read( .await .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; - // Mark all user mentions as read - PersonMention::mark_all_as_read(&mut context.pool(), person_id) + // Mark all comment mentions as read + PersonCommentMention::mark_all_as_read(&mut context.pool(), person_id) .await .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; + // Mark all post mentions as read + PersonPostMention::mark_all_as_read(&mut context.pool(), person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; + // Mark all private_messages as read PrivateMessage::mark_all_as_read(&mut context.pool(), person_id) .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; - Ok(Json(GetRepliesResponse { replies: vec![] })) + Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/local_user/notifications/mark_comment_mention_read.rs b/crates/api/src/local_user/notifications/mark_comment_mention_read.rs new file mode 100644 index 0000000000..4a9c400924 --- /dev/null +++ b/crates/api/src/local_user/notifications/mark_comment_mention_read.rs @@ -0,0 +1,50 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{ + context::LemmyContext, + person::{MarkPersonCommentMentionAsRead, PersonCommentMentionResponse}, +}; +use lemmy_db_schema::{ + source::person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm}, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PersonCommentMentionView; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; + +#[tracing::instrument(skip(context))] +pub async fn mark_comment_mention_as_read( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let person_comment_mention_id = data.person_comment_mention_id; + let read_person_comment_mention = + PersonCommentMention::read(&mut context.pool(), person_comment_mention_id).await?; + + if local_user_view.person.id != read_person_comment_mention.recipient_id { + Err(LemmyErrorType::CouldntUpdateComment)? + } + + let person_comment_mention_id = read_person_comment_mention.id; + let read = Some(data.read); + PersonCommentMention::update( + &mut context.pool(), + person_comment_mention_id, + &PersonCommentMentionUpdateForm { read }, + ) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; + + let person_comment_mention_id = read_person_comment_mention.id; + let person_id = local_user_view.person.id; + let person_comment_mention_view = PersonCommentMentionView::read( + &mut context.pool(), + person_comment_mention_id, + Some(person_id), + ) + .await?; + + Ok(Json(PersonCommentMentionResponse { + person_comment_mention_view, + })) +} diff --git a/crates/api/src/local_user/notifications/mark_mention_read.rs b/crates/api/src/local_user/notifications/mark_mention_read.rs deleted file mode 100644 index 9a839b2b41..0000000000 --- a/crates/api/src/local_user/notifications/mark_mention_read.rs +++ /dev/null @@ -1,45 +0,0 @@ -use actix_web::web::{Data, Json}; -use lemmy_api_common::{ - context::LemmyContext, - person::{MarkPersonMentionAsRead, PersonMentionResponse}, -}; -use lemmy_db_schema::{ - source::person_mention::{PersonMention, PersonMentionUpdateForm}, - traits::Crud, -}; -use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_actor::structs::PersonMentionView; -use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; - -#[tracing::instrument(skip(context))] -pub async fn mark_person_mention_as_read( - data: Json, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let person_mention_id = data.person_mention_id; - let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?; - - if local_user_view.person.id != read_person_mention.recipient_id { - Err(LemmyErrorType::CouldntUpdateComment)? - } - - let person_mention_id = read_person_mention.id; - let read = Some(data.read); - PersonMention::update( - &mut context.pool(), - person_mention_id, - &PersonMentionUpdateForm { read }, - ) - .await - .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; - - let person_mention_id = read_person_mention.id; - let person_id = local_user_view.person.id; - let person_mention_view = - PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?; - - Ok(Json(PersonMentionResponse { - person_mention_view, - })) -} diff --git a/crates/api/src/local_user/notifications/mark_post_mention_read.rs b/crates/api/src/local_user/notifications/mark_post_mention_read.rs new file mode 100644 index 0000000000..35a12c65cf --- /dev/null +++ b/crates/api/src/local_user/notifications/mark_post_mention_read.rs @@ -0,0 +1,47 @@ +use actix_web::web::{Data, Json}; +use lemmy_api_common::{ + context::LemmyContext, + person::{MarkPersonPostMentionAsRead, PersonPostMentionResponse}, +}; +use lemmy_db_schema::{ + source::person_post_mention::{PersonPostMention, PersonPostMentionUpdateForm}, + traits::Crud, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PersonPostMentionView; +use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; + +#[tracing::instrument(skip(context))] +pub async fn mark_post_mention_as_read( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let person_post_mention_id = data.person_post_mention_id; + let read_person_post_mention = + PersonPostMention::read(&mut context.pool(), person_post_mention_id).await?; + + if local_user_view.person.id != read_person_post_mention.recipient_id { + Err(LemmyErrorType::CouldntUpdatePost)? + } + + let person_post_mention_id = read_person_post_mention.id; + let read = Some(data.read); + PersonPostMention::update( + &mut context.pool(), + person_post_mention_id, + &PersonPostMentionUpdateForm { read }, + ) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; + + let person_post_mention_id = read_person_post_mention.id; + let person_id = local_user_view.person.id; + let person_post_mention_view = + PersonPostMentionView::read(&mut context.pool(), person_post_mention_id, Some(person_id)) + .await?; + + Ok(Json(PersonPostMentionResponse { + person_post_mention_view, + })) +} diff --git a/crates/api/src/local_user/notifications/mod.rs b/crates/api/src/local_user/notifications/mod.rs index 35567afde9..9f2048d90e 100644 --- a/crates/api/src/local_user/notifications/mod.rs +++ b/crates/api/src/local_user/notifications/mod.rs @@ -1,6 +1,6 @@ -pub mod list_mentions; -pub mod list_replies; +pub mod list_inbox; pub mod mark_all_read; -pub mod mark_mention_read; +pub mod mark_comment_mention_read; +pub mod mark_post_mention_read; pub mod mark_reply_read; pub mod unread_count; diff --git a/crates/api/src/local_user/notifications/unread_count.rs b/crates/api/src/local_user/notifications/unread_count.rs index 4c6c65263e..4fa959329d 100644 --- a/crates/api/src/local_user/notifications/unread_count.rs +++ b/crates/api/src/local_user/notifications/unread_count.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{context::LemmyContext, person::GetUnreadCountResponse}; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; -use lemmy_db_views_actor::structs::{CommentReplyView, PersonMentionView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::InboxCombinedViewInternal; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -10,20 +10,10 @@ pub async fn unread_count( local_user_view: LocalUserView, ) -> LemmyResult> { let person_id = local_user_view.person.id; - - let replies = - CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).await?; - - let mentions = - PersonMentionView::get_unread_mentions(&mut context.pool(), &local_user_view.local_user) + let show_bot_accounts = local_user_view.local_user.show_bot_accounts; + let count = + InboxCombinedViewInternal::get_unread_count(&mut context.pool(), person_id, show_bot_accounts) .await?; - let private_messages = - PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?; - - Ok(Json(GetUnreadCountResponse { - replies, - mentions, - private_messages, - })) + Ok(Json(GetUnreadCountResponse { count })) } diff --git a/crates/api/src/local_user/report_count.rs b/crates/api/src/local_user/report_count.rs index 32448dcaae..0d24a4de94 100644 --- a/crates/api/src/local_user/report_count.rs +++ b/crates/api/src/local_user/report_count.rs @@ -4,12 +4,7 @@ use lemmy_api_common::{ person::{GetReportCount, GetReportCountResponse}, utils::check_community_mod_of_any_or_admin_action, }; -use lemmy_db_views::structs::{ - CommentReportView, - LocalUserView, - PostReportView, - PrivateMessageReportView, -}; +use lemmy_db_views::structs::{LocalUserView, ReportCombinedViewInternal}; use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] @@ -18,29 +13,14 @@ pub async fn report_count( context: Data, local_user_view: LocalUserView, ) -> LemmyResult> { - let person_id = local_user_view.person.id; - let admin = local_user_view.local_user.admin; - let community_id = data.community_id; - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - let comment_reports = - CommentReportView::get_report_count(&mut context.pool(), person_id, admin, community_id) - .await?; - - let post_reports = - PostReportView::get_report_count(&mut context.pool(), person_id, admin, community_id).await?; - - let private_message_reports = if admin && community_id.is_none() { - Some(PrivateMessageReportView::get_report_count(&mut context.pool()).await?) - } else { - None - }; + let count = ReportCombinedViewInternal::get_report_count( + &mut context.pool(), + &local_user_view, + data.community_id, + ) + .await?; - Ok(Json(GetReportCountResponse { - community_id, - comment_reports, - post_reports, - private_message_reports, - })) + Ok(Json(GetReportCountResponse { count })) } diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index 8ede8c31c9..7f2415e38c 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -60,8 +60,8 @@ pub async fn feature_post( let form = ModFeaturePostForm { mod_person_id: local_user_view.person.id, post_id: data.post_id, - featured: data.featured, - is_featured_community: data.feature_type == PostFeatureType::Community, + featured: Some(data.featured), + is_featured_community: Some(data.feature_type == PostFeatureType::Community), }; ModFeaturePost::create(&mut context.pool(), &form).await?; diff --git a/crates/api/src/post/like.rs b/crates/api/src/post/like.rs index 031e3f0dbc..6555228e91 100644 --- a/crates/api/src/post/like.rs +++ b/crates/api/src/post/like.rs @@ -5,9 +5,10 @@ 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, VoteItem}, + utils::{check_bot_account, check_community_user_action, check_local_vote_mode}, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::{ local_site::LocalSite, post::{PostLike, PostLikeForm, PostRead, PostReadForm}, @@ -29,7 +30,7 @@ pub async fn like_post( check_local_vote_mode( data.score, - VoteItem::Post(post_id), + PostOrCommentId::Post(post_id), &local_site, local_user_view.person.id, &mut context.pool(), diff --git a/crates/api/src/post_report/list.rs b/crates/api/src/post_report/list.rs deleted file mode 100644 index 7d1d50b0b3..0000000000 --- a/crates/api/src/post_report/list.rs +++ /dev/null @@ -1,37 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - post::{ListPostReports, ListPostReportsResponse}, - utils::check_community_mod_of_any_or_admin_action, -}; -use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView}; -use lemmy_utils::error::LemmyResult; - -/// Lists post reports for a community if an id is supplied -/// or returns all post reports for communities a user moderates -#[tracing::instrument(skip(context))] -pub async fn list_post_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let community_id = data.community_id; - let post_id = data.post_id; - let unresolved_only = data.unresolved_only.unwrap_or_default(); - - check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; - - let page = data.page; - let limit = data.limit; - let post_reports = PostReportQuery { - community_id, - post_id, - unresolved_only, - page, - limit, - } - .list(&mut context.pool(), &local_user_view) - .await?; - - Ok(Json(ListPostReportsResponse { post_reports })) -} diff --git a/crates/api/src/private_message/mark_read.rs b/crates/api/src/private_message/mark_read.rs index 7c213464b6..26655caef1 100644 --- a/crates/api/src/private_message/mark_read.rs +++ b/crates/api/src/private_message/mark_read.rs @@ -7,7 +7,8 @@ use lemmy_db_schema::{ source::private_message::{PrivateMessage, PrivateMessageUpdateForm}, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] diff --git a/crates/api/src/private_message_report/list.rs b/crates/api/src/private_message_report/list.rs deleted file mode 100644 index 79ef53e1c1..0000000000 --- a/crates/api/src/private_message_report/list.rs +++ /dev/null @@ -1,35 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - private_message::{ListPrivateMessageReports, ListPrivateMessageReportsResponse}, - utils::is_admin, -}; -use lemmy_db_views::{ - private_message_report_view::PrivateMessageReportQuery, - structs::LocalUserView, -}; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn list_pm_reports( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - is_admin(&local_user_view)?; - - let unresolved_only = data.unresolved_only.unwrap_or_default(); - let page = data.page; - let limit = data.limit; - let private_message_reports = PrivateMessageReportQuery { - unresolved_only, - page, - limit, - } - .list(&mut context.pool()) - .await?; - - Ok(Json(ListPrivateMessageReportsResponse { - private_message_reports, - })) -} diff --git a/crates/api/src/comment_report/create.rs b/crates/api/src/reports/comment_report/create.rs similarity index 97% rename from crates/api/src/comment_report/create.rs rename to crates/api/src/reports/comment_report/create.rs index 48066cfe66..a456ded369 100644 --- a/crates/api/src/comment_report/create.rs +++ b/crates/api/src/reports/comment_report/create.rs @@ -2,8 +2,8 @@ use crate::check_report_reason; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ - comment::{CommentReportResponse, CreateCommentReport}, context::LemmyContext, + reports::comment::{CommentReportResponse, CreateCommentReport}, send_activity::{ActivityChannel, SendActivityData}, utils::{ check_comment_deleted_or_removed, diff --git a/crates/api/src/post_report/mod.rs b/crates/api/src/reports/comment_report/mod.rs similarity index 70% rename from crates/api/src/post_report/mod.rs rename to crates/api/src/reports/comment_report/mod.rs index 3bb1a9b46f..c85613aa60 100644 --- a/crates/api/src/post_report/mod.rs +++ b/crates/api/src/reports/comment_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/api/src/comment_report/resolve.rs b/crates/api/src/reports/comment_report/resolve.rs similarity index 95% rename from crates/api/src/comment_report/resolve.rs rename to crates/api/src/reports/comment_report/resolve.rs index 58d5041dcf..5ab36054f2 100644 --- a/crates/api/src/comment_report/resolve.rs +++ b/crates/api/src/reports/comment_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ - comment::{CommentReportResponse, ResolveCommentReport}, context::LemmyContext, + reports::comment::{CommentReportResponse, ResolveCommentReport}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable}; diff --git a/crates/api/src/reports/mod.rs b/crates/api/src/reports/mod.rs new file mode 100644 index 0000000000..f23d1d71f2 --- /dev/null +++ b/crates/api/src/reports/mod.rs @@ -0,0 +1,4 @@ +pub mod comment_report; +pub mod post_report; +pub mod private_message_report; +pub mod report_combined; diff --git a/crates/api/src/post_report/create.rs b/crates/api/src/reports/post_report/create.rs similarity index 97% rename from crates/api/src/post_report/create.rs rename to crates/api/src/reports/post_report/create.rs index b9edf35c57..bc85bdbe7d 100644 --- a/crates/api/src/post_report/create.rs +++ b/crates/api/src/reports/post_report/create.rs @@ -3,7 +3,7 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, - post::{CreatePostReport, PostReportResponse}, + reports::post::{CreatePostReport, PostReportResponse}, send_activity::{ActivityChannel, SendActivityData}, utils::{ check_community_user_action, diff --git a/crates/api/src/private_message_report/mod.rs b/crates/api/src/reports/post_report/mod.rs similarity index 70% rename from crates/api/src/private_message_report/mod.rs rename to crates/api/src/reports/post_report/mod.rs index 3bb1a9b46f..c85613aa60 100644 --- a/crates/api/src/private_message_report/mod.rs +++ b/crates/api/src/reports/post_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/api/src/post_report/resolve.rs b/crates/api/src/reports/post_report/resolve.rs similarity index 96% rename from crates/api/src/post_report/resolve.rs rename to crates/api/src/reports/post_report/resolve.rs index 6523275131..26b182a456 100644 --- a/crates/api/src/post_report/resolve.rs +++ b/crates/api/src/reports/post_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - post::{PostReportResponse, ResolvePostReport}, + reports::post::{PostReportResponse, ResolvePostReport}, utils::check_community_mod_action, }; use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable}; diff --git a/crates/api/src/private_message_report/create.rs b/crates/api/src/reports/private_message_report/create.rs similarity index 96% rename from crates/api/src/private_message_report/create.rs rename to crates/api/src/reports/private_message_report/create.rs index de8ca390fa..17b5dceeb2 100644 --- a/crates/api/src/private_message_report/create.rs +++ b/crates/api/src/reports/private_message_report/create.rs @@ -2,7 +2,7 @@ use crate::check_report_reason; use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse}, + reports::private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse}, utils::send_new_report_email_to_admins, }; use lemmy_db_schema::{ diff --git a/crates/api/src/comment_report/mod.rs b/crates/api/src/reports/private_message_report/mod.rs similarity index 70% rename from crates/api/src/comment_report/mod.rs rename to crates/api/src/reports/private_message_report/mod.rs index 3bb1a9b46f..c85613aa60 100644 --- a/crates/api/src/comment_report/mod.rs +++ b/crates/api/src/reports/private_message_report/mod.rs @@ -1,3 +1,2 @@ pub mod create; -pub mod list; pub mod resolve; diff --git a/crates/api/src/private_message_report/resolve.rs b/crates/api/src/reports/private_message_report/resolve.rs similarity index 93% rename from crates/api/src/private_message_report/resolve.rs rename to crates/api/src/reports/private_message_report/resolve.rs index 7d821a60c9..3f812e4fe4 100644 --- a/crates/api/src/private_message_report/resolve.rs +++ b/crates/api/src/reports/private_message_report/resolve.rs @@ -1,7 +1,7 @@ use actix_web::web::{Data, Json}; use lemmy_api_common::{ context::LemmyContext, - private_message::{PrivateMessageReportResponse, ResolvePrivateMessageReport}, + reports::private_message::{PrivateMessageReportResponse, ResolvePrivateMessageReport}, utils::is_admin, }; use lemmy_db_schema::{source::private_message_report::PrivateMessageReport, traits::Reportable}; diff --git a/crates/api/src/reports/report_combined/list.rs b/crates/api/src/reports/report_combined/list.rs new file mode 100644 index 0000000000..12548d1891 --- /dev/null +++ b/crates/api/src/reports/report_combined/list.rs @@ -0,0 +1,41 @@ +use actix_web::web::{Data, Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + reports::combined::{ListReports, ListReportsResponse}, + utils::check_community_mod_of_any_or_admin_action, +}; +use lemmy_db_views::{report_combined_view::ReportCombinedQuery, structs::LocalUserView}; +use lemmy_utils::error::LemmyResult; + +/// Lists reports for a community if an id is supplied +/// or returns all reports for communities a user moderates +#[tracing::instrument(skip(context))] +pub async fn list_reports( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> LemmyResult> { + let community_id = data.community_id; + let unresolved_only = data.unresolved_only; + + check_community_mod_of_any_or_admin_action(&local_user_view, &mut context.pool()).await?; + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let reports = ReportCombinedQuery { + community_id, + unresolved_only, + page_after, + page_back, + } + .list(&mut context.pool(), &local_user_view) + .await?; + + Ok(Json(ListReportsResponse { reports })) +} diff --git a/crates/api/src/reports/report_combined/mod.rs b/crates/api/src/reports/report_combined/mod.rs new file mode 100644 index 0000000000..d17e233fbf --- /dev/null +++ b/crates/api/src/reports/report_combined/mod.rs @@ -0,0 +1 @@ +pub mod list; diff --git a/crates/api/src/site/admin_allow_instance.rs b/crates/api/src/site/admin_allow_instance.rs index 81879ecaed..cf3415b5b9 100644 --- a/crates/api/src/site/admin_allow_instance.rs +++ b/crates/api/src/site/admin_allow_instance.rs @@ -7,10 +7,13 @@ use lemmy_api_common::{ LemmyErrorType, SuccessResponse, }; -use lemmy_db_schema::source::{ - federation_allowlist::{FederationAllowList, FederationAllowListForm}, - instance::Instance, - mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, +use lemmy_db_schema::{ + source::{ + federation_allowlist::{FederationAllowList, FederationAllowListForm}, + instance::Instance, + mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, + }, + traits::Crud, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; @@ -47,7 +50,7 @@ pub async fn admin_allow_instance( reason: data.reason.clone(), allowed: data.allow, }; - AdminAllowInstance::insert(&mut context.pool(), &mod_log_form).await?; + AdminAllowInstance::create(&mut context.pool(), &mod_log_form).await?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/admin_block_instance.rs b/crates/api/src/site/admin_block_instance.rs index 54962ccf3a..f7b286ee1a 100644 --- a/crates/api/src/site/admin_block_instance.rs +++ b/crates/api/src/site/admin_block_instance.rs @@ -7,10 +7,13 @@ use lemmy_api_common::{ LemmyErrorType, SuccessResponse, }; -use lemmy_db_schema::source::{ - federation_blocklist::{FederationBlockList, FederationBlockListForm}, - instance::Instance, - mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, +use lemmy_db_schema::{ + source::{ + federation_blocklist::{FederationBlockList, FederationBlockListForm}, + instance::Instance, + mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, + }, + traits::Crud, }; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyResult; @@ -48,9 +51,8 @@ pub async fn admin_block_instance( admin_person_id: local_user_view.person.id, blocked: data.block, reason: data.reason.clone(), - when_: data.expires, }; - AdminBlockInstance::insert(&mut context.pool(), &mod_log_form).await?; + AdminBlockInstance::create(&mut context.pool(), &mod_log_form).await?; Ok(Json(SuccessResponse::default())) } diff --git a/crates/api/src/site/mod_log.rs b/crates/api/src/site/mod_log.rs index bbf147666a..42fa770a60 100644 --- a/crates/api/src/site/mod_log.rs +++ b/crates/api/src/site/mod_log.rs @@ -4,30 +4,10 @@ use lemmy_api_common::{ site::{GetModlog, GetModlogResponse}, utils::{check_community_mod_of_any_or_admin_action, check_private_instance}, }; -use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType}; +use lemmy_db_schema::source::local_site::LocalSite; use lemmy_db_views::structs::LocalUserView; -use lemmy_db_views_moderator::structs::{ - AdminAllowInstanceView, - AdminBlockInstanceView, - AdminPurgeCommentView, - AdminPurgeCommunityView, - AdminPurgePersonView, - AdminPurgePostView, - ModAddCommunityView, - ModAddView, - ModBanFromCommunityView, - ModBanView, - ModFeaturePostView, - ModHideCommunityView, - ModLockPostView, - ModRemoveCommentView, - ModRemoveCommunityView, - ModRemovePostView, - ModTransferCommunityView, - ModlogListParams, -}; +use lemmy_db_views_moderator::{self, modlog_combined_view::ModlogCombinedQuery}; use lemmy_utils::error::LemmyResult; -use ModlogActionType::*; #[tracing::instrument(skip(context))] pub async fn get_mod_log( @@ -39,7 +19,7 @@ pub async fn get_mod_log( check_private_instance(&local_user_view, &local_site)?; - let type_ = data.type_.unwrap_or(All); + let type_ = data.type_; let community_id = data.community_id; let is_mod_or_admin = if let Some(local_user_view) = local_user_view { @@ -56,150 +36,31 @@ pub async fn get_mod_log( } else { data.mod_person_id }; - let other_person_id = data.other_person_id; + let modded_person_id = data.modded_person_id; let post_id = data.post_id; let comment_id = data.comment_id; - let params = ModlogListParams { + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let modlog = ModlogCombinedQuery { + type_, community_id, mod_person_id, - other_person_id, + modded_person_id, post_id, comment_id, - page: data.page, - limit: data.limit, - hide_modlog_names, - }; - let removed_posts = match type_ { - All | ModRemovePost => ModRemovePostView::list(&mut context.pool(), params).await?, - _ => Default::default(), - }; - - let locked_posts = match type_ { - All | ModLockPost => ModLockPostView::list(&mut context.pool(), params).await?, - _ => Default::default(), - }; - - let featured_posts = match type_ { - All | ModFeaturePost => ModFeaturePostView::list(&mut context.pool(), params).await?, - _ => Default::default(), - }; - - let removed_comments = match type_ { - All | ModRemoveComment => ModRemoveCommentView::list(&mut context.pool(), params).await?, - _ => Default::default(), - }; - - let banned_from_community = match type_ { - All | ModBanFromCommunity => ModBanFromCommunityView::list(&mut context.pool(), params).await?, - _ => Default::default(), - }; - - let added_to_community = match type_ { - All | ModAddCommunity => ModAddCommunityView::list(&mut context.pool(), params).await?, - _ => Default::default(), - }; - - let transferred_to_community = match type_ { - All | ModTransferCommunity => { - ModTransferCommunityView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }; - - let hidden_communities = match type_ { - All | ModHideCommunity if other_person_id.is_none() => { - ModHideCommunityView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }; - - // These arrays are only for the full modlog, when a community isn't given - let ( - banned, - added, - removed_communities, - admin_purged_persons, - admin_purged_communities, - admin_purged_posts, - admin_purged_comments, - admin_block_instance, - admin_allow_instance, - ) = if data.community_id.is_none() { - ( - match type_ { - All | ModBan => ModBanView::list(&mut context.pool(), params).await?, - _ => Default::default(), - }, - match type_ { - All | ModAdd => ModAddView::list(&mut context.pool(), params).await?, - _ => Default::default(), - }, - match type_ { - All | ModRemoveCommunity if other_person_id.is_none() => { - ModRemoveCommunityView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }, - match type_ { - All | AdminPurgePerson if other_person_id.is_none() => { - AdminPurgePersonView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }, - match type_ { - All | AdminPurgeCommunity if other_person_id.is_none() => { - AdminPurgeCommunityView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }, - match type_ { - All | AdminPurgePost if other_person_id.is_none() => { - AdminPurgePostView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }, - match type_ { - All | AdminPurgeComment if other_person_id.is_none() => { - AdminPurgeCommentView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }, - match type_ { - All | AdminBlockInstance if other_person_id.is_none() => { - AdminBlockInstanceView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }, - match type_ { - All | AdminAllowInstance if other_person_id.is_none() => { - AdminAllowInstanceView::list(&mut context.pool(), params).await? - } - _ => Default::default(), - }, - ) - } else { - Default::default() - }; + hide_modlog_names: Some(hide_modlog_names), + page_after, + page_back, + } + .list(&mut context.pool()) + .await?; - // Return the jwt - Ok(Json(GetModlogResponse { - removed_posts, - locked_posts, - featured_posts, - removed_comments, - removed_communities, - banned_from_community, - banned, - added_to_community, - added, - transferred_to_community, - admin_purged_persons, - admin_purged_communities, - admin_purged_posts, - admin_purged_comments, - hidden_communities, - admin_block_instance, - admin_allow_instance, - })) + Ok(Json(GetModlogResponse { modlog })) } diff --git a/crates/api_common/src/build_response.rs b/crates/api_common/src/build_response.rs index b73c0e482f..0245a04597 100644 --- a/crates/api_common/src/build_response.rs +++ b/crates/api_common/src/build_response.rs @@ -12,14 +12,15 @@ use crate::{ }; use actix_web::web::Json; use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, LocalUserId, PostId}, + newtypes::{CommentId, CommunityId, LocalUserId, PostId, PostOrCommentId}, source::{ actor_language::CommunityLanguage, comment::Comment, comment_reply::{CommentReply, CommentReplyInsertForm}, community::Community, person::Person, - person_mention::{PersonMention, PersonMentionInsertForm}, + person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm}, + person_post_mention::{PersonPostMention, PersonPostMentionInsertForm}, post::Post, }, traits::Crud, @@ -94,7 +95,7 @@ pub async fn build_post_response( #[tracing::instrument(skip_all)] pub async fn send_local_notifs( mentions: Vec, - comment_id: CommentId, + post_or_comment_id: PostOrCommentId, person: &Person, do_send_email: bool, context: &LemmyContext, @@ -103,27 +104,42 @@ pub async fn send_local_notifs( let mut recipient_ids = Vec::new(); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); - // When called from api code, we have local user view and can read with CommentView - // to reduce db queries. But when receiving a federated comment the user view is None, - // which means that comments inside private communities cant be read. As a workaround - // we need to read the items manually to bypass this check. - let (comment, post, community) = if let Some(local_user_view) = local_user_view { - let comment_view = CommentView::read( - &mut context.pool(), - comment_id, - Some(&local_user_view.local_user), - ) - .await?; - ( - comment_view.comment, - comment_view.post, - comment_view.community, - ) - } else { - let comment = Comment::read(&mut context.pool(), comment_id).await?; - let post = Post::read(&mut context.pool(), comment.post_id).await?; - let community = Community::read(&mut context.pool(), post.community_id).await?; - (comment, post, community) + let (comment_opt, post, community) = match post_or_comment_id { + PostOrCommentId::Post(post_id) => { + let post_view = PostView::read( + &mut context.pool(), + post_id, + local_user_view.map(|view| &view.local_user), + false, + ) + .await?; + (None, post_view.post, post_view.community) + } + PostOrCommentId::Comment(comment_id) => { + // When called from api code, we have local user view and can read with CommentView + // to reduce db queries. But when receiving a federated comment the user view is None, + // which means that comments inside private communities cant be read. As a workaround + // we need to read the items manually to bypass this check. + if let Some(local_user_view) = local_user_view { + // Read the comment view to get extra info + let comment_view = CommentView::read( + &mut context.pool(), + comment_id, + Some(&local_user_view.local_user), + ) + .await?; + ( + Some(comment_view.comment), + comment_view.post, + comment_view.community, + ) + } else { + let comment = Comment::read(&mut context.pool(), comment_id).await?; + let post = Post::read(&mut context.pool(), comment.post_id).await?; + let community = Community::read(&mut context.pool(), post.community_id).await?; + (Some(comment), post, community) + } + } }; // Send the local mentions @@ -140,22 +156,38 @@ pub async fn send_local_notifs( // below by checking recipient ids recipient_ids.push(mention_user_view.local_user.id); - let user_mention_form = PersonMentionInsertForm { - recipient_id: mention_user_view.person.id, - comment_id, - read: None, - }; + // Make the correct reply form depending on whether its a post or comment mention + let comment_content_or_post_body = if let Some(comment) = &comment_opt { + let person_comment_mention_form = PersonCommentMentionInsertForm { + recipient_id: mention_user_view.person.id, + comment_id: comment.id, + read: None, + }; - // Allow this to fail softly, since comment edits might re-update or replace it - // Let the uniqueness handle this fail - PersonMention::create(&mut context.pool(), &user_mention_form) - .await - .ok(); + // Allow this to fail softly, since comment edits might re-update or replace it + // Let the uniqueness handle this fail + PersonCommentMention::create(&mut context.pool(), &person_comment_mention_form) + .await + .ok(); + comment.content.clone() + } else { + let person_post_mention_form = PersonPostMentionInsertForm { + recipient_id: mention_user_view.person.id, + post_id: post.id, + read: None, + }; + + // Allow this to fail softly, since edits might re-update or replace it + PersonPostMention::create(&mut context.pool(), &person_post_mention_form) + .await + .ok(); + post.body.clone().unwrap_or_default() + }; // Send an email to those local users that have notifications on if do_send_email { let lang = get_interface_language(&mention_user_view); - let content = markdown_to_html(&comment.content); + let content = markdown_to_html(&comment_content_or_post_body); send_email_to_user( &mention_user_view, &lang.notification_mentioned_by_subject(&person.name), @@ -168,99 +200,101 @@ pub async fn send_local_notifs( } // Send comment_reply to the parent commenter / poster - if let Some(parent_comment_id) = comment.parent_comment_id() { - let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; + if let Some(comment) = &comment_opt { + if let Some(parent_comment_id) = comment.parent_comment_id() { + let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; - // Get the parent commenter local_user - let parent_creator_id = parent_comment.creator_id; + // Get the parent commenter local_user + let parent_creator_id = parent_comment.creator_id; - let check_blocks = check_person_instance_community_block( - person.id, - parent_creator_id, - // Only block from the community's instance_id - community.instance_id, - community.id, - &mut context.pool(), - ) - .await - .is_err(); + let check_blocks = check_person_instance_community_block( + person.id, + parent_creator_id, + // Only block from the community's instance_id + community.instance_id, + community.id, + &mut context.pool(), + ) + .await + .is_err(); - // Don't send a notif to yourself - if parent_comment.creator_id != person.id && !check_blocks { - let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; - if let Ok(parent_user_view) = user_view { - // Don't duplicate notif if already mentioned by checking recipient ids - if !recipient_ids.contains(&parent_user_view.local_user.id) { - recipient_ids.push(parent_user_view.local_user.id); + // Don't send a notif to yourself + if parent_comment.creator_id != person.id && !check_blocks { + let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; + if let Ok(parent_user_view) = user_view { + // Don't duplicate notif if already mentioned by checking recipient ids + if !recipient_ids.contains(&parent_user_view.local_user.id) { + recipient_ids.push(parent_user_view.local_user.id); - let comment_reply_form = CommentReplyInsertForm { - recipient_id: parent_user_view.person.id, - comment_id: comment.id, - read: None, - }; + let comment_reply_form = CommentReplyInsertForm { + recipient_id: parent_user_view.person.id, + comment_id: comment.id, + read: None, + }; - // Allow this to fail softly, since comment edits might re-update or replace it - // Let the uniqueness handle this fail - CommentReply::create(&mut context.pool(), &comment_reply_form) - .await - .ok(); + // Allow this to fail softly, since comment edits might re-update or replace it + // Let the uniqueness handle this fail + CommentReply::create(&mut context.pool(), &comment_reply_form) + .await + .ok(); - if do_send_email { - let lang = get_interface_language(&parent_user_view); - let content = markdown_to_html(&comment.content); - send_email_to_user( - &parent_user_view, - &lang.notification_comment_reply_subject(&person.name), - &lang.notification_comment_reply_body(&content, &inbox_link, &person.name), - context.settings(), - ) - .await + if do_send_email { + let lang = get_interface_language(&parent_user_view); + let content = markdown_to_html(&comment.content); + send_email_to_user( + &parent_user_view, + &lang.notification_comment_reply_subject(&person.name), + &lang.notification_comment_reply_body(&content, &inbox_link, &person.name), + context.settings(), + ) + .await + } } } } - } - } else { - // Use the post creator to check blocks - let check_blocks = check_person_instance_community_block( - person.id, - post.creator_id, - // Only block from the community's instance_id - community.instance_id, - community.id, - &mut context.pool(), - ) - .await - .is_err(); + } else { + // Use the post creator to check blocks + let check_blocks = check_person_instance_community_block( + person.id, + post.creator_id, + // Only block from the community's instance_id + community.instance_id, + community.id, + &mut context.pool(), + ) + .await + .is_err(); - if post.creator_id != person.id && !check_blocks { - let creator_id = post.creator_id; - let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; - if let Ok(parent_user_view) = parent_user { - if !recipient_ids.contains(&parent_user_view.local_user.id) { - recipient_ids.push(parent_user_view.local_user.id); + if post.creator_id != person.id && !check_blocks { + let creator_id = post.creator_id; + let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; + if let Ok(parent_user_view) = parent_user { + if !recipient_ids.contains(&parent_user_view.local_user.id) { + recipient_ids.push(parent_user_view.local_user.id); - let comment_reply_form = CommentReplyInsertForm { - recipient_id: parent_user_view.person.id, - comment_id: comment.id, - read: None, - }; + let comment_reply_form = CommentReplyInsertForm { + recipient_id: parent_user_view.person.id, + comment_id: comment.id, + read: None, + }; - // Allow this to fail softly, since comment edits might re-update or replace it - // Let the uniqueness handle this fail - CommentReply::create(&mut context.pool(), &comment_reply_form) - .await - .ok(); + // Allow this to fail softly, since comment edits might re-update or replace it + // Let the uniqueness handle this fail + CommentReply::create(&mut context.pool(), &comment_reply_form) + .await + .ok(); - if do_send_email { - let lang = get_interface_language(&parent_user_view); - let content = markdown_to_html(&comment.content); - send_email_to_user( - &parent_user_view, - &lang.notification_post_reply_subject(&person.name), - &lang.notification_post_reply_body(&content, &inbox_link, &person.name), - context.settings(), - ) - .await + if do_send_email { + let lang = get_interface_language(&parent_user_view); + let content = markdown_to_html(&comment.content); + send_email_to_user( + &parent_user_view, + &lang.notification_post_reply_subject(&person.name), + &lang.notification_post_reply_body(&content, &inbox_link, &person.name), + context.settings(), + ) + .await + } } } } diff --git a/crates/api_common/src/comment.rs b/crates/api_common/src/comment.rs index e083657893..0d416e9f05 100644 --- a/crates/api_common/src/comment.rs +++ b/crates/api_common/src/comment.rs @@ -1,9 +1,9 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommentReportId, CommunityId, LanguageId, LocalUserId, PostId}, + newtypes::{CommentId, CommunityId, LanguageId, LocalUserId, PostId}, CommentSortType, ListingType, }; -use lemmy_db_views::structs::{CommentReportView, CommentView, VoteView}; +use lemmy_db_views::structs::{CommentView, VoteView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -131,8 +131,6 @@ pub struct GetComments { #[cfg_attr(feature = "full", ts(optional))] pub parent_id: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, @@ -146,60 +144,6 @@ pub struct GetCommentsResponse { pub comments: Vec, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Report a comment. -pub struct CreateCommentReport { - pub comment_id: CommentId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The comment report response. -pub struct CommentReportResponse { - pub comment_report_view: CommentReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a comment report (only doable by mods). -pub struct ResolveCommentReport { - pub report_id: CommentReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List comment reports. -pub struct ListCommentReports { - #[cfg_attr(feature = "full", ts(optional))] - pub comment_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, - /// if no community is given, it returns reports for all communities moderated by the auth user - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The comment report list response. -pub struct ListCommentReportsResponse { - pub comment_reports: Vec, -} - #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs index 6e09d904d6..8af1dec25c 100644 --- a/crates/api_common/src/lib.rs +++ b/crates/api_common/src/lib.rs @@ -11,6 +11,7 @@ pub mod oauth_provider; pub mod person; pub mod post; pub mod private_message; +pub mod reports; #[cfg(feature = "full")] pub mod request; #[cfg(feature = "full")] diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index b95cf5e774..a52fc26163 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -1,17 +1,33 @@ use lemmy_db_schema::{ - newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, + newtypes::{ + CommentReplyId, + CommunityId, + LanguageId, + PersonCommentMentionId, + PersonId, + PersonPostMentionId, + }, sensitive::SensitiveString, source::{login_token::LoginToken, site::Site}, CommentSortType, + InboxDataType, ListingType, PostListingMode, PostSortType, }; -use lemmy_db_views::structs::{CommentView, LocalImageView, PostView}; +use lemmy_db_views::structs::{ + LocalImageView, + PersonContentCombinedPaginationCursor, + PersonContentCombinedView, + PersonSavedCombinedPaginationCursor, +}; use lemmy_db_views_actor::structs::{ CommentReplyView, CommunityModeratorView, - PersonMentionView, + InboxCombinedPaginationCursor, + InboxCombinedView, + PersonCommentMentionView, + PersonPostMentionView, PersonView, }; use serde::{Deserialize, Serialize}; @@ -222,16 +238,6 @@ pub struct GetPersonDetails { /// Example: dessalines , or dessalines@xyz.tld #[cfg_attr(feature = "full", ts(optional))] pub username: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub sort: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, } #[skip_serializing_none] @@ -243,11 +249,58 @@ pub struct GetPersonDetailsResponse { pub person_view: PersonView, #[cfg_attr(feature = "full", ts(optional))] pub site: Option, - pub comments: Vec, - pub posts: Vec, pub moderates: Vec, } +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Gets a person's content (posts and comments) +/// +/// Either person_id, or username are required. +pub struct ListPersonContent { + #[cfg_attr(feature = "full", ts(optional))] + pub person_id: Option, + /// Example: dessalines , or dessalines@xyz.tld + #[cfg_attr(feature = "full", ts(optional))] + pub username: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A person's content response. +pub struct ListPersonContentResponse { + pub content: Vec, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Gets your saved posts and comments +pub struct ListPersonSaved { + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, +} + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A person's saved content response. +pub struct ListPersonSavedResponse { + pub saved: Vec, +} + #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] @@ -323,60 +376,52 @@ pub struct BlockPersonResponse { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// Get comment replies. -pub struct GetReplies { +/// Get your inbox (replies, comment mentions, post mentions, and messages) +pub struct ListInbox { #[cfg_attr(feature = "full", ts(optional))] - pub sort: Option, + pub type_: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, + pub unread_only: Option, #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, + pub page_cursor: Option, #[cfg_attr(feature = "full", ts(optional))] - pub unread_only: Option, + pub page_back: Option, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// Fetches your replies. -// TODO, replies and mentions below should be redone as tagged enums. -pub struct GetRepliesResponse { - pub replies: Vec, +/// Get your inbox (replies, comment mentions, post mentions, and messages) +pub struct ListInboxResponse { + pub inbox: Vec, } -#[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// Get mentions for your user. -pub struct GetPersonMentions { - #[cfg_attr(feature = "full", ts(optional))] - pub sort: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub unread_only: Option, +/// Mark a person mention as read. +pub struct MarkPersonCommentMentionAsRead { + pub person_comment_mention_id: PersonCommentMentionId, + pub read: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// The response of mentions for your user. -pub struct GetPersonMentionsResponse { - pub mentions: Vec, +/// The response for a person mention action. +pub struct PersonCommentMentionResponse { + pub person_comment_mention_view: PersonCommentMentionView, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Mark a person mention as read. -pub struct MarkPersonMentionAsRead { - pub person_mention_id: PersonMentionId, +pub struct MarkPersonPostMentionAsRead { + pub person_post_mention_id: PersonPostMentionId, pub read: bool, } @@ -384,8 +429,8 @@ pub struct MarkPersonMentionAsRead { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// The response for a person mention action. -pub struct PersonMentionResponse { - pub person_mention_view: PersonMentionView, +pub struct PersonPostMentionResponse { + pub person_post_mention_view: PersonPostMentionView, } #[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] @@ -448,22 +493,15 @@ pub struct GetReportCount { #[cfg_attr(feature = "full", ts(export))] /// A response for the number of reports. pub struct GetReportCountResponse { - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - pub comment_reports: i64, - pub post_reports: i64, - #[cfg_attr(feature = "full", ts(optional))] - pub private_message_reports: Option, + pub count: i64, } #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// A response containing counts for your notifications. +/// A response containing a count of unread notifications. pub struct GetUnreadCountResponse { - pub replies: i64, - pub mentions: i64, - pub private_messages: i64, + pub count: i64, } #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, Hash)] diff --git a/crates/api_common/src/post.rs b/crates/api_common/src/post.rs index 405de3a92d..81cd7363be 100644 --- a/crates/api_common/src/post.rs +++ b/crates/api_common/src/post.rs @@ -1,10 +1,10 @@ use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId, PostReportId}, + newtypes::{CommentId, CommunityId, DbUrl, LanguageId, PostId}, ListingType, PostFeatureType, PostSortType, }; -use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView}; +use lemmy_db_views::structs::{PaginationCursor, PostView, VoteView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -95,8 +95,6 @@ pub struct GetPosts { #[cfg_attr(feature = "full", ts(optional))] pub community_name: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, @@ -116,6 +114,8 @@ pub struct GetPosts { pub no_comments_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, } #[skip_serializing_none] @@ -247,61 +247,6 @@ pub struct SavePost { pub save: bool, } -#[derive(Debug, Serialize, Deserialize, Clone, Default)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Create a post report. -pub struct CreatePostReport { - pub post_id: PostId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The post report response. -pub struct PostReportResponse { - pub post_report_view: PostReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a post report (mods only). -pub struct ResolvePostReport { - pub report_id: PostReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List post reports. -pub struct ListPostReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, - // TODO make into tagged enum at some point - /// if no community is given, it returns reports for all communities moderated by the auth user - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub post_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The post reports response. -pub struct ListPostReportsResponse { - pub post_reports: Vec, -} - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/api_common/src/private_message.rs b/crates/api_common/src/private_message.rs index 666fe38659..f8134ea274 100644 --- a/crates/api_common/src/private_message.rs +++ b/crates/api_common/src/private_message.rs @@ -1,7 +1,6 @@ -use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId, PrivateMessageReportId}; -use lemmy_db_views::structs::{PrivateMessageReportView, PrivateMessageView}; +use lemmy_db_schema::newtypes::{PersonId, PrivateMessageId}; +use lemmy_db_views_actor::structs::PrivateMessageView; use serde::{Deserialize, Serialize}; -use serde_with::skip_serializing_none; #[cfg(feature = "full")] use ts_rs::TS; @@ -41,30 +40,6 @@ pub struct MarkPrivateMessageAsRead { pub read: bool, } -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Get your private messages. -pub struct GetPrivateMessages { - #[cfg_attr(feature = "full", ts(optional))] - pub unread_only: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub creator_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The private messages response. -pub struct PrivateMessagesResponse { - pub private_messages: Vec, -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] @@ -72,53 +47,3 @@ pub struct PrivateMessagesResponse { pub struct PrivateMessageResponse { pub private_message_view: PrivateMessageView, } - -#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Create a report for a private message. -pub struct CreatePrivateMessageReport { - pub private_message_id: PrivateMessageId, - pub reason: String, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// A private message report response. -pub struct PrivateMessageReportResponse { - pub private_message_report_view: PrivateMessageReportView, -} - -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// Resolve a private message report. -pub struct ResolvePrivateMessageReport { - pub report_id: PrivateMessageReportId, - pub resolved: bool, -} - -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// List private message reports. -// TODO , perhaps GetReports should be a tagged enum list too. -pub struct ListPrivateMessageReports { - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - /// Only shows the unresolved reports - #[cfg_attr(feature = "full", ts(optional))] - pub unresolved_only: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The response for list private message reports. -pub struct ListPrivateMessageReportsResponse { - pub private_message_reports: Vec, -} diff --git a/crates/api_common/src/reports/combined.rs b/crates/api_common/src/reports/combined.rs new file mode 100644 index 0000000000..69d9288305 --- /dev/null +++ b/crates/api_common/src/reports/combined.rs @@ -0,0 +1,32 @@ +use lemmy_db_schema::newtypes::CommunityId; +use lemmy_db_views::structs::{ReportCombinedPaginationCursor, ReportCombinedView}; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// List reports. +pub struct ListReports { + /// Only shows the unresolved reports + #[cfg_attr(feature = "full", ts(optional))] + pub unresolved_only: Option, + /// if no community is given, it returns reports for all communities moderated by the auth user + #[cfg_attr(feature = "full", ts(optional))] + pub community_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The post reports response. +pub struct ListReportsResponse { + pub reports: Vec, +} diff --git a/crates/api_common/src/reports/comment.rs b/crates/api_common/src/reports/comment.rs new file mode 100644 index 0000000000..d1a51a6a85 --- /dev/null +++ b/crates/api_common/src/reports/comment.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{CommentId, CommentReportId}; +use lemmy_db_views::structs::CommentReportView; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Report a comment. +pub struct CreateCommentReport { + pub comment_id: CommentId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The comment report response. +pub struct CommentReportResponse { + pub comment_report_view: CommentReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a comment report (only doable by mods). +pub struct ResolveCommentReport { + pub report_id: CommentReportId, + pub resolved: bool, +} diff --git a/crates/api_common/src/reports/mod.rs b/crates/api_common/src/reports/mod.rs new file mode 100644 index 0000000000..6584de1bc4 --- /dev/null +++ b/crates/api_common/src/reports/mod.rs @@ -0,0 +1,4 @@ +pub mod combined; +pub mod comment; +pub mod post; +pub mod private_message; diff --git a/crates/api_common/src/reports/post.rs b/crates/api_common/src/reports/post.rs new file mode 100644 index 0000000000..a4d20d575a --- /dev/null +++ b/crates/api_common/src/reports/post.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{PostId, PostReportId}; +use lemmy_db_views::structs::PostReportView; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a post report. +pub struct CreatePostReport { + pub post_id: PostId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The post report response. +pub struct PostReportResponse { + pub post_report_view: PostReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a post report (mods only). +pub struct ResolvePostReport { + pub report_id: PostReportId, + pub resolved: bool, +} diff --git a/crates/api_common/src/reports/private_message.rs b/crates/api_common/src/reports/private_message.rs new file mode 100644 index 0000000000..5fd401564c --- /dev/null +++ b/crates/api_common/src/reports/private_message.rs @@ -0,0 +1,31 @@ +use lemmy_db_schema::newtypes::{PrivateMessageId, PrivateMessageReportId}; +use lemmy_db_views::structs::PrivateMessageReportView; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Create a report for a private message. +pub struct CreatePrivateMessageReport { + pub private_message_id: PrivateMessageId, + pub reason: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A private message report response. +pub struct PrivateMessageReportResponse { + pub private_message_report_view: PrivateMessageReportView, +} + +#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Resolve a private message report. +pub struct ResolvePrivateMessageReport { + pub report_id: PrivateMessageReportId, + pub resolved: bool, +} diff --git a/crates/api_common/src/send_activity.rs b/crates/api_common/src/send_activity.rs index b606c9a901..07203ffe40 100644 --- a/crates/api_common/src/send_activity.rs +++ b/crates/api_common/src/send_activity.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ private_message::PrivateMessage, }, }; -use lemmy_db_views::structs::PrivateMessageView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::error::LemmyResult; use std::sync::{LazyLock, OnceLock}; use tokio::{ diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 7f1000b14e..5f86ae3b6b 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -42,25 +42,7 @@ use lemmy_db_views_actor::structs::{ CommunityView, PersonView, }; -use lemmy_db_views_moderator::structs::{ - AdminAllowInstanceView, - AdminBlockInstanceView, - AdminPurgeCommentView, - AdminPurgeCommunityView, - AdminPurgePersonView, - AdminPurgePostView, - ModAddCommunityView, - ModAddView, - ModBanFromCommunityView, - ModBanView, - ModFeaturePostView, - ModHideCommunityView, - ModLockPostView, - ModRemoveCommentView, - ModRemoveCommunityView, - ModRemovePostView, - ModTransferCommunityView, -}; +use lemmy_db_views_moderator::structs::{ModlogCombinedPaginationCursor, ModlogCombinedView}; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; #[cfg(feature = "full")] @@ -94,8 +76,6 @@ pub struct Search { #[cfg_attr(feature = "full", ts(optional))] pub post_url_only: Option, #[cfg_attr(feature = "full", ts(optional))] - pub saved_only: Option, - #[cfg_attr(feature = "full", ts(optional))] pub liked_only: Option, #[cfg_attr(feature = "full", ts(optional))] pub disliked_only: Option, @@ -141,7 +121,7 @@ pub struct ResolveObjectResponse { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Fetches the modlog. @@ -151,17 +131,17 @@ pub struct GetModlog { #[cfg_attr(feature = "full", ts(optional))] pub community_id: Option, #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - #[cfg_attr(feature = "full", ts(optional))] pub type_: Option, #[cfg_attr(feature = "full", ts(optional))] - pub other_person_id: Option, + pub modded_person_id: Option, #[cfg_attr(feature = "full", ts(optional))] pub post_id: Option, #[cfg_attr(feature = "full", ts(optional))] pub comment_id: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_cursor: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub page_back: Option, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -170,23 +150,7 @@ pub struct GetModlog { /// The modlog fetch response. // TODO this should be redone as a list of tagged enums pub struct GetModlogResponse { - pub removed_posts: Vec, - pub locked_posts: Vec, - pub featured_posts: Vec, - pub removed_comments: Vec, - pub removed_communities: Vec, - pub banned_from_community: Vec, - pub banned: Vec, - pub added_to_community: Vec, - pub transferred_to_community: Vec, - pub added: Vec, - pub admin_purged_persons: Vec, - pub admin_purged_communities: Vec, - pub admin_purged_posts: Vec, - pub admin_purged_comments: Vec, - pub hidden_communities: Vec, - pub admin_block_instance: Vec, - pub admin_allow_instance: Vec, + pub modlog: Vec, } #[skip_serializing_none] diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index 21154c823c..7b3ed820ef 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -11,7 +11,7 @@ use chrono::{DateTime, Days, Local, TimeZone, Utc}; use enum_map::{enum_map, EnumMap}; use lemmy_db_schema::{ aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm}, - newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId}, + newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId, PostOrCommentId}, source::{ comment::{Comment, CommentLike, CommentUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm}, @@ -293,23 +293,17 @@ pub async fn check_person_instance_community_block( Ok(()) } -/// A vote item type used to check the vote mode. -pub enum VoteItem { - Post(PostId), - Comment(CommentId), -} - #[tracing::instrument(skip_all)] pub async fn check_local_vote_mode( score: i16, - vote_item: VoteItem, + post_or_comment_id: PostOrCommentId, local_site: &LocalSite, person_id: PersonId, pool: &mut DbPool<'_>, ) -> LemmyResult<()> { - let (downvote_setting, upvote_setting) = match vote_item { - VoteItem::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), - VoteItem::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), + let (downvote_setting, upvote_setting) = match post_or_comment_id { + PostOrCommentId::Post(_) => (local_site.post_downvotes, local_site.post_upvotes), + PostOrCommentId::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes), }; let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable; @@ -317,9 +311,11 @@ pub async fn check_local_vote_mode( // Undo previous vote for item if new vote fails if downvote_fail || upvote_fail { - match vote_item { - VoteItem::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?, - VoteItem::Comment(comment_id) => CommentLike::remove(pool, person_id, comment_id).await?, + match post_or_comment_id { + PostOrCommentId::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?, + PostOrCommentId::Comment(comment_id) => { + CommentLike::remove(pool, person_id, comment_id).await? + } }; } Ok(()) @@ -1187,16 +1183,18 @@ fn build_proxied_image_url( mod tests { use super::*; - use lemmy_db_schema::source::{ - comment::CommentInsertForm, - community::CommunityInsertForm, - person::PersonInsertForm, - post::PostInsertForm, + use lemmy_db_schema::{ + source::{ + comment::CommentInsertForm, + community::CommunityInsertForm, + person::PersonInsertForm, + post::PostInsertForm, + }, + ModlogActionType, }; - use lemmy_db_views_moderator::structs::{ - ModRemoveCommentView, - ModRemovePostView, - ModlogListParams, + use lemmy_db_views_moderator::{ + modlog_combined_view::ModlogCombinedQuery, + structs::ModlogCombinedView, }; use pretty_assertions::assert_eq; use serial_test::serial; @@ -1332,45 +1330,58 @@ mod tests { .await?; // Verify that their posts and comments are removed. - let params = ModlogListParams { - community_id: None, - mod_person_id: None, - other_person_id: None, - post_id: None, - comment_id: None, - page: None, - limit: None, - hide_modlog_names: false, - }; - // Posts - let post_modlog = ModRemovePostView::list(pool, params).await?; + let post_modlog = ModlogCombinedQuery { + type_: Some(ModlogActionType::ModRemovePost), + ..Default::default() + } + .list(pool) + .await?; assert_eq!(2, post_modlog.len()); - let mod_removed_posts = post_modlog - .iter() + let posts_mapped = &post_modlog.iter().filter_map(|p| { + if let ModlogCombinedView::ModRemovePost(v) = p { + Some(v) + } else { + None + } + }); + let mod_removed_posts = posts_mapped + .clone() .map(|p| p.mod_remove_post.removed) .collect::>(); assert_eq!(vec![true, true], mod_removed_posts); - let removed_posts = post_modlog - .iter() + let removed_posts = posts_mapped + .clone() .map(|p| p.post.removed) .collect::>(); assert_eq!(vec![true, true], removed_posts); // Comments - let comment_modlog = ModRemoveCommentView::list(pool, params).await?; + let comment_modlog = ModlogCombinedQuery { + type_: Some(ModlogActionType::ModRemoveComment), + ..Default::default() + } + .list(pool) + .await?; assert_eq!(2, comment_modlog.len()); - let mod_removed_comments = comment_modlog - .iter() + let comments_mapped = &comment_modlog.iter().filter_map(|c| { + if let ModlogCombinedView::ModRemoveComment(v) = c { + Some(v) + } else { + None + } + }); + let mod_removed_comments = comments_mapped + .clone() .map(|p| p.mod_remove_comment.removed) .collect::>(); assert_eq!(vec![true, true], mod_removed_comments); - let removed_comments = comment_modlog - .iter() + let removed_comments = comments_mapped + .clone() .map(|p| p.comment.removed) .collect::>(); assert_eq!(vec![true, true], removed_comments); @@ -1386,34 +1397,60 @@ mod tests { .await?; // Posts - let post_modlog = ModRemovePostView::list(pool, params).await?; + let post_modlog = ModlogCombinedQuery { + type_: Some(ModlogActionType::ModRemovePost), + ..Default::default() + } + .list(pool) + .await?; assert_eq!(4, post_modlog.len()); - let mod_restored_posts = post_modlog - .iter() + let posts_mapped = &post_modlog.iter().filter_map(|p| { + if let ModlogCombinedView::ModRemovePost(v) = p { + Some(v) + } else { + None + } + }); + + let mod_restored_posts = posts_mapped + .clone() .map(|p| p.mod_remove_post.removed) .collect::>(); assert_eq!(vec![false, false, true, true], mod_restored_posts); - let restored_posts = post_modlog - .iter() + let restored_posts = posts_mapped + .clone() .map(|p| p.post.removed) .collect::>(); // All of these will be false, cause its the current state of the post assert_eq!(vec![false, false, false, false], restored_posts); // Comments - let comment_modlog = ModRemoveCommentView::list(pool, params).await?; + let comment_modlog = ModlogCombinedQuery { + type_: Some(ModlogActionType::ModRemoveComment), + ..Default::default() + } + .list(pool) + .await?; assert_eq!(4, comment_modlog.len()); - let mod_restored_comments = comment_modlog - .iter() + let comments_mapped = &comment_modlog.iter().filter_map(|c| { + if let ModlogCombinedView::ModRemoveComment(v) = c { + Some(v) + } else { + None + } + }); + + let mod_restored_comments = comments_mapped + .clone() .map(|p| p.mod_remove_comment.removed) .collect::>(); assert_eq!(vec![false, false, true, true], mod_restored_comments); - let restored_comments = comment_modlog - .iter() + let restored_comments = comments_mapped + .clone() .map(|p| p.comment.removed) .collect::>(); assert_eq!(vec![false, false, false, false], restored_comments); diff --git a/crates/api_crud/src/comment/create.rs b/crates/api_crud/src/comment/create.rs index edcf7db30f..692b85c17d 100644 --- a/crates/api_crud/src/comment/create.rs +++ b/crates/api_crud/src/comment/create.rs @@ -17,11 +17,12 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::PostOrCommentId, source::{ comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm}, comment_reply::{CommentReply, CommentReplyUpdateForm}, local_site::LocalSite, - person_mention::{PersonMention, PersonMentionUpdateForm}, + person_comment_mention::{PersonCommentMention, PersonCommentMentionUpdateForm}, }, traits::{Crud, Likeable}, }; @@ -117,7 +118,7 @@ pub async fn create_comment( let mentions = scrape_text_for_mentions(&content); let recipient_ids = send_local_notifs( mentions, - inserted_comment_id, + PostOrCommentId::Comment(inserted_comment_id), &local_user_view.person, true, &context, @@ -169,17 +170,18 @@ pub async fn create_comment( .with_lemmy_type(LemmyErrorType::CouldntUpdateReplies)?; } - // If the parent has PersonMentions mark them as read too - let person_mention = - PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; - if let Ok(Some(mention)) = person_mention { - PersonMention::update( + // If the parent has PersonCommentMentions mark them as read too + let person_comment_mention = + PersonCommentMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id) + .await; + if let Ok(Some(mention)) = person_comment_mention { + PersonCommentMention::update( &mut context.pool(), mention.id, - &PersonMentionUpdateForm { read: Some(true) }, + &PersonCommentMentionUpdateForm { read: Some(true) }, ) .await - .with_lemmy_type(LemmyErrorType::CouldntUpdatePersonMentions)?; + .with_lemmy_type(LemmyErrorType::CouldntUpdatePersonCommentMentions)?; } } diff --git a/crates/api_crud/src/comment/delete.rs b/crates/api_crud/src/comment/delete.rs index 60a22a2efc..cf90df6b6c 100644 --- a/crates/api_crud/src/comment/delete.rs +++ b/crates/api_crud/src/comment/delete.rs @@ -8,6 +8,7 @@ use lemmy_api_common::{ utils::check_community_user_action, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::comment::{Comment, CommentUpdateForm}, traits::Crud, }; @@ -60,7 +61,7 @@ pub async fn delete_comment( let recipient_ids = send_local_notifs( vec![], - comment_id, + PostOrCommentId::Comment(comment_id), &local_user_view.person, false, &context, diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 1ac6201e83..4436f8c87d 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -8,6 +8,7 @@ use lemmy_api_common::{ utils::check_community_mod_action, }; use lemmy_db_schema::{ + newtypes::PostOrCommentId, source::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, @@ -82,7 +83,7 @@ pub async fn remove_comment( let recipient_ids = send_local_notifs( vec![], - comment_id, + PostOrCommentId::Comment(comment_id), &local_user_view.person, false, &context, diff --git a/crates/api_crud/src/comment/update.rs b/crates/api_crud/src/comment/update.rs index 1af026204f..3cb1a3a4e1 100644 --- a/crates/api_crud/src/comment/update.rs +++ b/crates/api_crud/src/comment/update.rs @@ -15,6 +15,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::PostOrCommentId, source::{ comment::{Comment, CommentUpdateForm}, local_site::LocalSite, @@ -86,7 +87,7 @@ pub async fn update_comment( let mentions = scrape_text_for_mentions(&updated_comment_content); let recipient_ids = send_local_notifs( mentions, - comment_id, + PostOrCommentId::Comment(comment_id), &local_user_view.person, false, &context, diff --git a/crates/api_crud/src/post/create.rs b/crates/api_crud/src/post/create.rs index 948a7617ed..860e82f447 100644 --- a/crates/api_crud/src/post/create.rs +++ b/crates/api_crud/src/post/create.rs @@ -2,7 +2,7 @@ use super::convert_published_time; use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ - build_response::build_post_response, + build_response::{build_post_response, send_local_notifs}, context::LemmyContext, post::{CreatePost, PostResponse}, request::generate_post_link_metadata, @@ -17,6 +17,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::PostOrCommentId, source::{ community::Community, local_site::LocalSite, @@ -32,6 +33,7 @@ use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, spawn_try_task, utils::{ + mention::scrape_text_for_mentions, slurs::check_slurs, validation::{ is_url_blocked, @@ -148,6 +150,18 @@ pub async fn create_post( .await .with_lemmy_type(LemmyErrorType::CouldntLikePost)?; + // Scan the post body for user mentions, add those rows + let mentions = scrape_text_for_mentions(&inserted_post.body.clone().unwrap_or_default()); + send_local_notifs( + mentions, + PostOrCommentId::Post(inserted_post.id), + &local_user_view.person, + true, + &context, + Some(&local_user_view), + ) + .await?; + let read_form = PostReadForm::new(post_id, person_id); PostRead::mark_as_read(&mut context.pool(), &read_form).await?; diff --git a/crates/api_crud/src/post/update.rs b/crates/api_crud/src/post/update.rs index 24bbed0092..a93708b223 100644 --- a/crates/api_crud/src/post/update.rs +++ b/crates/api_crud/src/post/update.rs @@ -3,7 +3,7 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use chrono::Utc; use lemmy_api_common::{ - build_response::build_post_response, + build_response::{build_post_response, send_local_notifs}, context::LemmyContext, post::{EditPost, PostResponse}, request::generate_post_link_metadata, @@ -17,6 +17,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ impls::actor_language::validate_post_language, + newtypes::PostOrCommentId, source::{ community::Community, local_site::LocalSite, @@ -29,6 +30,7 @@ use lemmy_db_views::structs::{LocalUserView, PostView}; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{ + mention::scrape_text_for_mentions, slurs::check_slurs, validation::{ is_url_blocked, @@ -142,6 +144,18 @@ pub async fn update_post( .await .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; + // Scan the post body for user mentions, add those rows + let mentions = scrape_text_for_mentions(&updated_post.body.clone().unwrap_or_default()); + send_local_notifs( + mentions, + PostOrCommentId::Post(updated_post.id), + &local_user_view.person, + false, + &context, + Some(&local_user_view), + ) + .await?; + // send out federation/webmention if necessary match ( orig_post.post.scheduled_publish_time, diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs index 1a6a78d003..fd95a2b9ed 100644 --- a/crates/api_crud/src/private_message/create.rs +++ b/crates/api_crud/src/private_message/create.rs @@ -21,7 +21,8 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::{markdown::markdown_to_html, validation::is_valid_body_field}, diff --git a/crates/api_crud/src/private_message/delete.rs b/crates/api_crud/src/private_message/delete.rs index 30efc020ca..d06c8bc041 100644 --- a/crates/api_crud/src/private_message/delete.rs +++ b/crates/api_crud/src/private_message/delete.rs @@ -9,7 +9,8 @@ use lemmy_db_schema::{ source::private_message::{PrivateMessage, PrivateMessageUpdateForm}, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] diff --git a/crates/api_crud/src/private_message/mod.rs b/crates/api_crud/src/private_message/mod.rs index ab7fa4390b..fdb2f55613 100644 --- a/crates/api_crud/src/private_message/mod.rs +++ b/crates/api_crud/src/private_message/mod.rs @@ -1,4 +1,3 @@ pub mod create; pub mod delete; -pub mod read; pub mod update; diff --git a/crates/api_crud/src/private_message/read.rs b/crates/api_crud/src/private_message/read.rs deleted file mode 100644 index 7558b97fc7..0000000000 --- a/crates/api_crud/src/private_message/read.rs +++ /dev/null @@ -1,33 +0,0 @@ -use actix_web::web::{Data, Json, Query}; -use lemmy_api_common::{ - context::LemmyContext, - private_message::{GetPrivateMessages, PrivateMessagesResponse}, -}; -use lemmy_db_views::{private_message_view::PrivateMessageQuery, structs::LocalUserView}; -use lemmy_utils::error::LemmyResult; - -#[tracing::instrument(skip(context))] -pub async fn get_private_message( - data: Query, - context: Data, - local_user_view: LocalUserView, -) -> LemmyResult> { - let person_id = local_user_view.person.id; - - let page = data.page; - let limit = data.limit; - let unread_only = data.unread_only.unwrap_or_default(); - let creator_id = data.creator_id; - let messages = PrivateMessageQuery { - page, - limit, - unread_only, - creator_id, - } - .list(&mut context.pool(), person_id) - .await?; - - Ok(Json(PrivateMessagesResponse { - private_messages: messages, - })) -} diff --git a/crates/api_crud/src/private_message/update.rs b/crates/api_crud/src/private_message/update.rs index b9e4785efe..22c1da4a24 100644 --- a/crates/api_crud/src/private_message/update.rs +++ b/crates/api_crud/src/private_message/update.rs @@ -14,7 +14,8 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{LocalUserView, PrivateMessageView}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::{ error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, utils::validation::is_valid_body_field, diff --git a/crates/apub/src/activities/create_or_update/comment.rs b/crates/apub/src/activities/create_or_update/comment.rs index 72dae48b71..ba617e2dc3 100644 --- a/crates/apub/src/activities/create_or_update/comment.rs +++ b/crates/apub/src/activities/create_or_update/comment.rs @@ -29,7 +29,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::CommentAggregates, - newtypes::PersonId, + newtypes::{PersonId, PostOrCommentId}, source::{ activity::ActivitySendTargets, comment::{Comment, CommentLike, CommentLikeForm}, @@ -176,10 +176,17 @@ impl ActivityHandler for CreateOrUpdateNote { // TODO: for compatibility with other projects, it would be much better to read this from cc or // tags let mentions = scrape_text_for_mentions(&comment.content); - // TODO: this fails in local community comment as CommentView::read() returns nothing // without passing LocalUser - send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?; + send_local_notifs( + mentions, + PostOrCommentId::Comment(comment.id), + &actor, + do_send_email, + context, + None, + ) + .await?; Ok(()) } } diff --git a/crates/apub/src/activities/create_or_update/post.rs b/crates/apub/src/activities/create_or_update/post.rs index 832b2da6d4..f661d39b78 100644 --- a/crates/apub/src/activities/create_or_update/post.rs +++ b/crates/apub/src/activities/create_or_update/post.rs @@ -20,10 +20,10 @@ use activitypub_federation::{ protocol::verification::{verify_domains_match, verify_urls_match}, traits::{ActivityHandler, Actor, Object}, }; -use lemmy_api_common::context::LemmyContext; +use lemmy_api_common::{build_response::send_local_notifs, context::LemmyContext}; use lemmy_db_schema::{ aggregates::structs::PostAggregates, - newtypes::PersonId, + newtypes::{PersonId, PostOrCommentId}, source::{ activity::ActivitySendTargets, community::Community, @@ -32,7 +32,10 @@ use lemmy_db_schema::{ }, traits::{Crud, Likeable}, }; -use lemmy_utils::error::{LemmyError, LemmyResult}; +use lemmy_utils::{ + error::{LemmyError, LemmyResult}, + utils::mention::scrape_text_for_mentions, +}; use url::Url; impl CreateOrUpdatePage { @@ -124,6 +127,21 @@ impl ActivityHandler for CreateOrUpdatePage { // Calculate initial hot_rank for post PostAggregates::update_ranks(&mut context.pool(), post.id).await?; + let do_send_email = self.kind == CreateOrUpdateType::Create; + let actor = self.actor.dereference(context).await?; + + // Send the post body mentions + let mentions = scrape_text_for_mentions(&post.body.clone().unwrap_or_default()); + send_local_notifs( + mentions, + PostOrCommentId::Post(post.id), + &actor, + do_send_email, + context, + None, + ) + .await?; + Ok(()) } } diff --git a/crates/apub/src/activities/create_or_update/private_message.rs b/crates/apub/src/activities/create_or_update/private_message.rs index b6e7478efa..ce04a93301 100644 --- a/crates/apub/src/activities/create_or_update/private_message.rs +++ b/crates/apub/src/activities/create_or_update/private_message.rs @@ -14,7 +14,7 @@ use activitypub_federation::{ }; use lemmy_api_common::context::LemmyContext; use lemmy_db_schema::source::activity::ActivitySendTargets; -use lemmy_db_views::structs::PrivateMessageView; +use lemmy_db_views_actor::structs::PrivateMessageView; use lemmy_utils::error::{LemmyError, LemmyResult}; use url::Url; diff --git a/crates/apub/src/api/list_comments.rs b/crates/apub/src/api/list_comments.rs index 3e7a2f4ebe..2411b874a9 100644 --- a/crates/apub/src/api/list_comments.rs +++ b/crates/apub/src/api/list_comments.rs @@ -46,7 +46,6 @@ pub async fn list_comments( &site_view.local_site, )); let max_depth = data.max_depth; - let saved_only = data.saved_only; let liked_only = data.liked_only; let disliked_only = data.disliked_only; @@ -80,7 +79,6 @@ pub async fn list_comments( listing_type, sort, max_depth, - saved_only, liked_only, disliked_only, community_id, diff --git a/crates/apub/src/api/list_person_content.rs b/crates/apub/src/api/list_person_content.rs new file mode 100644 index 0000000000..477e62e851 --- /dev/null +++ b/crates/apub/src/api/list_person_content.rs @@ -0,0 +1,50 @@ +use super::resolve_person_id_from_id_or_username; +use activitypub_federation::config::Data; +use actix_web::web::{Json, Query}; +use lemmy_api_common::{ + context::LemmyContext, + person::{ListPersonContent, ListPersonContentResponse}, + utils::check_private_instance, +}; +use lemmy_db_views::{ + person_content_combined_view::PersonContentCombinedQuery, + structs::{LocalUserView, SiteView}, +}; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn list_person_content( + data: Query, + context: Data, + local_user_view: Option, +) -> LemmyResult> { + let local_site = SiteView::read_local(&mut context.pool()).await?; + + check_private_instance(&local_user_view, &local_site.local_site)?; + + let person_details_id = resolve_person_id_from_id_or_username( + &data.person_id, + &data.username, + &context, + &local_user_view, + ) + .await?; + + // parse pagination token + let page_after = if let Some(pa) = &data.page_cursor { + Some(pa.read(&mut context.pool()).await?) + } else { + None + }; + let page_back = data.page_back; + + let content = PersonContentCombinedQuery { + creator_id: person_details_id, + page_after, + page_back, + } + .list(&mut context.pool(), &local_user_view) + .await?; + + Ok(Json(ListPersonContentResponse { content })) +} diff --git a/crates/apub/src/api/list_posts.rs b/crates/apub/src/api/list_posts.rs index 63e737fdd0..6d043ae4f3 100644 --- a/crates/apub/src/api/list_posts.rs +++ b/crates/apub/src/api/list_posts.rs @@ -41,7 +41,6 @@ pub async fn list_posts( } else { data.community_id }; - let saved_only = data.saved_only; let show_hidden = data.show_hidden; let show_read = data.show_read; let show_nsfw = data.show_nsfw; @@ -77,7 +76,6 @@ pub async fn list_posts( listing_type, sort, community_id, - saved_only, liked_only, disliked_only, page, diff --git a/crates/apub/src/api/mod.rs b/crates/apub/src/api/mod.rs index 580be32286..9359eabc4b 100644 --- a/crates/apub/src/api/mod.rs +++ b/crates/apub/src/api/mod.rs @@ -1,12 +1,18 @@ +use crate::{fetcher::resolve_actor_identifier, objects::person::ApubPerson}; +use activitypub_federation::config::Data; +use lemmy_api_common::{context::LemmyContext, LemmyErrorType}; use lemmy_db_schema::{ - newtypes::CommunityId, - source::{local_site::LocalSite, local_user::LocalUser}, + newtypes::{CommunityId, PersonId}, + source::{local_site::LocalSite, local_user::LocalUser, person::Person}, CommentSortType, ListingType, PostSortType, }; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; pub mod list_comments; +pub mod list_person_content; pub mod list_posts; pub mod read_community; pub mod read_person; @@ -61,3 +67,28 @@ fn comment_sort_type_with_default( .unwrap_or(local_site.default_comment_sort_type), ) } + +async fn resolve_person_id_from_id_or_username( + person_id: &Option, + username: &Option, + context: &Data, + local_user_view: &Option, +) -> LemmyResult { + // Check to make sure a person name or an id is given + if username.is_none() && person_id.is_none() { + Err(LemmyErrorType::NoIdGiven)? + } + + Ok(match person_id { + Some(id) => *id, + None => { + if let Some(username) = username { + resolve_actor_identifier::(username, context, local_user_view, true) + .await? + .id + } else { + Err(LemmyErrorType::NotFound)? + } + } + }) +} diff --git a/crates/apub/src/api/read_person.rs b/crates/apub/src/api/read_person.rs index fac68cd63b..fdcb6ba585 100644 --- a/crates/apub/src/api/read_person.rs +++ b/crates/apub/src/api/read_person.rs @@ -1,4 +1,4 @@ -use crate::{fetcher::resolve_actor_identifier, objects::person::ApubPerson}; +use super::resolve_person_id_from_id_or_username; use activitypub_federation::config::Data; use actix_web::web::{Json, Query}; use lemmy_api_common::{ @@ -6,14 +6,9 @@ use lemmy_api_common::{ person::{GetPersonDetails, GetPersonDetailsResponse}, utils::{check_private_instance, read_site_for_actor}, }; -use lemmy_db_schema::{source::person::Person, utils::post_to_comment_sort_type}; -use lemmy_db_views::{ - comment_view::CommentQuery, - post_view::PostQuery, - structs::{LocalUserView, SiteView}, -}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonView}; -use lemmy_utils::error::{LemmyErrorType, LemmyResult}; +use lemmy_utils::error::LemmyResult; #[tracing::instrument(skip(context))] pub async fn read_person( @@ -21,73 +16,21 @@ pub async fn read_person( context: Data, local_user_view: Option, ) -> LemmyResult> { - // Check to make sure a person name or an id is given - if data.username.is_none() && data.person_id.is_none() { - Err(LemmyErrorType::NoIdGiven)? - } - let local_site = SiteView::read_local(&mut context.pool()).await?; check_private_instance(&local_user_view, &local_site.local_site)?; - let person_details_id = match data.person_id { - Some(id) => id, - None => { - if let Some(username) = &data.username { - resolve_actor_identifier::(username, &context, &local_user_view, true) - .await? - .id - } else { - Err(LemmyErrorType::NotFound)? - } - } - }; + let person_details_id = resolve_person_id_from_id_or_username( + &data.person_id, + &data.username, + &context, + &local_user_view, + ) + .await?; // You don't need to return settings for the user, since this comes back with GetSite // `my_user` let person_view = PersonView::read(&mut context.pool(), person_details_id).await?; - - let sort = data.sort; - let page = data.page; - let limit = data.limit; - let saved_only = data.saved_only; - let community_id = data.community_id; - // If its saved only, you don't care what creator it was - // Or, if its not saved, then you only want it for that specific creator - let creator_id = if !saved_only.unwrap_or_default() { - Some(person_details_id) - } else { - None - }; - - let local_user = local_user_view.as_ref().map(|l| &l.local_user); - - let posts = PostQuery { - sort, - saved_only, - local_user, - community_id, - page, - limit, - creator_id, - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await?; - - let comments = CommentQuery { - local_user, - sort: sort.map(post_to_comment_sort_type), - saved_only, - community_id, - page, - limit, - creator_id, - ..Default::default() - } - .list(&local_site.site, &mut context.pool()) - .await?; - let moderates = CommunityModeratorView::for_person( &mut context.pool(), person_details_id, @@ -97,12 +40,9 @@ pub async fn read_person( let site = read_site_for_actor(person_view.person.actor_id.clone(), &context).await?; - // Return the jwt Ok(Json(GetPersonDetailsResponse { person_view, site, moderates, - comments, - posts, })) } diff --git a/crates/apub/src/api/search.rs b/crates/apub/src/api/search.rs index cdc9bc55ed..0ae7053d39 100644 --- a/crates/apub/src/api/search.rs +++ b/crates/apub/src/api/search.rs @@ -53,7 +53,6 @@ pub async fn search( limit, title_only, post_url_only, - saved_only, liked_only, disliked_only, }) = data; @@ -86,7 +85,6 @@ pub async fn search( url_only: post_url_only, liked_only, disliked_only, - saved_only, ..Default::default() }; @@ -101,7 +99,6 @@ pub async fn search( limit, liked_only, disliked_only, - saved_only, ..Default::default() }; diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index 6184df7d34..d98df25adf 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -212,10 +212,7 @@ pub async fn import_settings( &context, |(saved, context)| async move { let comment = saved.dereference(&context).await?; - let form = CommentSavedForm { - person_id, - comment_id: comment.id, - }; + let form = CommentSavedForm::new(comment.id, person_id); CommentSaved::save(&mut context.pool(), &form).await?; LemmyResult::Ok(()) }, diff --git a/crates/db_schema/replaceable_schema/triggers.sql b/crates/db_schema/replaceable_schema/triggers.sql index e5b3e22d0d..4607c658dc 100644 --- a/crates/db_schema/replaceable_schema/triggers.sql +++ b/crates/db_schema/replaceable_schema/triggers.sql @@ -653,3 +653,218 @@ CREATE TRIGGER change_values FOR EACH ROW EXECUTE FUNCTION r.private_message_change_values (); +-- Combined tables triggers +-- These insert (published, item_id) into X_combined tables +-- Reports (comment_report, post_report, private_message_report) +CREATE PROCEDURE r.create_report_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.report_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO report_combined (published, thing_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER report_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.report_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_report_combined_trigger ('post_report'); + +CALL r.create_report_combined_trigger ('comment_report'); + +CALL r.create_report_combined_trigger ('private_message_report'); + +-- person_content (comment, post) +CREATE PROCEDURE r.create_person_content_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.person_content_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO person_content_combined (published, thing_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER person_content_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.person_content_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_person_content_combined_trigger ('post'); + +CALL r.create_person_content_combined_trigger ('comment'); + +-- person_saved (comment, post) +-- This one is a little different, because it triggers using x_actions.saved, +-- Rather than any row insert +CREATE PROCEDURE r.create_person_saved_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.person_saved_combined_change_values_thing ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + IF (TG_OP = 'DELETE') THEN + DELETE FROM person_saved_combined AS p + WHERE p.person_id = OLD.person_id + AND p.thing_id = OLD.thing_id; + ELSIF (TG_OP = 'INSERT') THEN + IF NEW.saved IS NOT NULL THEN + INSERT INTO person_saved_combined (published, person_id, thing_id) + VALUES (NEW.saved, NEW.person_id, NEW.thing_id); + END IF; + ELSIF (TG_OP = 'UPDATE') THEN + IF NEW.saved IS NOT NULL THEN + INSERT INTO person_saved_combined (published, person_id, thing_id) + VALUES (NEW.saved, NEW.person_id, NEW.thing_id); + -- If saved gets set as null, delete the row + ELSE + DELETE FROM person_saved_combined AS p + WHERE p.person_id = NEW.person_id + AND p.thing_id = NEW.thing_id; + END IF; + END IF; + RETURN NULL; + END $$; + CREATE TRIGGER person_saved_combined + AFTER INSERT OR DELETE OR UPDATE OF saved ON thing_actions + FOR EACH ROW + EXECUTE FUNCTION r.person_saved_combined_change_values_thing ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_person_saved_combined_trigger ('post'); + +CALL r.create_person_saved_combined_trigger ('comment'); + +-- modlog: (17 tables) +-- admin_allow_instance +-- admin_block_instance +-- admin_purge_comment +-- admin_purge_community +-- admin_purge_person +-- admin_purge_post +-- mod_add +-- mod_add_community +-- mod_ban +-- mod_ban_from_community +-- mod_feature_post +-- mod_hide_community +-- mod_lock_post +-- mod_remove_comment +-- mod_remove_community +-- mod_remove_post +-- mod_transfer_community +CREATE PROCEDURE r.create_modlog_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.modlog_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO modlog_combined (published, thing_id) + VALUES (NEW.when_, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER modlog_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.modlog_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_modlog_combined_trigger ('admin_allow_instance'); + +CALL r.create_modlog_combined_trigger ('admin_block_instance'); + +CALL r.create_modlog_combined_trigger ('admin_purge_comment'); + +CALL r.create_modlog_combined_trigger ('admin_purge_community'); + +CALL r.create_modlog_combined_trigger ('admin_purge_person'); + +CALL r.create_modlog_combined_trigger ('admin_purge_post'); + +CALL r.create_modlog_combined_trigger ('mod_add'); + +CALL r.create_modlog_combined_trigger ('mod_add_community'); + +CALL r.create_modlog_combined_trigger ('mod_ban'); + +CALL r.create_modlog_combined_trigger ('mod_ban_from_community'); + +CALL r.create_modlog_combined_trigger ('mod_feature_post'); + +CALL r.create_modlog_combined_trigger ('mod_hide_community'); + +CALL r.create_modlog_combined_trigger ('mod_lock_post'); + +CALL r.create_modlog_combined_trigger ('mod_remove_comment'); + +CALL r.create_modlog_combined_trigger ('mod_remove_community'); + +CALL r.create_modlog_combined_trigger ('mod_remove_post'); + +CALL r.create_modlog_combined_trigger ('mod_transfer_community'); + +-- Inbox: (replies, comment mentions, post mentions, and private_messages) +CREATE PROCEDURE r.create_inbox_combined_trigger (table_name text) +LANGUAGE plpgsql +AS $a$ +BEGIN + EXECUTE replace($b$ CREATE FUNCTION r.inbox_combined_thing_insert ( ) + RETURNS TRIGGER + LANGUAGE plpgsql + AS $$ + BEGIN + INSERT INTO inbox_combined (published, thing_id) + VALUES (NEW.published, NEW.id); + RETURN NEW; + END $$; + CREATE TRIGGER inbox_combined + AFTER INSERT ON thing + FOR EACH ROW + EXECUTE FUNCTION r.inbox_combined_thing_insert ( ); + $b$, + 'thing', + table_name); +END; +$a$; + +CALL r.create_inbox_combined_trigger ('comment_reply'); + +CALL r.create_inbox_combined_trigger ('person_comment_mention'); + +CALL r.create_inbox_combined_trigger ('person_post_mention'); + +CALL r.create_inbox_combined_trigger ('private_message'); + diff --git a/crates/db_schema/src/impls/comment.rs b/crates/db_schema/src/impls/comment.rs index 7dcc033a17..17cd6ce5cd 100644 --- a/crates/db_schema/src/impls/comment.rs +++ b/crates/db_schema/src/impls/comment.rs @@ -184,10 +184,6 @@ impl Saveable for CommentSaved { comment_saved_form: &CommentSavedForm, ) -> Result { let conn = &mut get_conn(pool).await?; - let comment_saved_form = ( - comment_saved_form, - comment_actions::saved.eq(now().nullable()), - ); insert_into(comment_actions::table) .values(comment_saved_form) .on_conflict((comment_actions::comment_id, comment_actions::person_id)) @@ -319,11 +315,7 @@ mod tests { }; // Comment Saved - let comment_saved_form = CommentSavedForm { - comment_id: inserted_comment.id, - person_id: inserted_person.id, - }; - + let comment_saved_form = CommentSavedForm::new(inserted_comment.id, inserted_person.id); let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await?; let expected_comment_saved = CommentSaved { diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index 41ced26f73..d9b1332ab8 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -1,43 +1,25 @@ use crate::{ newtypes::InstanceId, - schema::{admin_allow_instance, federation_allowlist}, - source::{ - federation_allowlist::{FederationAllowList, FederationAllowListForm}, - mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, - }, + schema::federation_allowlist, + source::federation_allowlist::{FederationAllowList, FederationAllowListForm}, utils::{get_conn, DbPool}, }; use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -impl AdminAllowInstance { - pub async fn insert(pool: &mut DbPool<'_>, form: &AdminAllowInstanceForm) -> Result<(), Error> { - let conn = &mut get_conn(pool).await?; - insert_into(admin_allow_instance::table) - .values(form) - .execute(conn) - .await?; - - Ok(()) - } -} - impl FederationAllowList { - pub async fn allow(pool: &mut DbPool<'_>, form: &FederationAllowListForm) -> Result<(), Error> { + pub async fn allow(pool: &mut DbPool<'_>, form: &FederationAllowListForm) -> Result { let conn = &mut get_conn(pool).await?; insert_into(federation_allowlist::table) .values(form) - .execute(conn) - .await?; - Ok(()) + .get_result::(conn) + .await } - pub async fn unallow(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result<(), Error> { - use federation_allowlist::dsl::instance_id; + pub async fn unallow(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result { let conn = &mut get_conn(pool).await?; - delete(federation_allowlist::table.filter(instance_id.eq(instance_id_))) + delete(federation_allowlist::table.filter(federation_allowlist::instance_id.eq(instance_id_))) .execute(conn) - .await?; - Ok(()) + .await } } diff --git a/crates/db_schema/src/impls/federation_blocklist.rs b/crates/db_schema/src/impls/federation_blocklist.rs index 4a42e81b6e..e4baf5b3f7 100644 --- a/crates/db_schema/src/impls/federation_blocklist.rs +++ b/crates/db_schema/src/impls/federation_blocklist.rs @@ -1,42 +1,24 @@ use crate::{ newtypes::InstanceId, - schema::{admin_block_instance, federation_blocklist}, - source::{ - federation_blocklist::{FederationBlockList, FederationBlockListForm}, - mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, - }, + schema::federation_blocklist, + source::federation_blocklist::{FederationBlockList, FederationBlockListForm}, utils::{get_conn, DbPool}, }; use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; -impl AdminBlockInstance { - pub async fn insert(pool: &mut DbPool<'_>, form: &AdminBlockInstanceForm) -> Result<(), Error> { - let conn = &mut get_conn(pool).await?; - insert_into(admin_block_instance::table) - .values(form) - .execute(conn) - .await?; - - Ok(()) - } -} - impl FederationBlockList { - pub async fn block(pool: &mut DbPool<'_>, form: &FederationBlockListForm) -> Result<(), Error> { + pub async fn block(pool: &mut DbPool<'_>, form: &FederationBlockListForm) -> Result { let conn = &mut get_conn(pool).await?; insert_into(federation_blocklist::table) .values(form) - .execute(conn) - .await?; - Ok(()) + .get_result::(conn) + .await } - pub async fn unblock(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result<(), Error> { - use federation_blocklist::dsl::instance_id; + pub async fn unblock(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result { let conn = &mut get_conn(pool).await?; - delete(federation_blocklist::table.filter(instance_id.eq(instance_id_))) + delete(federation_blocklist::table.filter(federation_blocklist::instance_id.eq(instance_id_))) .execute(conn) - .await?; - Ok(()) + .await } } diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index d4ea47800e..80b579c035 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -27,7 +27,8 @@ pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; -pub mod person_mention; +pub mod person_comment_mention; +pub mod person_post_mention; pub mod post; pub mod post_report; pub mod private_message; diff --git a/crates/db_schema/src/impls/mod_log/admin.rs b/crates/db_schema/src/impls/mod_log/admin.rs index c1b2bf69f1..7d007ccbaf 100644 --- a/crates/db_schema/src/impls/mod_log/admin.rs +++ b/crates/db_schema/src/impls/mod_log/admin.rs @@ -1,5 +1,25 @@ use crate::{ + newtypes::{ + AdminAllowInstanceId, + AdminBlockInstanceId, + AdminPurgeCommentId, + AdminPurgeCommunityId, + AdminPurgePersonId, + AdminPurgePostId, + }, + schema::{ + admin_allow_instance, + admin_block_instance, + admin_purge_comment, + admin_purge_community, + admin_purge_person, + admin_purge_post, + }, source::mod_log::admin::{ + AdminAllowInstance, + AdminAllowInstanceForm, + AdminBlockInstance, + AdminBlockInstanceForm, AdminPurgeComment, AdminPurgeCommentForm, AdminPurgeCommunity, @@ -19,12 +39,11 @@ use diesel_async::RunQueryDsl; impl Crud for AdminPurgePerson { type InsertForm = AdminPurgePersonForm; type UpdateForm = AdminPurgePersonForm; - type IdType = i32; + type IdType = AdminPurgePersonId; async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_person::dsl::admin_purge_person; let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_person) + insert_into(admin_purge_person::table) .values(form) .get_result::(conn) .await @@ -32,12 +51,11 @@ impl Crud for AdminPurgePerson { async fn update( pool: &mut DbPool<'_>, - from_id: i32, + from_id: Self::IdType, form: &Self::InsertForm, ) -> Result { - use crate::schema::admin_purge_person::dsl::admin_purge_person; let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_person.find(from_id)) + diesel::update(admin_purge_person::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -48,12 +66,11 @@ impl Crud for AdminPurgePerson { impl Crud for AdminPurgeCommunity { type InsertForm = AdminPurgeCommunityForm; type UpdateForm = AdminPurgeCommunityForm; - type IdType = i32; + type IdType = AdminPurgeCommunityId; async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_community::dsl::admin_purge_community; let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_community) + insert_into(admin_purge_community::table) .values(form) .get_result::(conn) .await @@ -61,12 +78,11 @@ impl Crud for AdminPurgeCommunity { async fn update( pool: &mut DbPool<'_>, - from_id: i32, + from_id: Self::IdType, form: &Self::InsertForm, ) -> Result { - use crate::schema::admin_purge_community::dsl::admin_purge_community; let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_community.find(from_id)) + diesel::update(admin_purge_community::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -77,12 +93,11 @@ impl Crud for AdminPurgeCommunity { impl Crud for AdminPurgePost { type InsertForm = AdminPurgePostForm; type UpdateForm = AdminPurgePostForm; - type IdType = i32; + type IdType = AdminPurgePostId; async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_post::dsl::admin_purge_post; let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_post) + insert_into(admin_purge_post::table) .values(form) .get_result::(conn) .await @@ -90,12 +105,11 @@ impl Crud for AdminPurgePost { async fn update( pool: &mut DbPool<'_>, - from_id: i32, + from_id: Self::IdType, form: &Self::InsertForm, ) -> Result { - use crate::schema::admin_purge_post::dsl::admin_purge_post; let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_post.find(from_id)) + diesel::update(admin_purge_post::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -106,12 +120,65 @@ impl Crud for AdminPurgePost { impl Crud for AdminPurgeComment { type InsertForm = AdminPurgeCommentForm; type UpdateForm = AdminPurgeCommentForm; - type IdType = i32; + type IdType = AdminPurgeCommentId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_comment::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: Self::IdType, + form: &Self::InsertForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_comment::table.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminAllowInstance { + type InsertForm = AdminAllowInstanceForm; + type UpdateForm = AdminAllowInstanceForm; + type IdType = AdminAllowInstanceId; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(admin_allow_instance::table) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: Self::IdType, + form: &Self::InsertForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(admin_allow_instance::table.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminBlockInstance { + type InsertForm = AdminBlockInstanceForm; + type UpdateForm = AdminBlockInstanceForm; + type IdType = AdminBlockInstanceId; async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_comment::dsl::admin_purge_comment; let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_comment) + insert_into(admin_block_instance::table) .values(form) .get_result::(conn) .await @@ -119,12 +186,11 @@ impl Crud for AdminPurgeComment { async fn update( pool: &mut DbPool<'_>, - from_id: i32, + from_id: Self::IdType, form: &Self::InsertForm, ) -> Result { - use crate::schema::admin_purge_comment::dsl::admin_purge_comment; let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_comment.find(from_id)) + diesel::update(admin_block_instance::table.find(from_id)) .set(form) .get_result::(conn) .await diff --git a/crates/db_schema/src/impls/mod_log/moderator.rs b/crates/db_schema/src/impls/mod_log/moderator.rs index 37b66480de..03e0fb9d9f 100644 --- a/crates/db_schema/src/impls/mod_log/moderator.rs +++ b/crates/db_schema/src/impls/mod_log/moderator.rs @@ -1,4 +1,30 @@ use crate::{ + newtypes::{ + ModAddCommunityId, + ModAddId, + ModBanFromCommunityId, + ModBanId, + ModFeaturePostId, + ModHideCommunityId, + ModLockPostId, + ModRemoveCommentId, + ModRemoveCommunityId, + ModRemovePostId, + ModTransferCommunityId, + }, + schema::{ + mod_add, + mod_add_community, + mod_ban, + mod_ban_from_community, + mod_feature_post, + mod_hide_community, + mod_lock_post, + mod_remove_comment, + mod_remove_community, + mod_remove_post, + mod_transfer_community, + }, source::mod_log::moderator::{ ModAdd, ModAddCommunity, @@ -33,12 +59,11 @@ use diesel_async::RunQueryDsl; impl Crud for ModRemovePost { type InsertForm = ModRemovePostForm; type UpdateForm = ModRemovePostForm; - type IdType = i32; + type IdType = ModRemovePostId; - async fn create(pool: &mut DbPool<'_>, form: &ModRemovePostForm) -> Result { - use crate::schema::mod_remove_post::dsl::mod_remove_post; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_remove_post) + insert_into(mod_remove_post::table) .values(form) .get_result::(conn) .await @@ -46,12 +71,11 @@ impl Crud for ModRemovePost { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModRemovePostForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_remove_post::dsl::mod_remove_post; let conn = &mut get_conn(pool).await?; - diesel::update(mod_remove_post.find(from_id)) + diesel::update(mod_remove_post::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -63,9 +87,8 @@ impl ModRemovePost { pool: &mut DbPool<'_>, forms: &Vec, ) -> Result { - use crate::schema::mod_remove_post::dsl::mod_remove_post; let conn = &mut get_conn(pool).await?; - insert_into(mod_remove_post) + insert_into(mod_remove_post::table) .values(forms) .execute(conn) .await @@ -76,12 +99,11 @@ impl ModRemovePost { impl Crud for ModLockPost { type InsertForm = ModLockPostForm; type UpdateForm = ModLockPostForm; - type IdType = i32; + type IdType = ModLockPostId; - async fn create(pool: &mut DbPool<'_>, form: &ModLockPostForm) -> Result { - use crate::schema::mod_lock_post::dsl::mod_lock_post; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_lock_post) + insert_into(mod_lock_post::table) .values(form) .get_result::(conn) .await @@ -89,12 +111,11 @@ impl Crud for ModLockPost { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModLockPostForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_lock_post::dsl::mod_lock_post; let conn = &mut get_conn(pool).await?; - diesel::update(mod_lock_post.find(from_id)) + diesel::update(mod_lock_post::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -105,12 +126,11 @@ impl Crud for ModLockPost { impl Crud for ModFeaturePost { type InsertForm = ModFeaturePostForm; type UpdateForm = ModFeaturePostForm; - type IdType = i32; + type IdType = ModFeaturePostId; - async fn create(pool: &mut DbPool<'_>, form: &ModFeaturePostForm) -> Result { - use crate::schema::mod_feature_post::dsl::mod_feature_post; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_feature_post) + insert_into(mod_feature_post::table) .values(form) .get_result::(conn) .await @@ -118,12 +138,11 @@ impl Crud for ModFeaturePost { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModFeaturePostForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_feature_post::dsl::mod_feature_post; let conn = &mut get_conn(pool).await?; - diesel::update(mod_feature_post.find(from_id)) + diesel::update(mod_feature_post::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -134,12 +153,11 @@ impl Crud for ModFeaturePost { impl Crud for ModRemoveComment { type InsertForm = ModRemoveCommentForm; type UpdateForm = ModRemoveCommentForm; - type IdType = i32; + type IdType = ModRemoveCommentId; - async fn create(pool: &mut DbPool<'_>, form: &ModRemoveCommentForm) -> Result { - use crate::schema::mod_remove_comment::dsl::mod_remove_comment; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_remove_comment) + insert_into(mod_remove_comment::table) .values(form) .get_result::(conn) .await @@ -147,12 +165,11 @@ impl Crud for ModRemoveComment { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModRemoveCommentForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_remove_comment::dsl::mod_remove_comment; let conn = &mut get_conn(pool).await?; - diesel::update(mod_remove_comment.find(from_id)) + diesel::update(mod_remove_comment::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -164,9 +181,8 @@ impl ModRemoveComment { pool: &mut DbPool<'_>, forms: &Vec, ) -> Result { - use crate::schema::mod_remove_comment::dsl::mod_remove_comment; let conn = &mut get_conn(pool).await?; - insert_into(mod_remove_comment) + insert_into(mod_remove_comment::table) .values(forms) .execute(conn) .await @@ -177,12 +193,11 @@ impl ModRemoveComment { impl Crud for ModRemoveCommunity { type InsertForm = ModRemoveCommunityForm; type UpdateForm = ModRemoveCommunityForm; - type IdType = i32; + type IdType = ModRemoveCommunityId; - async fn create(pool: &mut DbPool<'_>, form: &ModRemoveCommunityForm) -> Result { - use crate::schema::mod_remove_community::dsl::mod_remove_community; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_remove_community) + insert_into(mod_remove_community::table) .values(form) .get_result::(conn) .await @@ -190,12 +205,11 @@ impl Crud for ModRemoveCommunity { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModRemoveCommunityForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_remove_community::dsl::mod_remove_community; let conn = &mut get_conn(pool).await?; - diesel::update(mod_remove_community.find(from_id)) + diesel::update(mod_remove_community::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -206,12 +220,11 @@ impl Crud for ModRemoveCommunity { impl Crud for ModBanFromCommunity { type InsertForm = ModBanFromCommunityForm; type UpdateForm = ModBanFromCommunityForm; - type IdType = i32; + type IdType = ModBanFromCommunityId; - async fn create(pool: &mut DbPool<'_>, form: &ModBanFromCommunityForm) -> Result { - use crate::schema::mod_ban_from_community::dsl::mod_ban_from_community; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_ban_from_community) + insert_into(mod_ban_from_community::table) .values(form) .get_result::(conn) .await @@ -219,12 +232,11 @@ impl Crud for ModBanFromCommunity { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModBanFromCommunityForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_ban_from_community::dsl::mod_ban_from_community; let conn = &mut get_conn(pool).await?; - diesel::update(mod_ban_from_community.find(from_id)) + diesel::update(mod_ban_from_community::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -235,21 +247,23 @@ impl Crud for ModBanFromCommunity { impl Crud for ModBan { type InsertForm = ModBanForm; type UpdateForm = ModBanForm; - type IdType = i32; + type IdType = ModBanId; - async fn create(pool: &mut DbPool<'_>, form: &ModBanForm) -> Result { - use crate::schema::mod_ban::dsl::mod_ban; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_ban) + insert_into(mod_ban::table) .values(form) .get_result::(conn) .await } - async fn update(pool: &mut DbPool<'_>, from_id: i32, form: &ModBanForm) -> Result { - use crate::schema::mod_ban::dsl::mod_ban; + async fn update( + pool: &mut DbPool<'_>, + from_id: Self::IdType, + form: &Self::UpdateForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(mod_ban.find(from_id)) + diesel::update(mod_ban::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -260,12 +274,11 @@ impl Crud for ModBan { impl Crud for ModHideCommunity { type InsertForm = ModHideCommunityForm; type UpdateForm = ModHideCommunityForm; - type IdType = i32; + type IdType = ModHideCommunityId; - async fn create(pool: &mut DbPool<'_>, form: &ModHideCommunityForm) -> Result { - use crate::schema::mod_hide_community::dsl::mod_hide_community; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_hide_community) + insert_into(mod_hide_community::table) .values(form) .get_result::(conn) .await @@ -273,12 +286,11 @@ impl Crud for ModHideCommunity { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModHideCommunityForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_hide_community::dsl::mod_hide_community; let conn = &mut get_conn(pool).await?; - diesel::update(mod_hide_community.find(from_id)) + diesel::update(mod_hide_community::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -289,12 +301,11 @@ impl Crud for ModHideCommunity { impl Crud for ModAddCommunity { type InsertForm = ModAddCommunityForm; type UpdateForm = ModAddCommunityForm; - type IdType = i32; + type IdType = ModAddCommunityId; - async fn create(pool: &mut DbPool<'_>, form: &ModAddCommunityForm) -> Result { - use crate::schema::mod_add_community::dsl::mod_add_community; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_add_community) + insert_into(mod_add_community::table) .values(form) .get_result::(conn) .await @@ -302,12 +313,11 @@ impl Crud for ModAddCommunity { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModAddCommunityForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_add_community::dsl::mod_add_community; let conn = &mut get_conn(pool).await?; - diesel::update(mod_add_community.find(from_id)) + diesel::update(mod_add_community::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -318,12 +328,11 @@ impl Crud for ModAddCommunity { impl Crud for ModTransferCommunity { type InsertForm = ModTransferCommunityForm; type UpdateForm = ModTransferCommunityForm; - type IdType = i32; + type IdType = ModTransferCommunityId; - async fn create(pool: &mut DbPool<'_>, form: &ModTransferCommunityForm) -> Result { - use crate::schema::mod_transfer_community::dsl::mod_transfer_community; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_transfer_community) + insert_into(mod_transfer_community::table) .values(form) .get_result::(conn) .await @@ -331,12 +340,11 @@ impl Crud for ModTransferCommunity { async fn update( pool: &mut DbPool<'_>, - from_id: i32, - form: &ModTransferCommunityForm, + from_id: Self::IdType, + form: &Self::UpdateForm, ) -> Result { - use crate::schema::mod_transfer_community::dsl::mod_transfer_community; let conn = &mut get_conn(pool).await?; - diesel::update(mod_transfer_community.find(from_id)) + diesel::update(mod_transfer_community::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -347,21 +355,23 @@ impl Crud for ModTransferCommunity { impl Crud for ModAdd { type InsertForm = ModAddForm; type UpdateForm = ModAddForm; - type IdType = i32; + type IdType = ModAddId; - async fn create(pool: &mut DbPool<'_>, form: &ModAddForm) -> Result { - use crate::schema::mod_add::dsl::mod_add; + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(mod_add) + insert_into(mod_add::table) .values(form) .get_result::(conn) .await } - async fn update(pool: &mut DbPool<'_>, from_id: i32, form: &ModAddForm) -> Result { - use crate::schema::mod_add::dsl::mod_add; + async fn update( + pool: &mut DbPool<'_>, + from_id: Self::IdType, + form: &Self::UpdateForm, + ) -> Result { let conn = &mut get_conn(pool).await?; - diesel::update(mod_add.find(from_id)) + diesel::update(mod_add::table.find(from_id)) .set(form) .get_result::(conn) .await @@ -466,8 +476,8 @@ mod tests { let mod_feature_post_form = ModFeaturePostForm { mod_person_id: inserted_mod.id, post_id: inserted_post.id, - featured: false, - is_featured_community: true, + featured: Some(false), + is_featured_community: Some(true), }; let inserted_mod_feature_post = ModFeaturePost::create(pool, &mod_feature_post_form).await?; let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id).await?; diff --git a/crates/db_schema/src/impls/person_comment_mention.rs b/crates/db_schema/src/impls/person_comment_mention.rs new file mode 100644 index 0000000000..2cc303396d --- /dev/null +++ b/crates/db_schema/src/impls/person_comment_mention.rs @@ -0,0 +1,83 @@ +use crate::{ + diesel::OptionalExtension, + newtypes::{CommentId, PersonCommentMentionId, PersonId}, + schema::person_comment_mention, + source::person_comment_mention::{ + PersonCommentMention, + PersonCommentMentionInsertForm, + PersonCommentMentionUpdateForm, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for PersonCommentMention { + type InsertForm = PersonCommentMentionInsertForm; + type UpdateForm = PersonCommentMentionUpdateForm; + type IdType = PersonCommentMentionId; + + async fn create( + pool: &mut DbPool<'_>, + person_comment_mention_form: &Self::InsertForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + // since the return here isnt utilized, we dont need to do an update + // but get_result doesn't return the existing row here + insert_into(person_comment_mention::table) + .values(person_comment_mention_form) + .on_conflict(( + person_comment_mention::recipient_id, + person_comment_mention::comment_id, + )) + .do_update() + .set(person_comment_mention_form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + person_comment_mention_id: PersonCommentMentionId, + person_comment_mention_form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(person_comment_mention::table.find(person_comment_mention_id)) + .set(person_comment_mention_form) + .get_result::(conn) + .await + } +} + +impl PersonCommentMention { + pub async fn mark_all_as_read( + pool: &mut DbPool<'_>, + for_recipient_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + diesel::update( + person_comment_mention::table + .filter(person_comment_mention::recipient_id.eq(for_recipient_id)) + .filter(person_comment_mention::read.eq(false)), + ) + .set(person_comment_mention::read.eq(true)) + .get_results::(conn) + .await + } + + pub async fn read_by_comment_and_person( + pool: &mut DbPool<'_>, + for_comment_id: CommentId, + for_recipient_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + person_comment_mention::table + .filter(person_comment_mention::comment_id.eq(for_comment_id)) + .filter(person_comment_mention::recipient_id.eq(for_recipient_id)) + .first(conn) + .await + .optional() + } +} diff --git a/crates/db_schema/src/impls/person_mention.rs b/crates/db_schema/src/impls/person_mention.rs deleted file mode 100644 index 4331766838..0000000000 --- a/crates/db_schema/src/impls/person_mention.rs +++ /dev/null @@ -1,76 +0,0 @@ -use crate::{ - diesel::OptionalExtension, - newtypes::{CommentId, PersonId, PersonMentionId}, - schema::person_mention, - source::person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm}, - traits::Crud, - utils::{get_conn, DbPool}, -}; -use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; -use diesel_async::RunQueryDsl; - -#[async_trait] -impl Crud for PersonMention { - type InsertForm = PersonMentionInsertForm; - type UpdateForm = PersonMentionUpdateForm; - type IdType = PersonMentionId; - - async fn create( - pool: &mut DbPool<'_>, - person_mention_form: &Self::InsertForm, - ) -> Result { - let conn = &mut get_conn(pool).await?; - // since the return here isnt utilized, we dont need to do an update - // but get_result doesn't return the existing row here - insert_into(person_mention::table) - .values(person_mention_form) - .on_conflict((person_mention::recipient_id, person_mention::comment_id)) - .do_update() - .set(person_mention_form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - person_mention_id: PersonMentionId, - person_mention_form: &Self::UpdateForm, - ) -> Result { - let conn = &mut get_conn(pool).await?; - diesel::update(person_mention::table.find(person_mention_id)) - .set(person_mention_form) - .get_result::(conn) - .await - } -} - -impl PersonMention { - pub async fn mark_all_as_read( - pool: &mut DbPool<'_>, - for_recipient_id: PersonId, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - diesel::update( - person_mention::table - .filter(person_mention::recipient_id.eq(for_recipient_id)) - .filter(person_mention::read.eq(false)), - ) - .set(person_mention::read.eq(true)) - .get_results::(conn) - .await - } - - pub async fn read_by_comment_and_person( - pool: &mut DbPool<'_>, - for_comment_id: CommentId, - for_recipient_id: PersonId, - ) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - person_mention::table - .filter(person_mention::comment_id.eq(for_comment_id)) - .filter(person_mention::recipient_id.eq(for_recipient_id)) - .first(conn) - .await - .optional() - } -} diff --git a/crates/db_schema/src/impls/person_post_mention.rs b/crates/db_schema/src/impls/person_post_mention.rs new file mode 100644 index 0000000000..ef59b60e17 --- /dev/null +++ b/crates/db_schema/src/impls/person_post_mention.rs @@ -0,0 +1,83 @@ +use crate::{ + diesel::OptionalExtension, + newtypes::{PersonId, PersonPostMentionId, PostId}, + schema::person_post_mention, + source::person_post_mention::{ + PersonPostMention, + PersonPostMentionInsertForm, + PersonPostMentionUpdateForm, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for PersonPostMention { + type InsertForm = PersonPostMentionInsertForm; + type UpdateForm = PersonPostMentionUpdateForm; + type IdType = PersonPostMentionId; + + async fn create( + pool: &mut DbPool<'_>, + person_post_mention_form: &Self::InsertForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + // since the return here isnt utilized, we dont need to do an update + // but get_result doesn't return the existing row here + insert_into(person_post_mention::table) + .values(person_post_mention_form) + .on_conflict(( + person_post_mention::recipient_id, + person_post_mention::post_id, + )) + .do_update() + .set(person_post_mention_form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + person_post_mention_id: PersonPostMentionId, + person_post_mention_form: &Self::UpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(person_post_mention::table.find(person_post_mention_id)) + .set(person_post_mention_form) + .get_result::(conn) + .await + } +} + +impl PersonPostMention { + pub async fn mark_all_as_read( + pool: &mut DbPool<'_>, + for_recipient_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + diesel::update( + person_post_mention::table + .filter(person_post_mention::recipient_id.eq(for_recipient_id)) + .filter(person_post_mention::read.eq(false)), + ) + .set(person_post_mention::read.eq(true)) + .get_results::(conn) + .await + } + + pub async fn read_by_post_and_person( + pool: &mut DbPool<'_>, + for_post_id: PostId, + for_recipient_id: PersonId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + person_post_mention::table + .filter(person_post_mention::post_id.eq(for_post_id)) + .filter(person_post_mention::recipient_id.eq(for_recipient_id)) + .first(conn) + .await + .optional() + } +} diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index ad6e936190..f149094cde 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -219,6 +219,18 @@ pub enum ModlogActionType { AdminAllowInstance, } +#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A list of possible types for the inbox. +pub enum InboxDataType { + All, + CommentReply, + CommentMention, + PostMention, + PrivateMessage, +} + #[derive( EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash, )] @@ -275,6 +287,13 @@ pub enum FederationMode { Disable, } +pub trait InternalToCombinedView { + type CombinedView; + + /// Maps the combined DB row to an enum + fn map_to_enum(&self) -> Option; +} + /// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the /// vec on failure. #[macro_export] diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index c28be8222d..711d83684a 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -55,6 +55,11 @@ impl fmt::Display for CommentId { } } +pub enum PostOrCommentId { + Post(PostId), + Comment(CommentId), +} + #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] @@ -71,7 +76,7 @@ pub struct LocalUserId(pub i32); #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The private message id. -pub struct PrivateMessageId(i32); +pub struct PrivateMessageId(pub i32); impl fmt::Display for PrivateMessageId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -82,26 +87,32 @@ impl fmt::Display for PrivateMessageId { #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] -/// The person mention id. -pub struct PersonMentionId(i32); +/// The person comment mention id. +pub struct PersonCommentMentionId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The person post mention id. +pub struct PersonPostMentionId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The comment report id. -pub struct CommentReportId(i32); +pub struct CommentReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The post report id. -pub struct PostReportId(i32); +pub struct PostReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The private message report id. -pub struct PrivateMessageReportId(i32); +pub struct PrivateMessageReportId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] @@ -119,7 +130,7 @@ pub struct LanguageId(pub i32); #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The comment reply id. -pub struct CommentReplyId(i32); +pub struct CommentReplyId(pub i32); #[derive( Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default, Ord, PartialOrd, @@ -179,6 +190,117 @@ pub struct LtreeDef(pub String); #[cfg_attr(feature = "full", ts(export))] pub struct DbUrl(pub(crate) Box); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +/// The report combined id +pub struct ReportCombinedId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The person content combined id +pub struct PersonContentCombinedId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The person saved combined id +pub struct PersonSavedCombinedId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +pub struct ModlogCombinedId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType))] +/// The inbox combined id +pub struct InboxCombinedId(i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminAllowInstanceId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminBlockInstanceId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminPurgePersonId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminPurgeCommunityId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminPurgeCommentId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminPurgePostId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModRemovePostId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModRemoveCommentId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModRemoveCommunityId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModLockPostId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModFeaturePostId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModBanFromCommunityId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModBanId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModHideCommunityId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModAddCommunityId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModTransferCommunityId(pub i32); + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModAddId(pub i32); + impl DbUrl { pub fn inner(&self) -> &Url { &self.0 diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 66a65d1432..cb430a3fea 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -330,6 +330,17 @@ diesel::table! { } } +diesel::table! { + inbox_combined (id) { + id -> Int4, + published -> Timestamptz, + comment_reply_id -> Nullable, + person_comment_mention_id -> Nullable, + person_post_mention_id -> Nullable, + private_message_id -> Nullable, + } +} + diesel::table! { instance (id) { id -> Int4, @@ -633,6 +644,30 @@ diesel::table! { } } +diesel::table! { + modlog_combined (id) { + id -> Int4, + published -> Timestamptz, + admin_allow_instance_id -> Nullable, + admin_block_instance_id -> Nullable, + admin_purge_comment_id -> Nullable, + admin_purge_community_id -> Nullable, + admin_purge_person_id -> Nullable, + admin_purge_post_id -> Nullable, + mod_add_id -> Nullable, + mod_add_community_id -> Nullable, + mod_ban_id -> Nullable, + mod_ban_from_community_id -> Nullable, + mod_feature_post_id -> Nullable, + mod_hide_community_id -> Nullable, + mod_lock_post_id -> Nullable, + mod_remove_comment_id -> Nullable, + mod_remove_community_id -> Nullable, + mod_remove_post_id -> Nullable, + mod_transfer_community_id -> Nullable, + } +} + diesel::table! { oauth_account (oauth_provider_id, local_user_id) { local_user_id -> Int4, @@ -730,7 +765,7 @@ diesel::table! { } diesel::table! { - person_mention (id) { + person_comment_mention (id) { id -> Int4, recipient_id -> Int4, comment_id -> Int4, @@ -739,6 +774,35 @@ diesel::table! { } } +diesel::table! { + person_content_combined (id) { + id -> Int4, + published -> Timestamptz, + post_id -> Nullable, + comment_id -> Nullable, + } +} + +diesel::table! { + person_post_mention (id) { + id -> Int4, + recipient_id -> Int4, + post_id -> Int4, + read -> Bool, + published -> Timestamptz, + } +} + +diesel::table! { + person_saved_combined (id) { + id -> Int4, + published -> Timestamptz, + person_id -> Int4, + post_id -> Nullable, + comment_id -> Nullable, + } +} + diesel::table! { post (id) { id -> Int4, @@ -881,6 +945,16 @@ diesel::table! { } } +diesel::table! { + report_combined (id) { + id -> Int4, + published -> Timestamptz, + post_report_id -> Nullable, + comment_report_id -> Nullable, + private_message_report_id -> Nullable, + } +} + diesel::table! { secret (id) { id -> Int4, @@ -989,6 +1063,10 @@ diesel::joinable!(email_verification -> local_user (local_user_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id)); diesel::joinable!(federation_queue_state -> instance (instance_id)); +diesel::joinable!(inbox_combined -> comment_reply (comment_reply_id)); +diesel::joinable!(inbox_combined -> person_comment_mention (person_comment_mention_id)); +diesel::joinable!(inbox_combined -> person_post_mention (person_post_mention_id)); +diesel::joinable!(inbox_combined -> private_message (private_message_id)); diesel::joinable!(instance_actions -> instance (instance_id)); diesel::joinable!(instance_actions -> person (person_id)); diesel::joinable!(local_image -> local_user (local_user_id)); @@ -1014,14 +1092,38 @@ diesel::joinable!(mod_remove_community -> person (mod_person_id)); diesel::joinable!(mod_remove_post -> person (mod_person_id)); diesel::joinable!(mod_remove_post -> post (post_id)); diesel::joinable!(mod_transfer_community -> community (community_id)); +diesel::joinable!(modlog_combined -> admin_allow_instance (admin_allow_instance_id)); +diesel::joinable!(modlog_combined -> admin_block_instance (admin_block_instance_id)); +diesel::joinable!(modlog_combined -> admin_purge_comment (admin_purge_comment_id)); +diesel::joinable!(modlog_combined -> admin_purge_community (admin_purge_community_id)); +diesel::joinable!(modlog_combined -> admin_purge_person (admin_purge_person_id)); +diesel::joinable!(modlog_combined -> admin_purge_post (admin_purge_post_id)); +diesel::joinable!(modlog_combined -> mod_add (mod_add_id)); +diesel::joinable!(modlog_combined -> mod_add_community (mod_add_community_id)); +diesel::joinable!(modlog_combined -> mod_ban (mod_ban_id)); +diesel::joinable!(modlog_combined -> mod_ban_from_community (mod_ban_from_community_id)); +diesel::joinable!(modlog_combined -> mod_feature_post (mod_feature_post_id)); +diesel::joinable!(modlog_combined -> mod_hide_community (mod_hide_community_id)); +diesel::joinable!(modlog_combined -> mod_lock_post (mod_lock_post_id)); +diesel::joinable!(modlog_combined -> mod_remove_comment (mod_remove_comment_id)); +diesel::joinable!(modlog_combined -> mod_remove_community (mod_remove_community_id)); +diesel::joinable!(modlog_combined -> mod_remove_post (mod_remove_post_id)); +diesel::joinable!(modlog_combined -> mod_transfer_community (mod_transfer_community_id)); diesel::joinable!(oauth_account -> local_user (local_user_id)); diesel::joinable!(oauth_account -> oauth_provider (oauth_provider_id)); diesel::joinable!(password_reset_request -> local_user (local_user_id)); diesel::joinable!(person -> instance (instance_id)); diesel::joinable!(person_aggregates -> person (person_id)); diesel::joinable!(person_ban -> person (person_id)); -diesel::joinable!(person_mention -> comment (comment_id)); -diesel::joinable!(person_mention -> person (recipient_id)); +diesel::joinable!(person_comment_mention -> comment (comment_id)); +diesel::joinable!(person_comment_mention -> person (recipient_id)); +diesel::joinable!(person_content_combined -> comment (comment_id)); +diesel::joinable!(person_content_combined -> post (post_id)); +diesel::joinable!(person_post_mention -> person (recipient_id)); +diesel::joinable!(person_post_mention -> post (post_id)); +diesel::joinable!(person_saved_combined -> comment (comment_id)); +diesel::joinable!(person_saved_combined -> person (person_id)); +diesel::joinable!(person_saved_combined -> post (post_id)); diesel::joinable!(post -> community (community_id)); diesel::joinable!(post -> language (language_id)); diesel::joinable!(post -> person (creator_id)); @@ -1035,6 +1137,9 @@ diesel::joinable!(post_report -> post (post_id)); diesel::joinable!(private_message_report -> private_message (private_message_id)); diesel::joinable!(registration_application -> local_user (local_user_id)); diesel::joinable!(registration_application -> person (admin_id)); +diesel::joinable!(report_combined -> comment_report (comment_report_id)); +diesel::joinable!(report_combined -> post_report (post_report_id)); +diesel::joinable!(report_combined -> private_message_report (private_message_report_id)); diesel::joinable!(site -> instance (instance_id)); diesel::joinable!(site_aggregates -> site (site_id)); diesel::joinable!(site_language -> language (language_id)); @@ -1064,6 +1169,7 @@ diesel::allow_tables_to_appear_in_same_query!( federation_blocklist, federation_queue_state, image_details, + inbox_combined, instance, instance_actions, language, @@ -1086,6 +1192,7 @@ diesel::allow_tables_to_appear_in_same_query!( mod_remove_community, mod_remove_post, mod_transfer_community, + modlog_combined, oauth_account, oauth_provider, password_reset_request, @@ -1093,7 +1200,10 @@ diesel::allow_tables_to_appear_in_same_query!( person_actions, person_aggregates, person_ban, - person_mention, + person_comment_mention, + person_content_combined, + person_post_mention, + person_saved_combined, post, post_actions, post_aggregates, @@ -1103,6 +1213,7 @@ diesel::allow_tables_to_appear_in_same_query!( received_activity, registration_application, remote_image, + report_combined, secret, sent_activity, site, diff --git a/crates/db_schema/src/source/combined/inbox.rs b/crates/db_schema/src/source/combined/inbox.rs new file mode 100644 index 0000000000..523dd5040a --- /dev/null +++ b/crates/db_schema/src/source/combined/inbox.rs @@ -0,0 +1,33 @@ +use crate::newtypes::{ + CommentReplyId, + InboxCombinedId, + PersonCommentMentionId, + PersonPostMentionId, + PrivateMessageId, +}; +#[cfg(feature = "full")] +use crate::schema::inbox_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = inbox_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = inbox_combined_keys))] +/// A combined inbox table. +pub struct InboxCombined { + pub id: InboxCombinedId, + pub published: DateTime, + pub comment_reply_id: Option, + pub person_comment_mention_id: Option, + pub person_post_mention_id: Option, + pub private_message_id: Option, +} diff --git a/crates/db_schema/src/source/combined/mod.rs b/crates/db_schema/src/source/combined/mod.rs new file mode 100644 index 0000000000..2555ef5be7 --- /dev/null +++ b/crates/db_schema/src/source/combined/mod.rs @@ -0,0 +1,5 @@ +pub mod inbox; +pub mod modlog; +pub mod person_content; +pub mod person_saved; +pub mod report; diff --git a/crates/db_schema/src/source/combined/modlog.rs b/crates/db_schema/src/source/combined/modlog.rs new file mode 100644 index 0000000000..a5e488042b --- /dev/null +++ b/crates/db_schema/src/source/combined/modlog.rs @@ -0,0 +1,57 @@ +use crate::newtypes::{ + AdminAllowInstanceId, + AdminBlockInstanceId, + AdminPurgeCommentId, + AdminPurgeCommunityId, + AdminPurgePersonId, + AdminPurgePostId, + ModAddCommunityId, + ModAddId, + ModBanFromCommunityId, + ModBanId, + ModFeaturePostId, + ModHideCommunityId, + ModLockPostId, + ModRemoveCommentId, + ModRemoveCommunityId, + ModRemovePostId, + ModTransferCommunityId, + ModlogCombinedId, +}; +#[cfg(feature = "full")] +use crate::schema::modlog_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; + +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = modlog_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = modlog_combined_keys))] +/// A combined modlog table. +pub struct ModlogCombined { + pub id: ModlogCombinedId, + pub published: DateTime, + pub admin_allow_instance_id: Option, + pub admin_block_instance_id: Option, + pub admin_purge_comment_id: Option, + pub admin_purge_community_id: Option, + pub admin_purge_person_id: Option, + pub admin_purge_post_id: Option, + pub mod_add_id: Option, + pub mod_add_community_id: Option, + pub mod_ban_id: Option, + pub mod_ban_from_community_id: Option, + pub mod_feature_post_id: Option, + pub mod_hide_community_id: Option, + pub mod_lock_post_id: Option, + pub mod_remove_comment_id: Option, + pub mod_remove_community_id: Option, + pub mod_remove_post_id: Option, + pub mod_transfer_community_id: Option, +} diff --git a/crates/db_schema/src/source/combined/person_content.rs b/crates/db_schema/src/source/combined/person_content.rs new file mode 100644 index 0000000000..ed83401c04 --- /dev/null +++ b/crates/db_schema/src/source/combined/person_content.rs @@ -0,0 +1,25 @@ +use crate::newtypes::{CommentId, PersonContentCombinedId, PostId}; +#[cfg(feature = "full")] +use crate::schema::person_content_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = person_content_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = person_content_combined_keys))] +/// A combined table for a persons contents (posts and comments) +pub struct PersonContentCombined { + pub id: PersonContentCombinedId, + pub published: DateTime, + pub post_id: Option, + pub comment_id: Option, +} diff --git a/crates/db_schema/src/source/combined/person_saved.rs b/crates/db_schema/src/source/combined/person_saved.rs new file mode 100644 index 0000000000..afd91594dd --- /dev/null +++ b/crates/db_schema/src/source/combined/person_saved.rs @@ -0,0 +1,26 @@ +use crate::newtypes::{CommentId, PersonId, PersonSavedCombinedId, PostId}; +#[cfg(feature = "full")] +use crate::schema::person_saved_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = person_saved_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = person_saved_combined_keys))] +/// A combined person_saved table. +pub struct PersonSavedCombined { + pub id: PersonSavedCombinedId, + pub published: DateTime, + pub person_id: PersonId, + pub post_id: Option, + pub comment_id: Option, +} diff --git a/crates/db_schema/src/source/combined/report.rs b/crates/db_schema/src/source/combined/report.rs new file mode 100644 index 0000000000..5ea825b832 --- /dev/null +++ b/crates/db_schema/src/source/combined/report.rs @@ -0,0 +1,26 @@ +use crate::newtypes::{CommentReportId, PostReportId, PrivateMessageReportId, ReportCombinedId}; +#[cfg(feature = "full")] +use crate::schema::report_combined; +use chrono::{DateTime, Utc}; +#[cfg(feature = "full")] +use i_love_jesus::CursorKeysModule; +use serde::{Deserialize, Serialize}; +use serde_with::skip_serializing_none; + +#[skip_serializing_none] +#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[cfg_attr( + feature = "full", + derive(Identifiable, Queryable, Selectable, CursorKeysModule) +)] +#[cfg_attr(feature = "full", diesel(table_name = report_combined))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", cursor_keys_module(name = report_combined_keys))] +/// A combined reports table. +pub struct ReportCombined { + pub id: ReportCombinedId, + pub published: DateTime, + pub post_report_id: Option, + pub comment_report_id: Option, + pub private_message_report_id: Option, +} diff --git a/crates/db_schema/src/source/comment.rs b/crates/db_schema/src/source/comment.rs index d4001807f7..cc5d8c20c1 100644 --- a/crates/db_schema/src/source/comment.rs +++ b/crates/db_schema/src/source/comment.rs @@ -142,7 +142,10 @@ pub struct CommentSaved { #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = comment_actions))] +#[derive(derive_new::new)] pub struct CommentSavedForm { pub comment_id: CommentId, pub person_id: PersonId, + #[new(value = "Utc::now()")] + pub saved: DateTime, } diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 86def96911..4ad78c7dc4 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -5,6 +5,7 @@ use url::Url; pub mod activity; pub mod actor_language; pub mod captcha_answer; +pub mod combined; pub mod comment; pub mod comment_reply; pub mod comment_report; @@ -32,7 +33,8 @@ pub mod oauth_provider; pub mod password_reset_request; pub mod person; pub mod person_block; -pub mod person_mention; +pub mod person_comment_mention; +pub mod person_post_mention; pub mod post; pub mod post_report; pub mod private_message; diff --git a/crates/db_schema/src/source/mod_log/admin.rs b/crates/db_schema/src/source/mod_log/admin.rs index d6e48b8ee3..a7e81aff24 100644 --- a/crates/db_schema/src/source/mod_log/admin.rs +++ b/crates/db_schema/src/source/mod_log/admin.rs @@ -1,4 +1,15 @@ -use crate::newtypes::{CommunityId, InstanceId, PersonId, PostId}; +use crate::newtypes::{ + AdminAllowInstanceId, + AdminBlockInstanceId, + AdminPurgeCommentId, + AdminPurgeCommunityId, + AdminPurgePersonId, + AdminPurgePostId, + CommunityId, + InstanceId, + PersonId, + PostId, +}; #[cfg(feature = "full")] use crate::schema::{ admin_allow_instance, @@ -22,7 +33,7 @@ use ts_rs::TS; #[cfg_attr(feature = "full", ts(export))] /// When an admin purges a person. pub struct AdminPurgePerson { - pub id: i32, + pub id: AdminPurgePersonId, pub admin_person_id: PersonId, #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, @@ -44,7 +55,7 @@ pub struct AdminPurgePersonForm { #[cfg_attr(feature = "full", ts(export))] /// When an admin purges a community. pub struct AdminPurgeCommunity { - pub id: i32, + pub id: AdminPurgeCommunityId, pub admin_person_id: PersonId, #[cfg_attr(feature = "full", ts(optional))] pub reason: Option, @@ -66,7 +77,7 @@ pub struct AdminPurgeCommunityForm { #[cfg_attr(feature = "full", ts(export))] /// When an admin purges a post. pub struct AdminPurgePost { - pub id: i32, + pub id: AdminPurgePostId, pub admin_person_id: PersonId, pub community_id: CommunityId, #[cfg_attr(feature = "full", ts(optional))] @@ -90,7 +101,7 @@ pub struct AdminPurgePostForm { #[cfg_attr(feature = "full", ts(export))] /// When an admin purges a comment. pub struct AdminPurgeComment { - pub id: i32, + pub id: AdminPurgeCommentId, pub admin_person_id: PersonId, pub post_id: PostId, #[cfg_attr(feature = "full", ts(optional))] @@ -120,7 +131,7 @@ pub struct AdminPurgeCommentForm { #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] pub struct AdminAllowInstance { - pub id: i32, + pub id: AdminAllowInstanceId, pub instance_id: InstanceId, pub admin_person_id: PersonId, pub allowed: bool, @@ -153,7 +164,7 @@ pub struct AdminAllowInstanceForm { #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] pub struct AdminBlockInstance { - pub id: i32, + pub id: AdminBlockInstanceId, pub instance_id: InstanceId, pub admin_person_id: PersonId, pub blocked: bool, @@ -172,5 +183,4 @@ pub struct AdminBlockInstanceForm { pub admin_person_id: PersonId, pub blocked: bool, pub reason: Option, - pub when_: Option>, } diff --git a/crates/db_schema/src/source/mod_log/moderator.rs b/crates/db_schema/src/source/mod_log/moderator.rs index 470b643a51..5cf7c39e20 100644 --- a/crates/db_schema/src/source/mod_log/moderator.rs +++ b/crates/db_schema/src/source/mod_log/moderator.rs @@ -1,4 +1,20 @@ -use crate::newtypes::{CommentId, CommunityId, PersonId, PostId}; +use crate::newtypes::{ + CommentId, + CommunityId, + ModAddCommunityId, + ModAddId, + ModBanFromCommunityId, + ModBanId, + ModFeaturePostId, + ModHideCommunityId, + ModLockPostId, + ModRemoveCommentId, + ModRemoveCommunityId, + ModRemovePostId, + ModTransferCommunityId, + PersonId, + PostId, +}; #[cfg(feature = "full")] use crate::schema::{ mod_add, @@ -27,7 +43,7 @@ use ts_rs::TS; #[cfg_attr(feature = "full", ts(export))] /// When a moderator removes a post. pub struct ModRemovePost { - pub id: i32, + pub id: ModRemovePostId, pub mod_person_id: PersonId, pub post_id: PostId, #[cfg_attr(feature = "full", ts(optional))] @@ -52,7 +68,7 @@ pub struct ModRemovePostForm { #[cfg_attr(feature = "full", ts(export))] /// When a moderator locks a post (prevents new comments being made). pub struct ModLockPost { - pub id: i32, + pub id: ModLockPostId, pub mod_person_id: PersonId, pub post_id: PostId, pub locked: bool, @@ -74,7 +90,7 @@ pub struct ModLockPostForm { #[cfg_attr(feature = "full", ts(export))] /// When a moderator features a post on a community (pins it to the top). pub struct ModFeaturePost { - pub id: i32, + pub id: ModFeaturePostId, pub mod_person_id: PersonId, pub post_id: PostId, pub featured: bool, @@ -87,8 +103,8 @@ pub struct ModFeaturePost { pub struct ModFeaturePostForm { pub mod_person_id: PersonId, pub post_id: PostId, - pub featured: bool, - pub is_featured_community: bool, + pub featured: Option, + pub is_featured_community: Option, } #[skip_serializing_none] @@ -99,7 +115,7 @@ pub struct ModFeaturePostForm { #[cfg_attr(feature = "full", ts(export))] /// When a moderator removes a comment. pub struct ModRemoveComment { - pub id: i32, + pub id: ModRemoveCommentId, pub mod_person_id: PersonId, pub comment_id: CommentId, #[cfg_attr(feature = "full", ts(optional))] @@ -125,7 +141,7 @@ pub struct ModRemoveCommentForm { #[cfg_attr(feature = "full", ts(export))] /// When a moderator removes a community. pub struct ModRemoveCommunity { - pub id: i32, + pub id: ModRemoveCommunityId, pub mod_person_id: PersonId, pub community_id: CommunityId, #[cfg_attr(feature = "full", ts(optional))] @@ -151,7 +167,7 @@ pub struct ModRemoveCommunityForm { #[cfg_attr(feature = "full", ts(export))] /// When someone is banned from a community. pub struct ModBanFromCommunity { - pub id: i32, + pub id: ModBanFromCommunityId, pub mod_person_id: PersonId, pub other_person_id: PersonId, pub community_id: CommunityId, @@ -182,7 +198,7 @@ pub struct ModBanFromCommunityForm { #[cfg_attr(feature = "full", ts(export))] /// When someone is banned from the site. pub struct ModBan { - pub id: i32, + pub id: ModBanId, pub mod_person_id: PersonId, pub other_person_id: PersonId, #[cfg_attr(feature = "full", ts(optional))] @@ -210,7 +226,7 @@ pub struct ModHideCommunityForm { #[cfg_attr(feature = "full", ts(export))] /// When a community is hidden from public view. pub struct ModHideCommunity { - pub id: i32, + pub id: ModHideCommunityId, pub community_id: CommunityId, pub mod_person_id: PersonId, pub when_: DateTime, @@ -236,7 +252,7 @@ pub struct ModBanForm { #[cfg_attr(feature = "full", ts(export))] /// When someone is added as a community moderator. pub struct ModAddCommunity { - pub id: i32, + pub id: ModAddCommunityId, pub mod_person_id: PersonId, pub other_person_id: PersonId, pub community_id: CommunityId, @@ -260,7 +276,7 @@ pub struct ModAddCommunityForm { #[cfg_attr(feature = "full", ts(export))] /// When a moderator transfers a community to a new owner. pub struct ModTransferCommunity { - pub id: i32, + pub id: ModTransferCommunityId, pub mod_person_id: PersonId, pub other_person_id: PersonId, pub community_id: CommunityId, @@ -282,7 +298,7 @@ pub struct ModTransferCommunityForm { #[cfg_attr(feature = "full", ts(export))] /// When someone is added as a site moderator. pub struct ModAdd { - pub id: i32, + pub id: ModAddId, pub mod_person_id: PersonId, pub other_person_id: PersonId, pub removed: bool, diff --git a/crates/db_schema/src/source/person_mention.rs b/crates/db_schema/src/source/person_comment_mention.rs similarity index 63% rename from crates/db_schema/src/source/person_mention.rs rename to crates/db_schema/src/source/person_comment_mention.rs index 9c3005655a..bd70af3074 100644 --- a/crates/db_schema/src/source/person_mention.rs +++ b/crates/db_schema/src/source/person_comment_mention.rs @@ -1,6 +1,6 @@ -use crate::newtypes::{CommentId, PersonId, PersonMentionId}; +use crate::newtypes::{CommentId, PersonCommentMentionId, PersonId}; #[cfg(feature = "full")] -use crate::schema::person_mention; +use crate::schema::person_comment_mention; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; #[cfg(feature = "full")] @@ -12,12 +12,12 @@ use ts_rs::TS; derive(Queryable, Selectable, Associations, Identifiable, TS) )] #[cfg_attr(feature = "full", diesel(belongs_to(crate::source::comment::Comment)))] -#[cfg_attr(feature = "full", diesel(table_name = person_mention))] +#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// A person mention. -pub struct PersonMention { - pub id: PersonMentionId, +pub struct PersonCommentMention { + pub id: PersonCommentMentionId, pub recipient_id: PersonId, pub comment_id: CommentId, pub read: bool, @@ -25,15 +25,15 @@ pub struct PersonMention { } #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_mention))] -pub struct PersonMentionInsertForm { +#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))] +pub struct PersonCommentMentionInsertForm { pub recipient_id: PersonId, pub comment_id: CommentId, pub read: Option, } #[cfg_attr(feature = "full", derive(AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = person_mention))] -pub struct PersonMentionUpdateForm { +#[cfg_attr(feature = "full", diesel(table_name = person_comment_mention))] +pub struct PersonCommentMentionUpdateForm { pub read: Option, } diff --git a/crates/db_schema/src/source/person_post_mention.rs b/crates/db_schema/src/source/person_post_mention.rs new file mode 100644 index 0000000000..b1c00febf5 --- /dev/null +++ b/crates/db_schema/src/source/person_post_mention.rs @@ -0,0 +1,39 @@ +use crate::newtypes::{PersonId, PersonPostMentionId, PostId}; +#[cfg(feature = "full")] +use crate::schema::person_post_mention; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "full")] +use ts_rs::TS; + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr( + feature = "full", + derive(Queryable, Selectable, Associations, Identifiable, TS) +)] +#[cfg_attr(feature = "full", diesel(belongs_to(crate::source::post::Post)))] +#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A person mention. +pub struct PersonPostMention { + pub id: PersonPostMentionId, + pub recipient_id: PersonId, + pub post_id: PostId, + pub read: bool, + pub published: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))] +pub struct PersonPostMentionInsertForm { + pub recipient_id: PersonId, + pub post_id: PostId, + pub read: Option, +} + +#[cfg_attr(feature = "full", derive(AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = person_post_mention))] +pub struct PersonPostMentionUpdateForm { + pub read: Option, +} diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index df8124c8a9..20dca5139f 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -40,6 +40,7 @@ ts-rs = { workspace = true, optional = true } actix-web = { workspace = true, optional = true } i-love-jesus = { workspace = true, optional = true } chrono = { workspace = true } +derive-new.workspace = true [dev-dependencies] serial_test = { workspace = true } diff --git a/crates/db_views/src/comment_report_view.rs b/crates/db_views/src/comment_report_view.rs index b4a23a0da6..6154b9b565 100644 --- a/crates/db_views/src/comment_report_view.rs +++ b/crates/db_views/src/comment_report_view.rs @@ -1,7 +1,6 @@ -use crate::structs::{CommentReportView, LocalUserView}; +use crate::structs::CommentReportView; use diesel::{ dsl::now, - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -12,7 +11,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::{CommentId, CommentReportId, CommunityId, PersonId}, + newtypes::{CommentReportId, PersonId}, schema::{ comment, comment_actions, @@ -26,26 +25,21 @@ use lemmy_db_schema::{ post, }, source::community::CommunityFollower, - utils::{ - actions, - actions_alias, - functions::coalesce, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, CommentReportView, (CommentReportId, PersonId)>, - impl ListFn<'a, CommentReportView, (CommentReportQuery, &'a LocalUserView)>, -> { - let all_joins = |query: comment_report::BoxedQuery<'a, Pg>, my_person_id: PersonId| { - query +impl CommentReportView { + /// returns the CommentReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: CommentReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + comment_report::table + .find(report_id) .inner_join(comment::table) .inner_join(post::table.on(comment::post_id.eq(post::id))) .inner_join(community::table.on(post::community_id.eq(community::id))) @@ -117,456 +111,7 @@ fn queries<'a>() -> Queries< comment_actions::like_score.nullable(), aliases::person2.fields(person::all_columns).nullable(), )) - }; - - let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (CommentReportId, PersonId)| async move { - all_joins( - comment_report::table.find(report_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, - (options, user): (CommentReportQuery, &'a LocalUserView)| async move { - let mut query = all_joins(comment_report::table.into_boxed(), user.person.id); - - if let Some(community_id) = options.community_id { - query = query.filter(post::community_id.eq(community_id)); - } - - if let Some(comment_id) = options.comment_id { - query = query.filter(comment_report::comment_id.eq(comment_id)); - } - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(comment_report::resolved.eq(false)) - .order_by(comment_report::published.asc()); - } else { - query = query.order_by(comment_report::published.desc()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query.limit(limit).offset(offset); - - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); - } - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl CommentReportView { - /// returns the CommentReportView for the provided report_id - /// - /// * `report_id` - the report id to obtain - pub async fn read( - pool: &mut DbPool<'_>, - report_id: CommentReportId, - my_person_id: PersonId, - ) -> Result { - queries().read(pool, (report_id, my_person_id)).await - } - - /// Returns the current unresolved comment report count for the communities you mod - pub async fn get_report_count( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - admin: bool, - community_id: Option, - ) -> Result { - use diesel::dsl::count; - - let conn = &mut get_conn(pool).await?; - - let mut query = comment_report::table - .inner_join(comment::table) - .inner_join(post::table.on(comment::post_id.eq(post::id))) - .filter(comment_report::resolved.eq(false)) - .into_boxed(); - - if let Some(community_id) = community_id { - query = query.filter(post::community_id.eq(community_id)) - } - - // If its not an admin, get only the ones you mod - if !admin { - query - .inner_join( - community_actions::table.on( - community_actions::community_id - .eq(post::community_id) - .and(community_actions::person_id.eq(my_person_id)) - .and(community_actions::became_moderator.is_not_null()), - ), - ) - .select(count(comment_report::id)) - .first::(conn) - .await - } else { - query - .select(count(comment_report::id)) - .first::(conn) - .await - } - } -} - -#[derive(Default)] -pub struct CommentReportQuery { - pub community_id: Option, - pub comment_id: Option, - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl CommentReportQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - user: &LocalUserView, - ) -> Result, Error> { - queries().list(pool, (self, user)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::{ - comment_report_view::{CommentReportQuery, CommentReportView}, - structs::LocalUserView, - }; - use lemmy_db_schema::{ - aggregates::structs::CommentAggregates, - source::{ - comment::{Comment, CommentInsertForm}, - comment_report::{CommentReport, CommentReportForm}, - community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - }, - traits::{Crud, Joinable, Reportable}, - utils::{build_db_pool_for_tests, RANK_DEFAULT}, - CommunityVisibility, - SubscribedType, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_crv"); - - let inserted_timmy = Person::create(pool, &new_person).await?; - - let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; - let timmy_view = LocalUserView { - local_user: timmy_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_timmy.clone(), - counts: Default::default(), - }; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_crv"); - - let inserted_sara = Person::create(pool, &new_person_2).await?; - - // Add a third person, since new ppl can only report something once. - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_crv"); - - let inserted_jessica = Person::create(pool, &new_person_3).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test community crv".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - // Make timmy a mod - let timmy_moderator_form = CommunityModeratorForm { - community_id: inserted_community.id, - person_id: inserted_timmy.id, - }; - - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; - - let new_post = PostInsertForm::new( - "A test post crv".into(), - inserted_timmy.id, - inserted_community.id, - ); - - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_timmy.id, - inserted_post.id, - "A test comment 32".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - // sara reports - let sara_report_form = CommentReportForm { - creator_id: inserted_sara.id, - comment_id: inserted_comment.id, - original_comment_text: "this was it at time of creation".into(), - reason: "from sara".into(), - }; - - let inserted_sara_report = CommentReport::report(pool, &sara_report_form).await?; - - // jessica reports - let jessica_report_form = CommentReportForm { - creator_id: inserted_jessica.id, - comment_id: inserted_comment.id, - original_comment_text: "this was it at time of creation".into(), - reason: "from jessica".into(), - }; - - let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; - - let agg = CommentAggregates::read(pool, inserted_comment.id).await?; - - let read_jessica_report_view = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - let expected_jessica_report_view = CommentReportView { - comment_report: inserted_jessica_report.clone(), - comment: inserted_comment.clone(), - post: inserted_post, - creator_is_moderator: true, - creator_is_admin: false, - creator_blocked: false, - subscribed: SubscribedType::NotSubscribed, - saved: false, - community: Community { - id: inserted_community.id, - name: inserted_community.name, - icon: None, - removed: false, - deleted: false, - nsfw: false, - actor_id: inserted_community.actor_id.clone(), - local: true, - title: inserted_community.title, - sidebar: None, - description: None, - updated: None, - banner: None, - hidden: false, - posting_restricted_to_mods: false, - published: inserted_community.published, - private_key: inserted_community.private_key, - public_key: inserted_community.public_key, - last_refreshed_at: inserted_community.last_refreshed_at, - followers_url: inserted_community.followers_url, - inbox_url: inserted_community.inbox_url, - moderators_url: inserted_community.moderators_url, - featured_url: inserted_community.featured_url, - instance_id: inserted_instance.id, - visibility: CommunityVisibility::Public, - }, - creator: Person { - id: inserted_jessica.id, - name: inserted_jessica.name, - display_name: None, - published: inserted_jessica.published, - avatar: None, - actor_id: inserted_jessica.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_jessica.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_jessica.private_key, - public_key: inserted_jessica.public_key, - last_refreshed_at: inserted_jessica.last_refreshed_at, - }, - comment_creator: Person { - id: inserted_timmy.id, - name: inserted_timmy.name.clone(), - display_name: None, - published: inserted_timmy.published, - avatar: None, - actor_id: inserted_timmy.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_timmy.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_timmy.private_key.clone(), - public_key: inserted_timmy.public_key.clone(), - last_refreshed_at: inserted_timmy.last_refreshed_at, - }, - creator_banned_from_community: false, - counts: CommentAggregates { - comment_id: inserted_comment.id, - score: 0, - upvotes: 0, - downvotes: 0, - published: agg.published, - child_count: 0, - hot_rank: RANK_DEFAULT, - controversy_rank: 0.0, - report_count: 2, - unresolved_report_count: 2, - }, - my_vote: None, - resolver: None, - }; - - assert_eq!(read_jessica_report_view, expected_jessica_report_view); - - let mut expected_sara_report_view = expected_jessica_report_view.clone(); - expected_sara_report_view.comment_report = inserted_sara_report; - expected_sara_report_view.creator = Person { - id: inserted_sara.id, - name: inserted_sara.name, - display_name: None, - published: inserted_sara.published, - avatar: None, - actor_id: inserted_sara.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_sara.inbox_url.clone(), - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - private_key: inserted_sara.private_key, - public_key: inserted_sara.public_key, - last_refreshed_at: inserted_sara.last_refreshed_at, - }; - - // Do a batch read of timmys reports - let reports = CommentReportQuery::default() - .list(pool, &timmy_view) - .await?; - - assert_eq!( - reports, - [ - expected_jessica_report_view.clone(), - expected_sara_report_view.clone(), - ] - ); - - // Make sure the counts are correct - let report_count = - CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(2, report_count); - - // Try to resolve the report - CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - let read_jessica_report_view_after_resolve = - CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - - let mut expected_jessica_report_view_after_resolve = expected_jessica_report_view; - expected_jessica_report_view_after_resolve - .comment_report - .resolved = true; - expected_jessica_report_view_after_resolve - .comment_report - .resolver_id = Some(inserted_timmy.id); - expected_jessica_report_view_after_resolve - .comment_report - .updated = read_jessica_report_view_after_resolve - .comment_report - .updated; - expected_jessica_report_view_after_resolve - .counts - .unresolved_report_count = 1; - expected_sara_report_view.counts.unresolved_report_count = 1; - expected_jessica_report_view_after_resolve.resolver = Some(Person { - id: inserted_timmy.id, - name: inserted_timmy.name.clone(), - display_name: None, - published: inserted_timmy.published, - avatar: None, - actor_id: inserted_timmy.actor_id.clone(), - local: true, - banned: false, - deleted: false, - bot_account: false, - bio: None, - banner: None, - updated: None, - inbox_url: inserted_timmy.inbox_url.clone(), - private_key: inserted_timmy.private_key.clone(), - public_key: inserted_timmy.public_key.clone(), - last_refreshed_at: inserted_timmy.last_refreshed_at, - matrix_user_id: None, - ban_expires: None, - instance_id: inserted_instance.id, - }); - - assert_eq!( - read_jessica_report_view_after_resolve, - expected_jessica_report_view_after_resolve - ); - - // Do a batch read of timmys reports - // It should only show saras, which is unresolved - let reports_after_resolve = CommentReportQuery { - unresolved_only: (true), - ..Default::default() - } - .list(pool, &timmy_view) - .await?; - assert_eq!(reports_after_resolve[0], expected_sara_report_view); - assert_eq!(reports_after_resolve.len(), 1); - - // Make sure the counts are correct - let report_count_after_resolved = - CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(1, report_count_after_resolved); - - Person::delete(pool, inserted_timmy.id).await?; - Person::delete(pool, inserted_sara.id).await?; - Person::delete(pool, inserted_jessica.id).await?; - Community::delete(pool, inserted_community.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) + .first(conn) + .await } } diff --git a/crates/db_views/src/comment_view.rs b/crates/db_views/src/comment_view.rs index 2cf751f9f2..0067d0807b 100644 --- a/crates/db_views/src/comment_view.rs +++ b/crates/db_views/src/comment_view.rs @@ -189,13 +189,6 @@ fn queries<'a>() -> Queries< } } - // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only.unwrap_or_default() { - query = query - .filter(comment_actions::saved.is_not_null()) - .then_order_by(comment_actions::saved.desc()); - } - if let Some(my_id) = options.local_user.person_id() { let not_creator_filter = comment::creator_id.ne(my_id); if options.liked_only.unwrap_or_default() { @@ -337,7 +330,6 @@ pub struct CommentQuery<'a> { pub creator_id: Option, pub local_user: Option<&'a LocalUser>, pub search_term: Option, - pub saved_only: Option, pub liked_only: Option, pub disliked_only: Option, pub page: Option, @@ -381,15 +373,7 @@ mod tests { newtypes::LanguageId, source::{ actor_language::LocalUserLanguage, - comment::{ - Comment, - CommentInsertForm, - CommentLike, - CommentLikeForm, - CommentSaved, - CommentSavedForm, - CommentUpdateForm, - }, + comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm}, community::{ Community, CommunityFollower, @@ -411,7 +395,7 @@ mod tests { post::{Post, PostInsertForm, PostUpdateForm}, site::{Site, SiteInsertForm}, }, - traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable}, utils::{build_db_pool_for_tests, RANK_DEFAULT}, CommunityVisibility, SubscribedType, @@ -897,47 +881,6 @@ mod tests { cleanup(data, pool).await } - #[tokio::test] - #[serial] - async fn test_saved_order() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - // Save two comments - let save_comment_0_form = CommentSavedForm { - person_id: data.timmy_local_user_view.person.id, - comment_id: data.inserted_comment_0.id, - }; - CommentSaved::save(pool, &save_comment_0_form).await?; - - let save_comment_2_form = CommentSavedForm { - person_id: data.timmy_local_user_view.person.id, - comment_id: data.inserted_comment_2.id, - }; - CommentSaved::save(pool, &save_comment_2_form).await?; - - // Fetch the saved comments - let comments = CommentQuery { - local_user: Some(&data.timmy_local_user_view.local_user), - saved_only: Some(true), - ..Default::default() - } - .list(&data.site, pool) - .await?; - - // There should only be two comments - assert_eq!(2, comments.len()); - - // The first comment, should be the last one saved (descending order) - assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); - - // The second comment, should be the first one saved - assert_eq!(comments[1].comment.id, data.inserted_comment_0.id); - - cleanup(data, pool).await - } - async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { CommentLike::remove( pool, diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index e93c7409d1..06411a0cbb 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -12,16 +12,20 @@ pub mod local_image_view; #[cfg(feature = "full")] pub mod local_user_view; #[cfg(feature = "full")] +pub mod person_content_combined_view; +#[cfg(feature = "full")] +pub mod person_saved_combined_view; +#[cfg(feature = "full")] pub mod post_report_view; #[cfg(feature = "full")] pub mod post_view; #[cfg(feature = "full")] pub mod private_message_report_view; #[cfg(feature = "full")] -pub mod private_message_view; -#[cfg(feature = "full")] pub mod registration_application_view; #[cfg(feature = "full")] +pub mod report_combined_view; +#[cfg(feature = "full")] pub mod site_view; pub mod structs; #[cfg(feature = "full")] diff --git a/crates/db_views/src/person_content_combined_view.rs b/crates/db_views/src/person_content_combined_view.rs new file mode 100644 index 0000000000..0dca668855 --- /dev/null +++ b/crates/db_views/src/person_content_combined_view.rs @@ -0,0 +1,428 @@ +use crate::structs::{ + CommentView, + LocalUserView, + PersonContentCombinedPaginationCursor, + PersonContentCombinedView, + PersonContentViewInternal, + PostView, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::creator_community_actions, + newtypes::PersonId, + schema::{ + comment, + comment_actions, + comment_aggregates, + community, + community_actions, + image_details, + local_user, + person, + person_actions, + person_content_combined, + post, + post_actions, + post_aggregates, + }, + source::{ + combined::person_content::{person_content_combined_keys as key, PersonContentCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, + InternalToCombinedView, +}; +use lemmy_utils::error::LemmyResult; + +impl PersonContentCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &PersonContentCombinedView) -> PersonContentCombinedPaginationCursor { + let (prefix, id) = match view { + PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), + PersonContentCombinedView::Post(v) => ('P', v.post.id.0), + }; + // hex encoding to prevent ossification + PersonContentCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = person_content_combined::table + .select(PersonContentCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(person_content_combined::comment_id.eq(id)), + "P" => query.filter(person_content_combined::post_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(PersonContentCombined); + +#[derive(derive_new::new)] +pub struct PersonContentCombinedQuery { + pub creator_id: PersonId, + #[new(default)] + pub page_after: Option, + #[new(default)] + pub page_back: Option, +} + +impl PersonContentCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &Option, + ) -> LemmyResult> { + let my_person_id = user.as_ref().map(|u| u.local_user.person_id); + let item_creator = person::id; + + let conn = &mut get_conn(pool).await?; + + // Notes: since the post_id and comment_id are optional columns, + // many joins must use an OR condition. + // For example, the creator must be the person table joined to either: + // - post.creator_id + // - comment.creator_id + let query = person_content_combined::table + // The comment + .left_join(comment::table.on(person_content_combined::comment_id.eq(comment::id.nullable()))) + // The post + // It gets a bit complicated here, because since both comments and post combined have a post + // attached, you can do an inner join. + .inner_join( + post::table.on( + person_content_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.eq(post::id)), + ), + ) + // The item creator + .inner_join( + person::table.on( + comment::creator_id + .eq(item_creator) + // Need to filter out the post rows where the post_id given is null + // Otherwise you'll get duped post rows + .or( + post::creator_id + .eq(item_creator) + .and(person_content_combined::post_id.is_not_null()), + ), + ), + ) + // The community + .inner_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(post_actions::table, my_person_id, post::id)) + .left_join(actions(person_actions::table, my_person_id, item_creator)) + .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .left_join( + comment_aggregates::table + .on(person_content_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), + ) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + // The creator id filter + .filter(item_creator.eq(self.creator_id)) + .select(( + // Post-specific + post_aggregates::all_columns, + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + // Comment-specific + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Shared + post::all_columns, + community::all_columns, + person::all_columns, + CommunityFollower::select_subscribed_type(), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + )) + .into_boxed(); + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // Sorting by published + query = query + .then_desc(key::published) + // Tie breaker + .then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +impl InternalToCombinedView for PersonContentViewInternal { + type CombinedView = PersonContentCombinedView; + + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(comment), Some(counts)) = (v.comment, v.comment_counts) { + Some(PersonContentCombinedView::Comment(CommentView { + comment, + counts, + post: v.post, + community: v.community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + })) + } else { + Some(PersonContentCombinedView::Post(PostView { + post: v.post, + community: v.community, + unread_comments: v.post_unread_comments, + counts: v.post_counts, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + image_details: v.image_details, + banned_from_community: v.banned_from_community, + })) + } + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + person_content_combined_view::PersonContentCombinedQuery, + structs::PersonContentCombinedView, + }; + use lemmy_db_schema::{ + source::{ + comment::{Comment, CommentInsertForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, + }, + traits::Crud, + utils::{build_db_pool_for_tests, DbPool}, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + sara: Person, + timmy_post: Post, + timmy_post_2: Post, + sara_post: Post, + timmy_comment: Comment, + sara_comment: Comment, + sara_comment_2: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv"); + let timmy = Person::create(pool, &timmy_form).await?; + + let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); + let sara = Person::create(pool, &sara_form).await?; + + let community_form = CommunityInsertForm::new( + instance.id, + "test community pcv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + let timmy_post_form = PostInsertForm::new("timmy post prv".into(), timmy.id, community.id); + let timmy_post = Post::create(pool, &timmy_post_form).await?; + + let timmy_post_form_2 = PostInsertForm::new("timmy post prv 2".into(), timmy.id, community.id); + let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + + let sara_post_form = PostInsertForm::new("sara post prv".into(), sara.id, community.id); + let sara_post = Post::create(pool, &sara_post_form).await?; + + let timmy_comment_form = + CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv".into()); + let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + + let sara_comment_form = + CommentInsertForm::new(sara.id, timmy_post.id, "sara comment prv".into()); + let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + + let sara_comment_form_2 = + CommentInsertForm::new(sara.id, timmy_post_2.id, "sara comment prv 2".into()); + let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + + Ok(Data { + instance, + timmy, + sara, + timmy_post, + timmy_post_2, + sara_post, + timmy_comment, + sara_comment, + sara_comment_2, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Do a batch read of timmy + let timmy_content = PersonContentCombinedQuery::new(data.timmy.id) + .list(pool, &None) + .await?; + assert_eq!(3, timmy_content.len()); + + // Make sure the types are correct + if let PersonContentCombinedView::Comment(v) = &timmy_content[0] { + assert_eq!(data.timmy_comment.id, v.comment.id); + assert_eq!(data.timmy.id, v.creator.id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &timmy_content[1] { + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &timmy_content[2] { + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + + // Do a batch read of sara + let sara_content = PersonContentCombinedQuery::new(data.sara.id) + .list(pool, &None) + .await?; + assert_eq!(3, sara_content.len()); + + // Make sure the report types are correct + if let PersonContentCombinedView::Comment(v) = &sara_content[0] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.sara.id, v.creator.id); + // This one was to timmy_post_2 + assert_eq!(data.timmy_post_2.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Comment(v) = &sara_content[1] { + assert_eq!(data.sara_comment.id, v.comment.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Post(v) = &sara_content[2] { + assert_eq!(data.sara_post.id, v.post.id); + assert_eq!(data.sara.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views/src/person_saved_combined_view.rs b/crates/db_views/src/person_saved_combined_view.rs new file mode 100644 index 0000000000..9c800c016e --- /dev/null +++ b/crates/db_views/src/person_saved_combined_view.rs @@ -0,0 +1,391 @@ +use crate::structs::{ + LocalUserView, + PersonContentCombinedView, + PersonContentViewInternal, + PersonSavedCombinedPaginationCursor, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::creator_community_actions, + schema::{ + comment, + comment_actions, + comment_aggregates, + community, + community_actions, + image_details, + local_user, + person, + person_actions, + person_saved_combined, + post, + post_actions, + post_aggregates, + }, + source::{ + combined::person_saved::{person_saved_combined_keys as key, PersonSavedCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, + InternalToCombinedView, +}; +use lemmy_utils::error::LemmyResult; + +impl PersonSavedCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &PersonContentCombinedView) -> PersonSavedCombinedPaginationCursor { + let (prefix, id) = match view { + PersonContentCombinedView::Comment(v) => ('C', v.comment.id.0), + PersonContentCombinedView::Post(v) => ('P', v.post.id.0), + }; + // hex encoding to prevent ossification + PersonSavedCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = person_saved_combined::table + .select(PersonSavedCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(person_saved_combined::comment_id.eq(id)), + "P" => query.filter(person_saved_combined::post_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(PersonSavedCombined); + +#[derive(Default)] +pub struct PersonSavedCombinedQuery { + pub page_after: Option, + pub page_back: Option, +} + +impl PersonSavedCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &LocalUserView, + ) -> LemmyResult> { + let my_person_id = user.local_user.person_id; + let item_creator = person::id; + + let conn = &mut get_conn(pool).await?; + + // Notes: since the post_id and comment_id are optional columns, + // many joins must use an OR condition. + // For example, the creator must be the person table joined to either: + // - post.creator_id + // - comment.creator_id + let query = person_saved_combined::table + // The comment + .left_join(comment::table.on(person_saved_combined::comment_id.eq(comment::id.nullable()))) + // The post + // It gets a bit complicated here, because since both comments and post combined have a post + // attached, you can do an inner join. + .inner_join( + post::table.on( + person_saved_combined::post_id + .eq(post::id.nullable()) + .or(comment::post_id.eq(post::id)), + ), + ) + // The item creator + .inner_join( + person::table.on( + comment::creator_id + .eq(item_creator) + // Need to filter out the post rows where the post_id given is null + // Otherwise you'll get duped post rows + .or( + post::creator_id + .eq(item_creator) + .and(person_saved_combined::post_id.is_not_null()), + ), + ), + ) + // The community + .inner_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .left_join( + comment_aggregates::table + .on(person_saved_combined::comment_id.eq(comment_aggregates::comment_id.nullable())), + ) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment::id, + )) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + // The person id filter + .filter(person_saved_combined::person_id.eq(my_person_id)) + .select(( + // Post-specific + post_aggregates::all_columns, + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + // Comment-specific + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Shared + post::all_columns, + community::all_columns, + person::all_columns, + CommunityFollower::select_subscribed_type(), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + )) + .into_boxed(); + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // Sorting by published + query = query + .then_desc(key::published) + // Tie breaker + .then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + person_saved_combined_view::PersonSavedCombinedQuery, + structs::{LocalUserView, PersonContentCombinedView}, + }; + use lemmy_db_schema::{ + source::{ + comment::{Comment, CommentInsertForm, CommentSaved, CommentSavedForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + local_user_vote_display_mode::LocalUserVoteDisplayMode, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm, PostSaved, PostSavedForm}, + }, + traits::{Crud, Saveable}, + utils::{build_db_pool_for_tests, DbPool}, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + timmy_view: LocalUserView, + sara: Person, + timmy_post: Post, + sara_comment: Comment, + sara_comment_2: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv"); + let timmy = Person::create(pool, &timmy_form).await?; + let timmy_local_user_form = LocalUserInsertForm::test_form(timmy.id); + let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; + let timmy_view = LocalUserView { + local_user: timmy_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: timmy.clone(), + counts: Default::default(), + }; + + let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); + let sara = Person::create(pool, &sara_form).await?; + + let community_form = CommunityInsertForm::new( + instance.id, + "test community pcv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + let timmy_post_form = PostInsertForm::new("timmy post prv".into(), timmy.id, community.id); + let timmy_post = Post::create(pool, &timmy_post_form).await?; + + let timmy_post_form_2 = PostInsertForm::new("timmy post prv 2".into(), timmy.id, community.id); + let timmy_post_2 = Post::create(pool, &timmy_post_form_2).await?; + + let sara_post_form = PostInsertForm::new("sara post prv".into(), sara.id, community.id); + let _sara_post = Post::create(pool, &sara_post_form).await?; + + let timmy_comment_form = + CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv".into()); + let _timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + + let sara_comment_form = + CommentInsertForm::new(sara.id, timmy_post.id, "sara comment prv".into()); + let sara_comment = Comment::create(pool, &sara_comment_form, None).await?; + + let sara_comment_form_2 = + CommentInsertForm::new(sara.id, timmy_post_2.id, "sara comment prv 2".into()); + let sara_comment_2 = Comment::create(pool, &sara_comment_form_2, None).await?; + + Ok(Data { + instance, + timmy, + timmy_view, + sara, + timmy_post, + sara_comment, + sara_comment_2, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Do a batch read of timmy saved + let timmy_saved = PersonSavedCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_eq!(0, timmy_saved.len()); + + // Save a few things + let save_sara_comment_2 = + CommentSavedForm::new(data.sara_comment_2.id, data.timmy_view.person.id); + CommentSaved::save(pool, &save_sara_comment_2).await?; + + let save_sara_comment = CommentSavedForm::new(data.sara_comment.id, data.timmy_view.person.id); + CommentSaved::save(pool, &save_sara_comment).await?; + + let post_save_form = PostSavedForm::new(data.timmy_post.id, data.timmy.id); + PostSaved::save(pool, &post_save_form).await?; + + let timmy_saved = PersonSavedCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_eq!(3, timmy_saved.len()); + + // Make sure the types and order are correct + if let PersonContentCombinedView::Post(v) = &timmy_saved[0] { + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Comment(v) = &timmy_saved[1] { + assert_eq!(data.sara_comment.id, v.comment.id); + assert_eq!(data.sara.id, v.comment.creator_id); + } else { + panic!("wrong type"); + } + if let PersonContentCombinedView::Comment(v) = &timmy_saved[2] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.sara.id, v.comment.creator_id); + } else { + panic!("wrong type"); + } + + // Try unsaving 2 things + CommentSaved::unsave(pool, &save_sara_comment).await?; + PostSaved::unsave(pool, &post_save_form).await?; + + let timmy_saved = PersonSavedCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_eq!(1, timmy_saved.len()); + + if let PersonContentCombinedView::Comment(v) = &timmy_saved[0] { + assert_eq!(data.sara_comment_2.id, v.comment.id); + assert_eq!(data.sara.id, v.comment.creator_id); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views/src/post_report_view.rs b/crates/db_views/src/post_report_view.rs index 9429c258f8..4c7fd676c4 100644 --- a/crates/db_views/src/post_report_view.rs +++ b/crates/db_views/src/post_report_view.rs @@ -1,6 +1,5 @@ -use crate::structs::{LocalUserView, PostReportView}; +use crate::structs::PostReportView; use diesel::{ - pg::Pg, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -11,7 +10,7 @@ use diesel::{ use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases::{self, creator_community_actions}, - newtypes::{CommunityId, PersonId, PostId, PostReportId}, + newtypes::{PersonId, PostReportId}, schema::{ community, community_actions, @@ -24,26 +23,22 @@ use lemmy_db_schema::{ post_report, }, source::community::CommunityFollower, - utils::{ - actions, - actions_alias, - functions::coalesce, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, PostReportView, (PostReportId, PersonId)>, - impl ListFn<'a, PostReportView, (PostReportQuery, &'a LocalUserView)>, -> { - let all_joins = |query: post_report::BoxedQuery<'a, Pg>, my_person_id: PersonId| { - query +impl PostReportView { + /// returns the PostReportView for the provided report_id + /// + /// * `report_id` - the report id to obtain + pub async fn read( + pool: &mut DbPool<'_>, + report_id: PostReportId, + my_person_id: PersonId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + post_report::table + .find(report_id) .inner_join(post::table) .inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(person::table.on(post_report::creator_id.eq(person::id))) @@ -104,322 +99,7 @@ fn queries<'a>() -> Queries< post_aggregates::all_columns, aliases::person2.fields(person::all_columns.nullable()), )) - }; - - let read = move |mut conn: DbConn<'a>, (report_id, my_person_id): (PostReportId, PersonId)| async move { - all_joins( - post_report::table.find(report_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, (options, user): (PostReportQuery, &'a LocalUserView)| async move { - let mut query = all_joins(post_report::table.into_boxed(), user.person.id); - - if let Some(community_id) = options.community_id { - query = query.filter(post::community_id.eq(community_id)); - } - - if let Some(post_id) = options.post_id { - query = query.filter(post::id.eq(post_id)); - } - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(post_report::resolved.eq(false)) - .order_by(post_report::published.asc()); - } else { - query = query.order_by(post_report::published.desc()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query.limit(limit).offset(offset); - - // If its not an admin, get only the ones you mod - if !user.local_user.admin { - query = query.filter(community_actions::became_moderator.is_not_null()); - } - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl PostReportView { - /// returns the PostReportView for the provided report_id - /// - /// * `report_id` - the report id to obtain - pub async fn read( - pool: &mut DbPool<'_>, - report_id: PostReportId, - my_person_id: PersonId, - ) -> Result { - queries().read(pool, (report_id, my_person_id)).await - } - - /// returns the current unresolved post report count for the communities you mod - pub async fn get_report_count( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - admin: bool, - community_id: Option, - ) -> Result { - use diesel::dsl::count; - let conn = &mut get_conn(pool).await?; - let mut query = post_report::table - .inner_join(post::table) - .filter(post_report::resolved.eq(false)) - .into_boxed(); - - if let Some(community_id) = community_id { - query = query.filter(post::community_id.eq(community_id)) - } - - // If its not an admin, get only the ones you mod - if !admin { - query - .inner_join( - community_actions::table.on( - community_actions::community_id - .eq(post::community_id) - .and(community_actions::person_id.eq(my_person_id)) - .and(community_actions::became_moderator.is_not_null()), - ), - ) - .select(count(post_report::id)) - .first::(conn) - .await - } else { - query - .select(count(post_report::id)) - .first::(conn) - .await - } - } -} - -#[derive(Default)] -pub struct PostReportQuery { - pub community_id: Option, - pub post_id: Option, - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl PostReportQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - user: &LocalUserView, - ) -> Result, Error> { - queries().list(pool, (self, user)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::{ - post_report_view::{PostReportQuery, PostReportView}, - structs::LocalUserView, - }; - use lemmy_db_schema::{ - aggregates::structs::PostAggregates, - assert_length, - source::{ - community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm}, - local_user_vote_display_mode::LocalUserVoteDisplayMode, - person::{Person, PersonInsertForm}, - post::{Post, PostInsertForm}, - post_report::{PostReport, PostReportForm}, - }, - traits::{Crud, Joinable, Reportable}, - utils::build_db_pool_for_tests, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_prv"); - - let inserted_timmy = Person::create(pool, &new_person).await?; - - let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id); - let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?; - let timmy_view = LocalUserView { - local_user: timmy_local_user, - local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), - person: inserted_timmy.clone(), - counts: Default::default(), - }; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_prv"); - - let inserted_sara = Person::create(pool, &new_person_2).await?; - - // Add a third person, since new ppl can only report something once. - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_prv"); - - let inserted_jessica = Person::create(pool, &new_person_3).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test community prv".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - // Make timmy a mod - let timmy_moderator_form = CommunityModeratorForm { - community_id: inserted_community.id, - person_id: inserted_timmy.id, - }; - - let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?; - - let new_post = PostInsertForm::new( - "A test post crv".into(), - inserted_timmy.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - // sara reports - let sara_report_form = PostReportForm { - creator_id: inserted_sara.id, - post_id: inserted_post.id, - original_post_name: "Orig post".into(), - original_post_url: None, - original_post_body: None, - reason: "from sara".into(), - }; - - PostReport::report(pool, &sara_report_form).await?; - - let new_post_2 = PostInsertForm::new( - "A test post crv 2".into(), - inserted_timmy.id, - inserted_community.id, - ); - let inserted_post_2 = Post::create(pool, &new_post_2).await?; - - // jessica reports - let jessica_report_form = PostReportForm { - creator_id: inserted_jessica.id, - post_id: inserted_post_2.id, - original_post_name: "Orig post".into(), - original_post_url: None, - original_post_body: None, - reason: "from jessica".into(), - }; - - let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; - - let read_jessica_report_view = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - - // Make sure the triggers are reading the aggregates correctly. - let agg_1 = PostAggregates::read(pool, inserted_post.id).await?; - let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?; - - assert_eq!( - read_jessica_report_view.post_report, - inserted_jessica_report - ); - assert_eq!(read_jessica_report_view.post, inserted_post_2); - assert_eq!(read_jessica_report_view.community.id, inserted_community.id); - assert_eq!(read_jessica_report_view.creator.id, inserted_jessica.id); - assert_eq!(read_jessica_report_view.post_creator.id, inserted_timmy.id); - assert_eq!(read_jessica_report_view.my_vote, None); - assert_eq!(read_jessica_report_view.resolver, None); - assert_eq!(agg_1.report_count, 1); - assert_eq!(agg_1.unresolved_report_count, 1); - assert_eq!(agg_2.report_count, 1); - assert_eq!(agg_2.unresolved_report_count, 1); - - // Do a batch read of timmys reports - let reports = PostReportQuery::default().list(pool, &timmy_view).await?; - - assert_eq!(reports[1].creator.id, inserted_sara.id); - assert_eq!(reports[0].creator.id, inserted_jessica.id); - - // Make sure the counts are correct - let report_count = - PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(2, report_count); - - // Pretend the post was removed, and resolve all reports for that object. - // This is called manually in the API for post removals - PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, inserted_timmy.id) - .await?; - - let read_jessica_report_view_after_resolve = - PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?; - assert!(read_jessica_report_view_after_resolve.post_report.resolved); - assert_eq!( - read_jessica_report_view_after_resolve - .post_report - .resolver_id, - Some(inserted_timmy.id) - ); - assert_eq!( - read_jessica_report_view_after_resolve - .resolver - .map(|r| r.id), - Some(inserted_timmy.id) - ); - - // Make sure the unresolved_post report got decremented in the trigger - let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?; - assert_eq!(agg_2.report_count, 1); - assert_eq!(agg_2.unresolved_report_count, 0); - - // Make sure the other unresolved report isn't changed - let agg_1 = PostAggregates::read(pool, inserted_post.id).await?; - assert_eq!(agg_1.report_count, 1); - assert_eq!(agg_1.unresolved_report_count, 1); - - // Do a batch read of timmys reports - // It should only show saras, which is unresolved - let reports_after_resolve = PostReportQuery { - unresolved_only: true, - ..Default::default() - } - .list(pool, &timmy_view) - .await?; - assert_length!(1, reports_after_resolve); - assert_eq!(reports_after_resolve[0].creator.id, inserted_sara.id); - - // Make sure the counts are correct - let report_count_after_resolved = - PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?; - assert_eq!(1, report_count_after_resolved); - - Person::delete(pool, inserted_timmy.id).await?; - Person::delete(pool, inserted_sara.id).await?; - Person::delete(pool, inserted_jessica.id).await?; - Community::delete(pool, inserted_community.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) + .first(conn) + .await } } diff --git a/crates/db_views/src/post_view.rs b/crates/db_views/src/post_view.rs index c6d1b036f1..e4a65721ec 100644 --- a/crates/db_views/src/post_view.rs +++ b/crates/db_views/src/post_view.rs @@ -287,15 +287,7 @@ fn queries<'a>() -> Queries< query = query.filter(post_aggregates::comments.eq(0)); }; - // If its saved only, then filter, and order by the saved time, not the comment creation time. - if options.saved_only.unwrap_or_default() { - query = query - .filter(post_actions::saved.is_not_null()) - .then_order_by(post_actions::saved.desc()); - } - // Only hide the read posts, if the saved_only is false. Otherwise ppl with the hide_read - // setting wont be able to see saved posts. - else if !options + if !options .show_read .unwrap_or(options.local_user.show_read_posts()) { @@ -488,7 +480,6 @@ pub struct PostQuery<'a> { pub local_user: Option<&'a LocalUser>, pub search_term: Option, pub url_only: Option, - pub saved_only: Option, pub liked_only: Option, pub disliked_only: Option, pub title_only: Option, @@ -646,13 +637,11 @@ mod tests { PostLikeForm, PostRead, PostReadForm, - PostSaved, - PostSavedForm, PostUpdateForm, }, site::Site, }, - traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable}, + traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable}, utils::{build_db_pool, build_db_pool_for_tests, get_conn, uplete, DbPool, RANK_DEFAULT}, CommunityVisibility, PostSortType, @@ -1090,34 +1079,6 @@ mod tests { cleanup(data, pool).await } - #[tokio::test] - #[serial] - async fn post_listing_saved_only() -> LemmyResult<()> { - let pool = &build_db_pool()?; - let pool = &mut pool.into(); - let data = init_data(pool).await?; - - // Save only the bot post - // The saved_only should only show the bot post - 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 - let read_saved_post_listing = PostQuery { - community_id: Some(data.inserted_community.id), - saved_only: Some(true), - ..data.default_post_query() - } - .list(&data.site, pool) - .await?; - - // This should only include the bot post, not the one you created - assert_eq!(vec![POST_BY_BOT], names(&read_saved_post_listing)); - - cleanup(data, pool).await - } - #[tokio::test] #[serial] async fn creator_info() -> LemmyResult<()> { diff --git a/crates/db_views/src/private_message_report_view.rs b/crates/db_views/src/private_message_report_view.rs index e59d996089..956ccf0e1b 100644 --- a/crates/db_views/src/private_message_report_view.rs +++ b/crates/db_views/src/private_message_report_view.rs @@ -1,76 +1,13 @@ use crate::structs::PrivateMessageReportView; -use diesel::{ - pg::Pg, - result::Error, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; +use diesel::{result::Error, ExpressionMethods, JoinOnDsl, NullableExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ aliases, newtypes::PrivateMessageReportId, schema::{person, private_message, private_message_report}, - utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, + utils::{get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, PrivateMessageReportView, PrivateMessageReportId>, - impl ListFn<'a, PrivateMessageReportView, PrivateMessageReportQuery>, -> { - let all_joins = - |query: private_message_report::BoxedQuery<'a, Pg>| { - query - .inner_join(private_message::table) - .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .inner_join( - aliases::person1 - .on(private_message_report::creator_id.eq(aliases::person1.field(person::id))), - ) - .left_join(aliases::person2.on( - private_message_report::resolver_id.eq(aliases::person2.field(person::id).nullable()), - )) - .select(( - private_message_report::all_columns, - private_message::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - aliases::person2.fields(person::all_columns).nullable(), - )) - }; - - let read = move |mut conn: DbConn<'a>, report_id: PrivateMessageReportId| async move { - all_joins(private_message_report::table.find(report_id).into_boxed()) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, options: PrivateMessageReportQuery| async move { - let mut query = all_joins(private_message_report::table.into_boxed()); - - // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest - // first (FIFO) - if options.unresolved_only { - query = query - .filter(private_message_report::resolved.eq(false)) - .order_by(private_message_report::published.asc()); - } else { - query = query.order_by(private_message_report::published.desc()); - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query - .limit(limit) - .offset(offset) - .load::(&mut conn) - .await - }; - - Queries::new(read, list) -} - impl PrivateMessageReportView { /// returns the PrivateMessageReportView for the provided report_id /// @@ -79,118 +16,28 @@ impl PrivateMessageReportView { pool: &mut DbPool<'_>, report_id: PrivateMessageReportId, ) -> Result { - queries().read(pool, report_id).await - } - - /// Returns the current unresolved post report count for the communities you mod - pub async fn get_report_count(pool: &mut DbPool<'_>) -> Result { - use diesel::dsl::count; let conn = &mut get_conn(pool).await?; - private_message_report::table + .find(report_id) .inner_join(private_message::table) - .filter(private_message_report::resolved.eq(false)) - .into_boxed() - .select(count(private_message_report::id)) - .first::(conn) + .inner_join(person::table.on(private_message::creator_id.eq(person::id))) + .inner_join( + aliases::person1 + .on(private_message_report::creator_id.eq(aliases::person1.field(person::id))), + ) + .left_join( + aliases::person2.on( + private_message_report::resolver_id.eq(aliases::person2.field(person::id).nullable()), + ), + ) + .select(( + private_message_report::all_columns, + private_message::all_columns, + person::all_columns, + aliases::person1.fields(person::all_columns), + aliases::person2.fields(person::all_columns).nullable(), + )) + .first(conn) .await } } - -#[derive(Default)] -pub struct PrivateMessageReportQuery { - pub page: Option, - pub limit: Option, - pub unresolved_only: bool, -} - -impl PrivateMessageReportQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, self).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::private_message_report_view::PrivateMessageReportQuery; - use lemmy_db_schema::{ - assert_length, - source::{ - instance::Instance, - person::{Person, PersonInsertForm}, - private_message::{PrivateMessage, PrivateMessageInsertForm}, - private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, - }, - traits::{Crud, Reportable}, - utils::build_db_pool_for_tests, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person_1 = PersonInsertForm::test_form(inserted_instance.id, "timmy_mrv"); - let inserted_timmy = Person::create(pool, &new_person_1).await?; - - let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); - let inserted_jessica = Person::create(pool, &new_person_2).await?; - - // timmy sends private message to jessica - let pm_form = PrivateMessageInsertForm::new( - inserted_timmy.id, - inserted_jessica.id, - "something offensive".to_string(), - ); - let pm = PrivateMessage::create(pool, &pm_form).await?; - - // jessica reports private message - let pm_report_form = PrivateMessageReportForm { - creator_id: inserted_jessica.id, - original_pm_text: pm.content.clone(), - private_message_id: pm.id, - reason: "its offensive".to_string(), - }; - let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; - - let reports = PrivateMessageReportQuery::default().list(pool).await?; - assert_length!(1, reports); - assert!(!reports[0].private_message_report.resolved); - assert_eq!(inserted_timmy.name, reports[0].private_message_creator.name); - assert_eq!(inserted_jessica.name, reports[0].creator.name); - assert_eq!(pm_report.reason, reports[0].private_message_report.reason); - assert_eq!(pm.content, reports[0].private_message.content); - - let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "admin_mrv"); - let inserted_admin = Person::create(pool, &new_person_3).await?; - - // admin resolves the report (after taking appropriate action) - PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id).await?; - - let reports = PrivateMessageReportQuery { - unresolved_only: (false), - ..Default::default() - } - .list(pool) - .await?; - assert_length!(1, reports); - assert!(reports[0].private_message_report.resolved); - assert!(reports[0].resolver.is_some()); - assert_eq!( - Some(&inserted_admin.name), - reports[0].resolver.as_ref().map(|r| &r.name) - ); - - Instance::delete(pool, inserted_instance.id).await?; - - Ok(()) - } -} diff --git a/crates/db_views/src/private_message_view.rs b/crates/db_views/src/private_message_view.rs deleted file mode 100644 index 2286b7dc67..0000000000 --- a/crates/db_views/src/private_message_view.rs +++ /dev/null @@ -1,397 +0,0 @@ -use crate::structs::PrivateMessageView; -use diesel::{ - debug_query, - pg::Pg, - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - aliases, - newtypes::{PersonId, PrivateMessageId}, - schema::{instance_actions, person, person_actions, private_message}, - utils::{actions, get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn}, -}; -use tracing::debug; - -fn queries<'a>() -> Queries< - impl ReadFn<'a, PrivateMessageView, PrivateMessageId>, - impl ListFn<'a, PrivateMessageView, (PrivateMessageQuery, PersonId)>, -> { - let all_joins = |query: private_message::BoxedQuery<'a, Pg>| { - query - .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .inner_join( - aliases::person1.on(private_message::recipient_id.eq(aliases::person1.field(person::id))), - ) - .left_join(actions( - person_actions::table, - Some(aliases::person1.field(person::id)), - private_message::creator_id, - )) - .left_join(actions( - instance_actions::table, - Some(aliases::person1.field(person::id)), - person::instance_id, - )) - }; - - let selection = ( - private_message::all_columns, - person::all_columns, - aliases::person1.fields(person::all_columns), - ); - - let read = move |mut conn: DbConn<'a>, private_message_id: PrivateMessageId| async move { - all_joins(private_message::table.find(private_message_id).into_boxed()) - .order_by(private_message::published.desc()) - .select(selection) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, - (options, recipient_id): (PrivateMessageQuery, PersonId)| async move { - let mut query = all_joins(private_message::table.into_boxed()) - .select(selection) - // Dont show replies from blocked users - .filter(person_actions::blocked.is_null()) - // Dont show replies from blocked instances - .filter(instance_actions::blocked.is_null()); - - // If its unread, I only want the ones to me - if options.unread_only { - query = query.filter(private_message::read.eq(false)); - if let Some(i) = options.creator_id { - query = query.filter(private_message::creator_id.eq(i)) - } - query = query.filter(private_message::recipient_id.eq(recipient_id)); - } - // Otherwise, I want the ALL view to show both sent and received - else { - query = query.filter( - private_message::recipient_id - .eq(recipient_id) - .or(private_message::creator_id.eq(recipient_id)), - ); - if let Some(i) = options.creator_id { - query = query.filter( - private_message::creator_id - .eq(i) - .or(private_message::recipient_id.eq(i)), - ) - } - } - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query = query - .filter(private_message::deleted.eq(false)) - .limit(limit) - .offset(offset) - .order_by(private_message::published.desc()); - - debug!( - "Private Message View Query: {:?}", - debug_query::(&query) - ); - - query.load::(&mut conn).await - }; - - Queries::new(read, list) -} - -impl PrivateMessageView { - pub async fn read( - pool: &mut DbPool<'_>, - private_message_id: PrivateMessageId, - ) -> Result { - queries().read(pool, private_message_id).await - } - - /// Gets the number of unread messages - pub async fn get_unread_messages( - pool: &mut DbPool<'_>, - my_person_id: PersonId, - ) -> Result { - use diesel::dsl::count; - let conn = &mut get_conn(pool).await?; - private_message::table - // Necessary to get the senders instance_id - .inner_join(person::table.on(private_message::creator_id.eq(person::id))) - .left_join(actions( - person_actions::table, - Some(my_person_id), - private_message::creator_id, - )) - .left_join(actions( - instance_actions::table, - Some(my_person_id), - person::instance_id, - )) - // Dont count replies from blocked users - .filter(person_actions::blocked.is_null()) - // Dont count replies from blocked instances - .filter(instance_actions::blocked.is_null()) - .filter(private_message::read.eq(false)) - .filter(private_message::recipient_id.eq(my_person_id)) - .filter(private_message::deleted.eq(false)) - .select(count(private_message::id)) - .first::(conn) - .await - } -} - -#[derive(Default)] -pub struct PrivateMessageQuery { - pub unread_only: bool, - pub page: Option, - pub limit: Option, - pub creator_id: Option, -} - -impl PrivateMessageQuery { - pub async fn list( - self, - pool: &mut DbPool<'_>, - recipient_id: PersonId, - ) -> Result, Error> { - queries().list(pool, (self, recipient_id)).await - } -} - -#[cfg(test)] -#[expect(clippy::indexing_slicing)] -mod tests { - - use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView}; - use lemmy_db_schema::{ - assert_length, - newtypes::InstanceId, - source::{ - instance::Instance, - instance_block::{InstanceBlock, InstanceBlockForm}, - person::{Person, PersonInsertForm}, - person_block::{PersonBlock, PersonBlockForm}, - private_message::{PrivateMessage, PrivateMessageInsertForm}, - }, - traits::{Blockable, Crud}, - utils::{build_db_pool_for_tests, DbPool}, - }; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - struct Data { - instance: Instance, - timmy: Person, - jess: Person, - sara: Person, - } - - async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { - let message_content = String::new(); - - let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_rav"); - - let timmy = Person::create(pool, &timmy_form).await?; - - let sara_form = PersonInsertForm::test_form(instance.id, "sara_rav"); - - let sara = Person::create(pool, &sara_form).await?; - - let jess_form = PersonInsertForm::test_form(instance.id, "jess_rav"); - - let jess = Person::create(pool, &jess_form).await?; - - let sara_timmy_message_form = - PrivateMessageInsertForm::new(sara.id, timmy.id, message_content.clone()); - PrivateMessage::create(pool, &sara_timmy_message_form).await?; - - let sara_jess_message_form = - PrivateMessageInsertForm::new(sara.id, jess.id, message_content.clone()); - PrivateMessage::create(pool, &sara_jess_message_form).await?; - - let timmy_sara_message_form = - PrivateMessageInsertForm::new(timmy.id, sara.id, message_content.clone()); - PrivateMessage::create(pool, &timmy_sara_message_form).await?; - - let jess_timmy_message_form = - PrivateMessageInsertForm::new(jess.id, timmy.id, message_content.clone()); - PrivateMessage::create(pool, &jess_timmy_message_form).await?; - - Ok(Data { - instance, - timmy, - jess, - sara, - }) - } - - async fn cleanup(instance_id: InstanceId, pool: &mut DbPool<'_>) -> LemmyResult<()> { - // This also deletes all persons and private messages thanks to sql `on delete cascade` - Instance::delete(pool, instance_id).await?; - Ok(()) - } - - #[tokio::test] - #[serial] - async fn read_private_messages() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let Data { - timmy, - jess, - sara, - instance, - } = init_data(pool).await?; - - let timmy_messages = PrivateMessageQuery { - unread_only: false, - creator_id: None, - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(3, &timmy_messages); - assert_eq!(timmy_messages[0].creator.id, jess.id); - assert_eq!(timmy_messages[0].recipient.id, timmy.id); - assert_eq!(timmy_messages[1].creator.id, timmy.id); - assert_eq!(timmy_messages[1].recipient.id, sara.id); - assert_eq!(timmy_messages[2].creator.id, sara.id); - assert_eq!(timmy_messages[2].recipient.id, timmy.id); - - let timmy_unread_messages = PrivateMessageQuery { - unread_only: true, - creator_id: None, - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(2, &timmy_unread_messages); - assert_eq!(timmy_unread_messages[0].creator.id, jess.id); - assert_eq!(timmy_unread_messages[0].recipient.id, timmy.id); - assert_eq!(timmy_unread_messages[1].creator.id, sara.id); - assert_eq!(timmy_unread_messages[1].recipient.id, timmy.id); - - let timmy_sara_messages = PrivateMessageQuery { - unread_only: false, - creator_id: Some(sara.id), - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(2, &timmy_sara_messages); - assert_eq!(timmy_sara_messages[0].creator.id, timmy.id); - assert_eq!(timmy_sara_messages[0].recipient.id, sara.id); - assert_eq!(timmy_sara_messages[1].creator.id, sara.id); - assert_eq!(timmy_sara_messages[1].recipient.id, timmy.id); - - let timmy_sara_unread_messages = PrivateMessageQuery { - unread_only: true, - creator_id: Some(sara.id), - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(1, &timmy_sara_unread_messages); - assert_eq!(timmy_sara_unread_messages[0].creator.id, sara.id); - assert_eq!(timmy_sara_unread_messages[0].recipient.id, timmy.id); - - cleanup(instance.id, pool).await - } - - #[tokio::test] - #[serial] - async fn ensure_person_block() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let Data { - timmy, - sara, - instance, - jess: _, - } = init_data(pool).await?; - - // Make sure blocks are working - let timmy_blocks_sara_form = PersonBlockForm { - person_id: timmy.id, - target_id: sara.id, - }; - - let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form).await?; - - let expected_block = PersonBlock { - person_id: timmy.id, - target_id: sara.id, - published: inserted_block.published, - }; - assert_eq!(expected_block, inserted_block); - - let timmy_messages = PrivateMessageQuery { - unread_only: true, - creator_id: None, - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(1, &timmy_messages); - - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; - assert_eq!(timmy_unread_messages, 1); - - cleanup(instance.id, pool).await - } - - #[tokio::test] - #[serial] - async fn ensure_instance_block() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - let Data { - timmy, - jess: _, - sara, - instance, - } = init_data(pool).await?; - // Make sure instance_blocks are working - let timmy_blocks_instance_form = InstanceBlockForm { - person_id: timmy.id, - instance_id: sara.instance_id, - }; - - let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form).await?; - - let expected_instance_block = InstanceBlock { - person_id: timmy.id, - instance_id: sara.instance_id, - published: inserted_instance_block.published, - }; - assert_eq!(expected_instance_block, inserted_instance_block); - - let timmy_messages = PrivateMessageQuery { - unread_only: true, - creator_id: None, - ..Default::default() - } - .list(pool, timmy.id) - .await?; - - assert_length!(0, &timmy_messages); - - let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?; - assert_eq!(timmy_unread_messages, 0); - cleanup(instance.id, pool).await - } -} diff --git a/crates/db_views/src/report_combined_view.rs b/crates/db_views/src/report_combined_view.rs new file mode 100644 index 0000000000..0a103cfc22 --- /dev/null +++ b/crates/db_views/src/report_combined_view.rs @@ -0,0 +1,972 @@ +use crate::structs::{ + CommentReportView, + LocalUserView, + PostReportView, + PrivateMessageReportView, + ReportCombinedPaginationCursor, + ReportCombinedView, + ReportCombinedViewInternal, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + PgExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::CommunityId, + schema::{ + comment, + comment_actions, + comment_aggregates, + comment_report, + community, + community_actions, + local_user, + person, + person_actions, + post, + post_actions, + post_aggregates, + post_report, + private_message, + private_message_report, + report_combined, + }, + source::{ + combined::report::{report_combined_keys as key, ReportCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool, ReverseTimestampKey}, + InternalToCombinedView, +}; +use lemmy_utils::error::LemmyResult; + +impl ReportCombinedViewInternal { + /// returns the current unresolved report count for the communities you mod + pub async fn get_report_count( + pool: &mut DbPool<'_>, + user: &LocalUserView, + community_id: Option, + ) -> Result { + use diesel::dsl::count; + + let conn = &mut get_conn(pool).await?; + let my_person_id = user.local_user.person_id; + + let mut query = report_combined::table + .left_join(post_report::table) + .left_join(comment_report::table) + .left_join(private_message_report::table) + // Need to join to comment and post to get the community + .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) + // The post + .left_join( + post::table.on( + post_report::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)), + ), + ) + .left_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .filter( + post_report::resolved + .or(comment_report::resolved) + .or(private_message_report::resolved) + .is_distinct_from(true), + ) + .into_boxed(); + + if let Some(community_id) = community_id { + query = query.filter(post::community_id.eq(community_id)) + } + + // If its not an admin, get only the ones you mod + if !user.local_user.admin { + query = query.filter(community_actions::became_moderator.is_not_null()); + } + + query + .select(count(report_combined::id)) + .first::(conn) + .await + } +} + +impl ReportCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &ReportCombinedView) -> ReportCombinedPaginationCursor { + let (prefix, id) = match view { + ReportCombinedView::Comment(v) => ('C', v.comment_report.id.0), + ReportCombinedView::Post(v) => ('P', v.post_report.id.0), + ReportCombinedView::PrivateMessage(v) => ('M', v.private_message_report.id.0), + }; + // hex encoding to prevent ossification + ReportCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = report_combined::table + .select(ReportCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "C" => query.filter(report_combined::comment_report_id.eq(id)), + "P" => query.filter(report_combined::post_report_id.eq(id)), + "M" => query.filter(report_combined::private_message_report_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(ReportCombined); + +#[derive(Default)] +pub struct ReportCombinedQuery { + pub community_id: Option, + pub unresolved_only: Option, + pub page_after: Option, + pub page_back: Option, +} + +impl ReportCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + user: &LocalUserView, + ) -> LemmyResult> { + let my_person_id = user.local_user.person_id; + let report_creator = person::id; + let item_creator = aliases::person1.field(person::id); + let resolver = aliases::person2.field(person::id).nullable(); + + let conn = &mut get_conn(pool).await?; + + // Notes: since the post_report_id and comment_report_id are optional columns, + // many joins must use an OR condition. + // For example, the report creator must be the person table joined to either: + // - post_report.creator_id + // - comment_report.creator_id + let mut query = report_combined::table + .left_join(post_report::table) + .left_join(comment_report::table) + .left_join(private_message_report::table) + // The report creator + .inner_join( + person::table.on( + post_report::creator_id + .eq(report_creator) + .or(comment_report::creator_id.eq(report_creator)) + .or(private_message_report::creator_id.eq(report_creator)), + ), + ) + // The comment + .left_join(comment::table.on(comment_report::comment_id.eq(comment::id))) + // The private message + .left_join( + private_message::table + .on(private_message_report::private_message_id.eq(private_message::id)), + ) + // The post + .left_join( + post::table.on( + post_report::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)), + ), + ) + // The item creator (`item_creator` is the id of this person) + .inner_join( + aliases::person1.on( + post::creator_id + .eq(item_creator) + .or(comment::creator_id.eq(item_creator)) + .or(private_message::creator_id.eq(item_creator)), + ), + ) + // The community + .left_join(community::table.on(post::community_id.eq(community::id))) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + .left_join(post_aggregates::table.on(post_report::post_id.eq(post_aggregates::post_id))) + .left_join( + comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)), + ) + // The resolver + .left_join( + aliases::person2.on( + private_message_report::resolver_id + .eq(resolver) + .or(post_report::resolver_id.eq(resolver)) + .or(comment_report::resolver_id.eq(resolver)), + ), + ) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment_report::comment_id, + )) + .select(( + // Post-specific + post_report::all_columns.nullable(), + post::all_columns.nullable(), + post_aggregates::all_columns.nullable(), + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ) + .nullable(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + // Comment-specific + comment_report::all_columns.nullable(), + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + // Private-message-specific + private_message_report::all_columns.nullable(), + private_message::all_columns.nullable(), + // Shared + person::all_columns, + aliases::person1.fields(person::all_columns), + community::all_columns.nullable(), + CommunityFollower::select_subscribed_type(), + aliases::person2.fields(person::all_columns.nullable()), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + )) + .into_boxed(); + + if let Some(community_id) = self.community_id { + query = query.filter(community::id.eq(community_id)); + } + + // If its not an admin, get only the ones you mod + if !user.local_user.admin { + query = query.filter(community_actions::became_moderator.is_not_null()); + } + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // If viewing all reports, order by newest, but if viewing unresolved only, show the oldest + // first (FIFO) + if self.unresolved_only.unwrap_or_default() { + query = query + .filter( + post_report::resolved + .or(comment_report::resolved) + .or(private_message_report::resolved) + .is_distinct_from(true), + ) + // TODO: when a `then_asc` method is added, use it here, make the id sort direction match, + // and remove the separate index; unless additional columns are added to this sort + .then_desc(ReverseTimestampKey(key::published)); + } else { + query = query.then_desc(key::published); + } + + // Tie breaker + query = query.then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +impl InternalToCombinedView for ReportCombinedViewInternal { + type CombinedView = ReportCombinedView; + + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = ( + v.post_report, + v.post.clone(), + v.community.clone(), + v.post_unread_comments, + v.post_counts, + ) { + Some(ReportCombinedView::Post(PostReportView { + post_report, + post, + community, + unread_comments, + counts, + creator: v.report_creator, + post_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + resolver: v.resolver, + })) + } else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = ( + v.comment_report, + v.comment, + v.comment_counts, + v.post, + v.community, + ) { + Some(ReportCombinedView::Comment(CommentReportView { + comment_report, + comment, + counts, + post, + community, + creator: v.report_creator, + comment_creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + resolver: v.resolver, + })) + } else if let (Some(private_message_report), Some(private_message)) = + (v.private_message_report, v.private_message) + { + Some(ReportCombinedView::PrivateMessage( + PrivateMessageReportView { + private_message_report, + private_message, + creator: v.report_creator, + private_message_creator: v.item_creator, + resolver: v.resolver, + }, + )) + } else { + None + } + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{ + report_combined_view::ReportCombinedQuery, + structs::{ + CommentReportView, + LocalUserView, + PostReportView, + ReportCombinedView, + ReportCombinedViewInternal, + }, + }; + use lemmy_db_schema::{ + aggregates::structs::{CommentAggregates, PostAggregates}, + assert_length, + source::{ + comment::{Comment, CommentInsertForm}, + comment_report::{CommentReport, CommentReportForm}, + community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm}, + instance::Instance, + local_user::{LocalUser, LocalUserInsertForm}, + local_user_vote_display_mode::LocalUserVoteDisplayMode, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, + post_report::{PostReport, PostReportForm}, + private_message::{PrivateMessage, PrivateMessageInsertForm}, + private_message_report::{PrivateMessageReport, PrivateMessageReportForm}, + }, + traits::{Crud, Joinable, Reportable}, + utils::{build_db_pool_for_tests, DbPool}, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + sara: Person, + jessica: Person, + timmy_view: LocalUserView, + admin_view: LocalUserView, + community: Community, + post: Post, + post_2: Post, + comment: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rcv"); + let inserted_timmy = Person::create(pool, &timmy_form).await?; + let timmy_local_user_form = LocalUserInsertForm::test_form(inserted_timmy.id); + let timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![]).await?; + let timmy_view = LocalUserView { + local_user: timmy_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_timmy.clone(), + counts: Default::default(), + }; + + // Make an admin, to be able to see private message reports. + let admin_form = PersonInsertForm::test_form(inserted_instance.id, "admin_rcv"); + let inserted_admin = Person::create(pool, &admin_form).await?; + let admin_local_user_form = LocalUserInsertForm::test_form_admin(inserted_admin.id); + let admin_local_user = LocalUser::create(pool, &admin_local_user_form, vec![]).await?; + let admin_view = LocalUserView { + local_user: admin_local_user, + local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), + person: inserted_admin.clone(), + counts: Default::default(), + }; + + let sara_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rcv"); + let inserted_sara = Person::create(pool, &sara_form).await?; + + let jessica_form = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv"); + let inserted_jessica = Person::create(pool, &jessica_form).await?; + + let community_form = CommunityInsertForm::new( + inserted_instance.id, + "test community crv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let inserted_community = Community::create(pool, &community_form).await?; + + // Make timmy a mod + let timmy_moderator_form = CommunityModeratorForm { + community_id: inserted_community.id, + person_id: inserted_timmy.id, + }; + CommunityModerator::join(pool, &timmy_moderator_form).await?; + + let post_form = PostInsertForm::new( + "A test post crv".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post = Post::create(pool, &post_form).await?; + + let new_post_2 = PostInsertForm::new( + "A test post crv 2".into(), + inserted_timmy.id, + inserted_community.id, + ); + let inserted_post_2 = Post::create(pool, &new_post_2).await?; + + // Timmy creates a comment + let comment_form = CommentInsertForm::new( + inserted_timmy.id, + inserted_post.id, + "A test comment rv".into(), + ); + let inserted_comment = Comment::create(pool, &comment_form, None).await?; + + Ok(Data { + instance: inserted_instance, + timmy: inserted_timmy, + sara: inserted_sara, + jessica: inserted_jessica, + admin_view, + timmy_view, + community: inserted_community, + post: inserted_post, + post_2: inserted_post_2, + comment: inserted_comment, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_combined() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports the post + let sara_report_post_form = PostReportForm { + creator_id: data.sara.id, + post_id: data.post.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from sara".into(), + }; + let inserted_post_report = PostReport::report(pool, &sara_report_post_form).await?; + + // Sara reports the comment + let sara_report_comment_form = CommentReportForm { + creator_id: data.sara.id, + comment_id: data.comment.id, + original_comment_text: "A test comment rv".into(), + reason: "from sara".into(), + }; + CommentReport::report(pool, &sara_report_comment_form).await?; + + // Timmy creates a private message report + let pm_form = PrivateMessageInsertForm::new( + data.timmy.id, + data.sara.id, + "something offensive crv".to_string(), + ); + let inserted_pm = PrivateMessage::create(pool, &pm_form).await?; + + // sara reports private message + let pm_report_form = PrivateMessageReportForm { + creator_id: data.sara.id, + original_pm_text: inserted_pm.content.clone(), + private_message_id: inserted_pm.id, + reason: "its offensive".to_string(), + }; + PrivateMessageReport::report(pool, &pm_report_form).await?; + + // Do a batch read of admins reports + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_eq!(3, reports.len()); + + // Make sure the report types are correct + if let ReportCombinedView::Post(v) = &reports[2] { + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy.id, v.post_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[1] { + assert_eq!(data.comment.id, v.comment.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.comment_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert_eq!(inserted_pm.id, v.private_message.id); + } else { + panic!("wrong type"); + } + + let report_count_admin = + ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?; + assert_eq!(3, report_count_admin); + + // Timmy should only see 2 reports, since they're not an admin, + // but they do mod the community + let reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + assert_eq!(2, reports.len()); + + // Make sure the report types are correct + if let ReportCombinedView::Post(v) = &reports[1] { + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy.id, v.post_creator.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[0] { + assert_eq!(data.comment.id, v.comment.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.comment_creator.id); + } else { + panic!("wrong type"); + } + + let report_count_timmy = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count_timmy); + + // Resolve the post report + PostReport::resolve(pool, inserted_post_report.id, data.timmy.id).await?; + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + assert_length!(1, reports_after_resolve); + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_private_message_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // timmy sends private message to jessica + let pm_form = PrivateMessageInsertForm::new( + data.timmy.id, + data.jessica.id, + "something offensive".to_string(), + ); + let pm = PrivateMessage::create(pool, &pm_form).await?; + + // jessica reports private message + let pm_report_form = PrivateMessageReportForm { + creator_id: data.jessica.id, + original_pm_text: pm.content.clone(), + private_message_id: pm.id, + reason: "its offensive".to_string(), + }; + let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?; + + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, reports); + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert!(!v.private_message_report.resolved); + assert_eq!(data.timmy.name, v.private_message_creator.name); + assert_eq!(data.jessica.name, v.creator.name); + assert_eq!(pm_report.reason, v.private_message_report.reason); + assert_eq!(pm.content, v.private_message.content); + } else { + panic!("wrong type"); + } + + // admin resolves the report (after taking appropriate action) + PrivateMessageReport::resolve(pool, pm_report.id, data.admin_view.person.id).await?; + + let reports = ReportCombinedQuery::default() + .list(pool, &data.admin_view) + .await?; + assert_length!(1, reports); + if let ReportCombinedView::PrivateMessage(v) = &reports[0] { + assert!(v.private_message_report.resolved); + assert!(v.resolver.is_some()); + assert_eq!( + Some(&data.admin_view.person.name), + v.resolver.as_ref().map(|r| &r.name) + ); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_post_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports + let sara_report_form = PostReportForm { + creator_id: data.sara.id, + post_id: data.post.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from sara".into(), + }; + + PostReport::report(pool, &sara_report_form).await?; + + // jessica reports + let jessica_report_form = PostReportForm { + creator_id: data.jessica.id, + post_id: data.post_2.id, + original_post_name: "Orig post".into(), + original_post_url: None, + original_post_body: None, + reason: "from jessica".into(), + }; + + let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?; + + let read_jessica_report_view = + PostReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + + // Make sure the triggers are reading the aggregates correctly. + let agg_1 = PostAggregates::read(pool, data.post.id).await?; + let agg_2 = PostAggregates::read(pool, data.post_2.id).await?; + + assert_eq!( + read_jessica_report_view.post_report, + inserted_jessica_report + ); + assert_eq!(read_jessica_report_view.post, data.post_2); + assert_eq!(read_jessica_report_view.community.id, data.community.id); + assert_eq!(read_jessica_report_view.creator.id, data.jessica.id); + assert_eq!(read_jessica_report_view.post_creator.id, data.timmy.id); + assert_eq!(read_jessica_report_view.my_vote, None); + assert_eq!(read_jessica_report_view.resolver, None); + assert_eq!(agg_1.report_count, 1); + assert_eq!(agg_1.unresolved_report_count, 1); + assert_eq!(agg_2.report_count, 1); + assert_eq!(agg_2.unresolved_report_count, 1); + + // Do a batch read of timmys reports + let reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Post(v) = &reports[1] { + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Post(v) = &reports[0] { + assert_eq!(v.creator.id, data.jessica.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count); + + // Pretend the post was removed, and resolve all reports for that object. + // This is called manually in the API for post removals + PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, data.timmy.id) + .await?; + + let read_jessica_report_view_after_resolve = + PostReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + assert!(read_jessica_report_view_after_resolve.post_report.resolved); + assert_eq!( + read_jessica_report_view_after_resolve + .post_report + .resolver_id, + Some(data.timmy.id) + ); + assert_eq!( + read_jessica_report_view_after_resolve + .resolver + .map(|r| r.id), + Some(data.timmy.id) + ); + + // Make sure the unresolved_post report got decremented in the trigger + let agg_2 = PostAggregates::read(pool, data.post_2.id).await?; + assert_eq!(agg_2.report_count, 1); + assert_eq!(agg_2.unresolved_report_count, 0); + + // Make sure the other unresolved report isn't changed + let agg_1 = PostAggregates::read(pool, data.post.id).await?; + assert_eq!(agg_1.report_count, 1); + assert_eq!(agg_1.unresolved_report_count, 1); + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Post(v) = &reports_after_resolve[0] { + assert_length!(1, reports_after_resolve); + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn test_comment_reports() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // sara reports + let sara_report_form = CommentReportForm { + creator_id: data.sara.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from sara".into(), + }; + + CommentReport::report(pool, &sara_report_form).await?; + + // jessica reports + let jessica_report_form = CommentReportForm { + creator_id: data.jessica.id, + comment_id: data.comment.id, + original_comment_text: "this was it at time of creation".into(), + reason: "from jessica".into(), + }; + + let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?; + + let agg = CommentAggregates::read(pool, data.comment.id).await?; + assert_eq!(agg.report_count, 2); + + let read_jessica_report_view = + CommentReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + assert_eq!(read_jessica_report_view.counts.unresolved_report_count, 2); + + // Do a batch read of timmys reports + let reports = ReportCombinedQuery::default() + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Comment(v) = &reports[0] { + assert_eq!(v.creator.id, data.jessica.id); + } else { + panic!("wrong type"); + } + if let ReportCombinedView::Comment(v) = &reports[1] { + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(2, report_count); + + // Resolve the report + CommentReport::resolve(pool, inserted_jessica_report.id, data.timmy.id).await?; + let read_jessica_report_view_after_resolve = + CommentReportView::read(pool, inserted_jessica_report.id, data.timmy.id).await?; + + assert!( + read_jessica_report_view_after_resolve + .comment_report + .resolved + ); + assert_eq!( + read_jessica_report_view_after_resolve + .comment_report + .resolver_id, + Some(data.timmy.id) + ); + assert_eq!( + read_jessica_report_view_after_resolve + .resolver + .map(|r| r.id), + Some(data.timmy.id) + ); + + // Do a batch read of timmys reports + // It should only show saras, which is unresolved + let reports_after_resolve = ReportCombinedQuery { + unresolved_only: Some(true), + ..Default::default() + } + .list(pool, &data.timmy_view) + .await?; + + if let ReportCombinedView::Comment(v) = &reports_after_resolve[0] { + assert_length!(1, reports_after_resolve); + assert_eq!(v.creator.id, data.sara.id); + } else { + panic!("wrong type"); + } + + // Make sure the counts are correct + let report_count_after_resolved = + ReportCombinedViewInternal::get_report_count(pool, &data.timmy_view, None).await?; + assert_eq!(1, report_count_after_resolved); + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 4586fbcacd..6aad4af218 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -122,10 +122,28 @@ pub struct PostReportView { /// prevent ossification (api users love to make assumptions (e.g. parse stuff that looks like /// numbers as numbers) about apis that aren't part of the spec #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -#[cfg_attr(feature = "full", derive(ts_rs::TS))] +#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] pub struct PaginationCursor(pub String); +/// like PaginationCursor but for the report_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ReportCombinedPaginationCursor(pub String); + +/// like PaginationCursor but for the person_content_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct PersonContentCombinedPaginationCursor(pub String); + +/// like PaginationCursor but for the person_saved_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct PersonSavedCombinedPaginationCursor(pub String); + #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] @@ -153,17 +171,6 @@ pub struct PostView { pub unread_comments: i64, } -#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// A private message view. -pub struct PrivateMessageView { - pub private_message: PrivateMessage, - pub creator: Person, - pub recipient: Person, -} - #[skip_serializing_none] #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] @@ -237,3 +244,89 @@ pub struct LocalImageView { pub local_image: LocalImage, pub person: Person, } + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined report view +pub struct ReportCombinedViewInternal { + // Post-specific + pub post_report: Option, + pub post: Option, + pub post_counts: Option, + pub post_unread_comments: Option, + pub post_saved: bool, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + // Comment-specific + pub comment_report: Option, + pub comment: Option, + pub comment_counts: Option, + pub comment_saved: bool, + pub my_comment_vote: Option, + // Private-message-specific + pub private_message_report: Option, + pub private_message: Option, + // Shared + pub report_creator: Person, + pub item_creator: Person, + pub community: Option, + pub subscribed: SubscribedType, + pub resolver: Option, + pub item_creator_is_admin: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_is_moderator: bool, + pub item_creator_blocked: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum ReportCombinedView { + Post(PostReportView), + Comment(CommentReportView), + PrivateMessage(PrivateMessageReportView), +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined person_content view +pub struct PersonContentViewInternal { + // Post-specific + pub post_counts: PostAggregates, + pub post_unread_comments: i64, + pub post_saved: bool, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + pub image_details: Option, + // Comment-specific + pub comment: Option, + pub comment_counts: Option, + pub comment_saved: bool, + pub my_comment_vote: Option, + // Shared + pub post: Post, + pub community: Community, + pub item_creator: Person, + pub subscribed: SubscribedType, + pub item_creator_is_admin: bool, + pub item_creator_is_moderator: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_blocked: bool, + pub banned_from_community: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum PersonContentCombinedView { + Post(PostView), + Comment(CommentView), +} diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml index 18a79826bf..85f50d08c9 100644 --- a/crates/db_views_actor/Cargo.toml +++ b/crates/db_views_actor/Cargo.toml @@ -18,6 +18,7 @@ workspace = true full = [ "lemmy_db_schema/full", "lemmy_utils/full", + "i-love-jesus", "diesel", "diesel-async", "ts-rs", @@ -40,10 +41,10 @@ ts-rs = { workspace = true, optional = true } chrono.workspace = true strum = { workspace = true } lemmy_utils = { workspace = true, optional = true } +i-love-jesus = { workspace = true, optional = true } [dev-dependencies] serial_test = { workspace = true } tokio = { workspace = true } pretty_assertions = { workspace = true } url.workspace = true -lemmy_db_views = { workspace = true, features = ["full"] } diff --git a/crates/db_views_actor/src/comment_reply_view.rs b/crates/db_views_actor/src/comment_reply_view.rs index 6c5442e6af..43b527eb6f 100644 --- a/crates/db_views_actor/src/comment_reply_view.rs +++ b/crates/db_views_actor/src/comment_reply_view.rs @@ -1,7 +1,6 @@ use crate::structs::CommentReplyView; use diesel::{ - dsl::{exists, not}, - pg::Pg, + dsl::exists, result::Error, BoolExpressionMethods, ExpressionMethods, @@ -25,36 +24,28 @@ use lemmy_db_schema::{ person_actions, post, }, - source::{community::CommunityFollower, local_user::LocalUser}, - utils::{ - actions, - actions_alias, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, - CommentSortType, + source::community::CommunityFollower, + utils::{actions, actions_alias, get_conn, DbPool}, }; -fn queries<'a>() -> Queries< - impl ReadFn<'a, CommentReplyView, (CommentReplyId, Option)>, - impl ListFn<'a, CommentReplyView, CommentReplyQuery>, -> { - let creator_is_admin = exists( - local_user::table.filter( - comment::creator_id - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ); +impl CommentReplyView { + pub async fn read( + pool: &mut DbPool<'_>, + comment_reply_id: CommentReplyId, + my_person_id: Option, + ) -> Result { + let conn = &mut get_conn(pool).await?; - let all_joins = move |query: comment_reply::BoxedQuery<'a, Pg>, - my_person_id: Option| { - query + let creator_is_admin = exists( + local_user::table.filter( + comment::creator_id + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ); + + comment_reply::table + .find(comment_reply_id) .inner_join(comment::table) .inner_join(person::table.on(comment::creator_id.eq(person::id))) .inner_join(post::table.on(comment::post_id.eq(post::id))) @@ -100,280 +91,7 @@ fn queries<'a>() -> Queries< person_actions::blocked.nullable().is_not_null(), comment_actions::like_score.nullable(), )) - }; - - let read = - move |mut conn: DbConn<'a>, - (comment_reply_id, my_person_id): (CommentReplyId, Option)| async move { - all_joins( - comment_reply::table.find(comment_reply_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, options: CommentReplyQuery| async move { - // These filters need to be kept in sync with the filters in - // CommentReplyView::get_unread_replies() - let mut query = all_joins(comment_reply::table.into_boxed(), options.my_person_id); - - if let Some(recipient_id) = options.recipient_id { - query = query.filter(comment_reply::recipient_id.eq(recipient_id)); - } - - if options.unread_only { - query = query.filter(comment_reply::read.eq(false)); - } - - if !options.show_bot_accounts { - query = query.filter(not(person::bot_account)); - }; - - query = match options.sort.unwrap_or(CommentSortType::New) { - CommentSortType::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()), - CommentSortType::Controversial => { - query.then_order_by(comment_aggregates::controversy_rank.desc()) - } - CommentSortType::New => query.then_order_by(comment_reply::published.desc()), - CommentSortType::Old => query.then_order_by(comment_reply::published.asc()), - CommentSortType::Top => query.order_by(comment_aggregates::score.desc()), - }; - - // Don't show replies from blocked persons - query = query.filter(person_actions::blocked.is_null()); - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query - .limit(limit) - .offset(offset) - .load::(&mut conn) + .first(conn) .await - }; - - Queries::new(read, list) -} - -impl CommentReplyView { - pub async fn read( - pool: &mut DbPool<'_>, - comment_reply_id: CommentReplyId, - my_person_id: Option, - ) -> Result { - queries().read(pool, (comment_reply_id, my_person_id)).await - } - - /// Gets the number of unread replies - pub async fn get_unread_replies( - pool: &mut DbPool<'_>, - local_user: &LocalUser, - ) -> Result { - use diesel::dsl::count; - - let conn = &mut get_conn(pool).await?; - - let mut query = comment_reply::table - .inner_join(comment::table) - .left_join(actions( - person_actions::table, - Some(local_user.person_id), - comment::creator_id, - )) - .inner_join(person::table.on(comment::creator_id.eq(person::id))) - .into_boxed(); - - // These filters need to be kept in sync with the filters in queries().list() - if !local_user.show_bot_accounts { - query = query.filter(not(person::bot_account)); - } - - query - // Don't count replies from blocked users - .filter(person_actions::blocked.is_null()) - .filter(comment_reply::recipient_id.eq(local_user.person_id)) - .filter(comment_reply::read.eq(false)) - .filter(comment::deleted.eq(false)) - .filter(comment::removed.eq(false)) - .select(count(comment_reply::id)) - .first::(conn) - .await - } -} - -#[derive(Default, Clone)] -pub struct CommentReplyQuery { - pub my_person_id: Option, - pub recipient_id: Option, - pub sort: Option, - pub unread_only: bool, - pub show_bot_accounts: bool, - pub page: Option, - pub limit: Option, -} - -impl CommentReplyQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, self).await - } -} - -#[cfg(test)] -mod tests { - - use crate::{comment_reply_view::CommentReplyQuery, structs::CommentReplyView}; - use lemmy_db_schema::{ - source::{ - comment::{Comment, CommentInsertForm}, - comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm}, - community::{Community, CommunityInsertForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, - person::{Person, PersonInsertForm, PersonUpdateForm}, - person_block::{PersonBlock, PersonBlockForm}, - post::{Post, PostInsertForm}, - }, - traits::{Blockable, Crud}, - utils::build_db_pool_for_tests, - }; - use lemmy_db_views::structs::LocalUserView; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let terry_form = PersonInsertForm::test_form(inserted_instance.id, "terrylake"); - let inserted_terry = Person::create(pool, &terry_form).await?; - - let recipient_form = PersonInsertForm { - local: Some(true), - ..PersonInsertForm::test_form(inserted_instance.id, "terrylakes recipient") - }; - - let inserted_recipient = Person::create(pool, &recipient_form).await?; - let recipient_id = inserted_recipient.id; - - let recipient_local_user = - LocalUser::create(pool, &LocalUserInsertForm::test_form(recipient_id), vec![]).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test community lake".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - let new_post = PostInsertForm::new( - "A test post".into(), - inserted_terry.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = - CommentInsertForm::new(inserted_terry.id, inserted_post.id, "A test comment".into()); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let comment_reply_form = CommentReplyInsertForm { - recipient_id: inserted_recipient.id, - comment_id: inserted_comment.id, - read: None, - }; - - let inserted_reply = CommentReply::create(pool, &comment_reply_form).await?; - - let expected_reply = CommentReply { - id: inserted_reply.id, - recipient_id: inserted_reply.recipient_id, - comment_id: inserted_reply.comment_id, - read: false, - published: inserted_reply.published, - }; - - let read_reply = CommentReply::read(pool, inserted_reply.id).await?; - - let comment_reply_update_form = CommentReplyUpdateForm { read: Some(false) }; - let updated_reply = - CommentReply::update(pool, inserted_reply.id, &comment_reply_update_form).await?; - - // Test to make sure counts and blocks work correctly - let unread_replies = CommentReplyView::get_unread_replies(pool, &recipient_local_user).await?; - - let query = CommentReplyQuery { - recipient_id: Some(recipient_id), - my_person_id: Some(recipient_id), - sort: None, - unread_only: false, - show_bot_accounts: true, - page: None, - limit: None, - }; - let replies = query.clone().list(pool).await?; - assert_eq!(1, unread_replies); - assert_eq!(1, replies.len()); - - // Block the person, and make sure these counts are now empty - let block_form = PersonBlockForm { - person_id: recipient_id, - target_id: inserted_terry.id, - }; - PersonBlock::block(pool, &block_form).await?; - - let unread_replies_after_block = - CommentReplyView::get_unread_replies(pool, &recipient_local_user).await?; - let replies_after_block = query.clone().list(pool).await?; - assert_eq!(0, unread_replies_after_block); - assert_eq!(0, replies_after_block.len()); - - // Unblock user so we can reuse the same person - PersonBlock::unblock(pool, &block_form).await?; - - // Turn Terry into a bot account - let person_update_form = PersonUpdateForm { - bot_account: Some(true), - ..Default::default() - }; - Person::update(pool, inserted_terry.id, &person_update_form).await?; - - let recipient_local_user_update_form = LocalUserUpdateForm { - show_bot_accounts: Some(false), - ..Default::default() - }; - LocalUser::update( - pool, - recipient_local_user.id, - &recipient_local_user_update_form, - ) - .await?; - let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id).await?; - - let unread_replies_after_hide_bots = - CommentReplyView::get_unread_replies(pool, &recipient_local_user_view.local_user).await?; - - let mut query_without_bots = query.clone(); - query_without_bots.show_bot_accounts = false; - let replies_after_hide_bots = query_without_bots.list(pool).await?; - assert_eq!(0, unread_replies_after_hide_bots); - assert_eq!(0, replies_after_hide_bots.len()); - - Comment::delete(pool, inserted_comment.id).await?; - Post::delete(pool, inserted_post.id).await?; - Community::delete(pool, inserted_community.id).await?; - Person::delete(pool, inserted_terry.id).await?; - Person::delete(pool, inserted_recipient.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - assert_eq!(expected_reply, read_reply); - assert_eq!(expected_reply, inserted_reply); - assert_eq!(expected_reply, updated_reply); - Ok(()) } } diff --git a/crates/db_views_actor/src/inbox_combined_view.rs b/crates/db_views_actor/src/inbox_combined_view.rs new file mode 100644 index 0000000000..7b28daf2c2 --- /dev/null +++ b/crates/db_views_actor/src/inbox_combined_view.rs @@ -0,0 +1,989 @@ +use crate::structs::{ + CommentReplyView, + InboxCombinedPaginationCursor, + InboxCombinedView, + InboxCombinedViewInternal, + PersonCommentMentionView, + PersonPostMentionView, + PrivateMessageView, +}; +use diesel::{ + dsl::not, + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::PersonId, + schema::{ + comment, + comment_actions, + comment_aggregates, + comment_reply, + community, + community_actions, + image_details, + inbox_combined, + instance_actions, + local_user, + person, + person_actions, + person_comment_mention, + person_post_mention, + post, + post_actions, + post_aggregates, + private_message, + }, + source::{ + combined::inbox::{inbox_combined_keys as key, InboxCombined}, + community::CommunityFollower, + }, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, + InboxDataType, + InternalToCombinedView, +}; +use lemmy_utils::error::LemmyResult; + +impl InboxCombinedViewInternal { + /// Gets the number of unread mentions + pub async fn get_unread_count( + pool: &mut DbPool<'_>, + my_person_id: PersonId, + show_bot_accounts: bool, + ) -> Result { + use diesel::dsl::count; + let conn = &mut get_conn(pool).await?; + + let item_creator = person::id; + let recipient_person = aliases::person1.field(person::id); + + let unread_filter = comment_reply::read + .eq(false) + .or(person_comment_mention::read.eq(false)) + .or(person_post_mention::read.eq(false)) + // If its unread, I only want the messages to me + .or( + private_message::read + .eq(false) + .and(private_message::recipient_id.eq(my_person_id)), + ); + + let item_creator_join = comment::creator_id + .eq(item_creator) + .or( + inbox_combined::person_post_mention_id + .is_not_null() + .and(post::creator_id.eq(item_creator)), + ) + .or(private_message::creator_id.eq(item_creator)); + + let recipient_join = comment_reply::recipient_id + .eq(recipient_person) + .or(person_comment_mention::recipient_id.eq(recipient_person)) + .or(person_post_mention::recipient_id.eq(recipient_person)) + .or(private_message::recipient_id.eq(recipient_person)); + + let comment_join = comment_reply::comment_id + .eq(comment::id) + .or(person_comment_mention::comment_id.eq(comment::id)) + // Filter out the deleted / removed + .and(not(comment::deleted)) + .and(not(comment::removed)); + + let post_join = person_post_mention::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)) + // Filter out the deleted / removed + .and(not(post::deleted)) + .and(not(post::removed)); + + // This could be a simple join, but you need to check for deleted here + let private_message_join = inbox_combined::private_message_id + .eq(private_message::id.nullable()) + .and(not(private_message::deleted)); + + let mut query = inbox_combined::table + .left_join(comment_reply::table) + .left_join(person_comment_mention::table) + .left_join(person_post_mention::table) + .left_join(private_message::table.on(private_message_join)) + .left_join(comment::table.on(comment_join)) + .left_join(post::table.on(post_join)) + // The item creator + .inner_join(person::table.on(item_creator_join)) + // The recipient + .inner_join(aliases::person1.on(recipient_join)) + .left_join(actions( + instance_actions::table, + Some(my_person_id), + person::instance_id, + )) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + // Filter for your user + .filter(recipient_person.eq(my_person_id)) + // Filter unreads + .filter(unread_filter) + // Don't count replies from blocked users + .filter(person_actions::blocked.is_null()) + .filter(instance_actions::blocked.is_null()) + .into_boxed(); + + // These filters need to be kept in sync with the filters in queries().list() + if !show_bot_accounts { + query = query.filter(not(person::bot_account)); + } + + query + .select(count(inbox_combined::id)) + .first::(conn) + .await + } +} + +impl InboxCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &InboxCombinedView) -> InboxCombinedPaginationCursor { + let (prefix, id) = match view { + InboxCombinedView::CommentReply(v) => ('R', v.comment_reply.id.0), + InboxCombinedView::CommentMention(v) => ('C', v.person_comment_mention.id.0), + InboxCombinedView::PostMention(v) => ('P', v.person_post_mention.id.0), + InboxCombinedView::PrivateMessage(v) => ('M', v.private_message.id.0), + }; + // hex encoding to prevent ossification + InboxCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = inbox_combined::table + .select(InboxCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "R" => query.filter(inbox_combined::comment_reply_id.eq(id)), + "C" => query.filter(inbox_combined::person_comment_mention_id.eq(id)), + "P" => query.filter(inbox_combined::person_post_mention_id.eq(id)), + "M" => query.filter(inbox_combined::private_message_id.eq(id)), + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(InboxCombined); + +#[derive(Default)] +pub struct InboxCombinedQuery { + pub type_: Option, + pub unread_only: Option, + pub show_bot_accounts: Option, + pub page_after: Option, + pub page_back: Option, +} + +impl InboxCombinedQuery { + pub async fn list( + self, + pool: &mut DbPool<'_>, + my_person_id: PersonId, + ) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + + let item_creator = person::id; + let recipient_person = aliases::person1.field(person::id); + + let item_creator_join = comment::creator_id + .eq(item_creator) + .or( + inbox_combined::person_post_mention_id + .is_not_null() + .and(post::creator_id.eq(item_creator)), + ) + .or(private_message::creator_id.eq(item_creator)); + + let recipient_join = comment_reply::recipient_id + .eq(recipient_person) + .or(person_comment_mention::recipient_id.eq(recipient_person)) + .or(person_post_mention::recipient_id.eq(recipient_person)) + .or(private_message::recipient_id.eq(recipient_person)); + + let comment_join = comment_reply::comment_id + .eq(comment::id) + .or(person_comment_mention::comment_id.eq(comment::id)) + // Filter out the deleted / removed + .and(not(comment::deleted)) + .and(not(comment::removed)); + + let post_join = person_post_mention::post_id + .eq(post::id) + .or(comment::post_id.eq(post::id)) + // Filter out the deleted / removed + .and(not(post::deleted)) + .and(not(post::removed)); + + // This could be a simple join, but you need to check for deleted here + let private_message_join = inbox_combined::private_message_id + .eq(private_message::id.nullable()) + .and(not(private_message::deleted)); + + let community_join = post::community_id.eq(community::id); + + let mut query = inbox_combined::table + .left_join(comment_reply::table) + .left_join(person_comment_mention::table) + .left_join(person_post_mention::table) + .left_join(private_message::table.on(private_message_join)) + // .left_join(private_message::table) + .left_join(comment::table.on(comment_join)) + .left_join(post::table.on(post_join)) + .left_join(community::table.on(community_join)) + // The item creator + .inner_join(person::table.on(item_creator_join)) + // The recipient + .inner_join(aliases::person1.on(recipient_join)) + .left_join(actions_alias( + creator_community_actions, + item_creator, + post::community_id, + )) + .left_join( + local_user::table.on( + item_creator + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ) + .left_join(actions( + community_actions::table, + Some(my_person_id), + post::community_id, + )) + .left_join(actions( + instance_actions::table, + Some(my_person_id), + person::instance_id, + )) + .left_join(actions(post_actions::table, Some(my_person_id), post::id)) + .left_join(actions( + person_actions::table, + Some(my_person_id), + item_creator, + )) + .left_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .left_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions( + comment_actions::table, + Some(my_person_id), + comment::id, + )) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + .select(( + // Specific + comment_reply::all_columns.nullable(), + person_comment_mention::all_columns.nullable(), + person_post_mention::all_columns.nullable(), + post_aggregates::all_columns.nullable(), + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ) + .nullable(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + post_actions::like_score.nullable(), + image_details::all_columns.nullable(), + private_message::all_columns.nullable(), + // Shared + post::all_columns.nullable(), + community::all_columns.nullable(), + comment::all_columns.nullable(), + comment_aggregates::all_columns.nullable(), + comment_actions::saved.nullable().is_not_null(), + comment_actions::like_score.nullable(), + CommunityFollower::select_subscribed_type(), + person::all_columns, + aliases::person1.fields(person::all_columns), + local_user::admin.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + person_actions::blocked.nullable().is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + )) + .into_boxed(); + + // Filters + if self.unread_only.unwrap_or_default() { + query = query + // The recipient filter (IE only show replies to you) + .filter(recipient_person.eq(my_person_id)) + .filter( + comment_reply::read + .eq(false) + .or(person_comment_mention::read.eq(false)) + .or(person_post_mention::read.eq(false)) + // If its unread, I only want the messages to me + .or(private_message::read.eq(false)), + ); + } else { + // A special case for private messages: show messages FROM you also. + // Use a not-null checks to catch the others + query = query.filter( + inbox_combined::comment_reply_id + .is_not_null() + .and(recipient_person.eq(my_person_id)) + .or( + inbox_combined::person_comment_mention_id + .is_not_null() + .and(recipient_person.eq(my_person_id)), + ) + .or( + inbox_combined::person_post_mention_id + .is_not_null() + .and(recipient_person.eq(my_person_id)), + ) + .or( + inbox_combined::private_message_id.is_not_null().and( + recipient_person + .eq(my_person_id) + .or(item_creator.eq(my_person_id)), + ), + ), + ); + } + + if !(self.show_bot_accounts.unwrap_or_default()) { + query = query.filter(not(person::bot_account)); + }; + + // Dont show replies from blocked users or instances + query = query + .filter(person_actions::blocked.is_null()) + .filter(instance_actions::blocked.is_null()); + + if let Some(type_) = self.type_ { + query = match type_ { + InboxDataType::All => query, + InboxDataType::CommentReply => query.filter(inbox_combined::comment_reply_id.is_not_null()), + InboxDataType::CommentMention => { + query.filter(inbox_combined::person_comment_mention_id.is_not_null()) + } + InboxDataType::PostMention => { + query.filter(inbox_combined::person_post_mention_id.is_not_null()) + } + InboxDataType::PrivateMessage => { + query.filter(inbox_combined::private_message_id.is_not_null()) + } + } + } + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // Sorting by published + query = query + .then_desc(key::published) + // Tie breaker + .then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +impl InternalToCombinedView for InboxCombinedViewInternal { + type CombinedView = InboxCombinedView; + + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(comment_reply), Some(comment), Some(counts), Some(post), Some(community)) = ( + v.comment_reply, + v.comment.clone(), + v.comment_counts.clone(), + v.post.clone(), + v.community.clone(), + ) { + Some(InboxCombinedView::CommentReply(CommentReplyView { + comment_reply, + comment, + counts, + recipient: v.item_recipient, + post, + community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + })) + } else if let ( + Some(person_comment_mention), + Some(comment), + Some(counts), + Some(post), + Some(community), + ) = ( + v.person_comment_mention, + v.comment, + v.comment_counts, + v.post.clone(), + v.community.clone(), + ) { + Some(InboxCombinedView::CommentMention( + PersonCommentMentionView { + person_comment_mention, + comment, + counts, + recipient: v.item_recipient, + post, + community, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.comment_saved, + my_vote: v.my_comment_vote, + banned_from_community: v.banned_from_community, + }, + )) + } else if let ( + Some(person_post_mention), + Some(post), + Some(counts), + Some(unread_comments), + Some(community), + ) = ( + v.person_post_mention, + v.post, + v.post_counts, + v.post_unread_comments, + v.community, + ) { + Some(InboxCombinedView::PostMention(PersonPostMentionView { + person_post_mention, + counts, + post, + community, + recipient: v.item_recipient, + unread_comments, + creator: v.item_creator, + creator_banned_from_community: v.item_creator_banned_from_community, + creator_is_moderator: v.item_creator_is_moderator, + creator_is_admin: v.item_creator_is_admin, + creator_blocked: v.item_creator_blocked, + subscribed: v.subscribed, + saved: v.post_saved, + read: v.post_read, + hidden: v.post_hidden, + my_vote: v.my_post_vote, + image_details: v.image_details, + banned_from_community: v.banned_from_community, + })) + } else if let Some(private_message) = v.private_message { + Some(InboxCombinedView::PrivateMessage(PrivateMessageView { + private_message, + creator: v.item_creator, + recipient: v.item_recipient, + })) + } else { + None + } + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + use crate::{ + inbox_combined_view::InboxCombinedQuery, + structs::{InboxCombinedView, InboxCombinedViewInternal, PrivateMessageView}, + }; + use lemmy_db_schema::{ + assert_length, + source::{ + comment::{Comment, CommentInsertForm}, + comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + instance_block::{InstanceBlock, InstanceBlockForm}, + person::{Person, PersonInsertForm, PersonUpdateForm}, + person_block::{PersonBlock, PersonBlockForm}, + person_comment_mention::{PersonCommentMention, PersonCommentMentionInsertForm}, + person_post_mention::{PersonPostMention, PersonPostMentionInsertForm}, + post::{Post, PostInsertForm}, + private_message::{PrivateMessage, PrivateMessageInsertForm}, + }, + traits::{Blockable, Crud}, + utils::{build_db_pool_for_tests, DbPool}, + InboxDataType, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + sara: Person, + jessica: Person, + timmy_post: Post, + jessica_post: Post, + timmy_comment: Comment, + sara_comment: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_pcv"); + let timmy = Person::create(pool, &timmy_form).await?; + + let sara_form = PersonInsertForm::test_form(instance.id, "sara_pcv"); + let sara = Person::create(pool, &sara_form).await?; + + let jessica_form = PersonInsertForm::test_form(instance.id, "jessica_mrv"); + let jessica = Person::create(pool, &jessica_form).await?; + + let community_form = CommunityInsertForm::new( + instance.id, + "test community pcv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + let timmy_post_form = PostInsertForm::new("timmy post prv".into(), timmy.id, community.id); + let timmy_post = Post::create(pool, &timmy_post_form).await?; + + let jessica_post_form = + PostInsertForm::new("jessica post prv".into(), jessica.id, community.id); + let jessica_post = Post::create(pool, &jessica_post_form).await?; + + let timmy_comment_form = + CommentInsertForm::new(timmy.id, timmy_post.id, "timmy comment prv".into()); + let timmy_comment = Comment::create(pool, &timmy_comment_form, None).await?; + + let sara_comment_form = + CommentInsertForm::new(sara.id, timmy_post.id, "sara comment prv".into()); + let sara_comment = Comment::create(pool, &sara_comment_form, Some(&timmy_comment.path)).await?; + + Ok(Data { + instance, + timmy, + sara, + jessica, + timmy_post, + jessica_post, + timmy_comment, + sara_comment, + }) + } + + async fn setup_private_messages(data: &Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + let sara_timmy_message_form = + PrivateMessageInsertForm::new(data.sara.id, data.timmy.id, "sara to timmy".into()); + PrivateMessage::create(pool, &sara_timmy_message_form).await?; + + let sara_jessica_message_form = + PrivateMessageInsertForm::new(data.sara.id, data.jessica.id, "sara to jessica".into()); + PrivateMessage::create(pool, &sara_jessica_message_form).await?; + + let timmy_sara_message_form = + PrivateMessageInsertForm::new(data.timmy.id, data.sara.id, "timmy to sara".into()); + PrivateMessage::create(pool, &timmy_sara_message_form).await?; + + let jessica_timmy_message_form = + PrivateMessageInsertForm::new(data.jessica.id, data.timmy.id, "jessica to timmy".into()); + PrivateMessage::create(pool, &jessica_timmy_message_form).await?; + + Ok(()) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn replies() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Sara replied to timmys comment, but lets create the row now + let form = CommentReplyInsertForm { + recipient_id: data.timmy.id, + comment_id: data.sara_comment.id, + read: None, + }; + let reply = CommentReply::create(pool, &form).await?; + + let timmy_unread_replies = + InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, true).await?; + assert_eq!(1, timmy_unread_replies); + + let timmy_inbox = InboxCombinedQuery::default() + .list(pool, data.timmy.id) + .await?; + assert_length!(1, timmy_inbox); + + if let InboxCombinedView::CommentReply(v) = &timmy_inbox[0] { + assert_eq!(data.sara_comment.id, v.comment_reply.comment_id); + assert_eq!(data.sara_comment.id, v.comment.id); + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.sara.id, v.creator.id); + assert_eq!(data.timmy.id, v.recipient.id); + } else { + panic!("wrong type"); + } + + // Mark it as read + let form = CommentReplyUpdateForm { read: Some(true) }; + CommentReply::update(pool, reply.id, &form).await?; + + let timmy_unread_replies = + InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, true).await?; + assert_eq!(0, timmy_unread_replies); + + let timmy_inbox_unread = InboxCombinedQuery { + unread_only: Some(true), + ..Default::default() + } + .list(pool, data.timmy.id) + .await?; + assert_length!(0, timmy_inbox_unread); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn mentions() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + // Timmy mentions sara in a comment + let timmy_mention_sara_comment_form = PersonCommentMentionInsertForm { + recipient_id: data.sara.id, + comment_id: data.timmy_comment.id, + read: None, + }; + PersonCommentMention::create(pool, &timmy_mention_sara_comment_form).await?; + + // Jessica mentions sara in a post + let jessica_mention_sara_post_form = PersonPostMentionInsertForm { + recipient_id: data.sara.id, + post_id: data.jessica_post.id, + read: None, + }; + PersonPostMention::create(pool, &jessica_mention_sara_post_form).await?; + + // Test to make sure counts and blocks work correctly + let sara_unread_mentions = + InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, true).await?; + assert_eq!(2, sara_unread_mentions); + + let sara_inbox = InboxCombinedQuery::default() + .list(pool, data.sara.id) + .await?; + assert_length!(2, sara_inbox); + + if let InboxCombinedView::PostMention(v) = &sara_inbox[0] { + assert_eq!(data.jessica_post.id, v.person_post_mention.post_id); + assert_eq!(data.jessica_post.id, v.post.id); + assert_eq!(data.jessica.id, v.creator.id); + assert_eq!(data.sara.id, v.recipient.id); + } else { + panic!("wrong type"); + } + + if let InboxCombinedView::CommentMention(v) = &sara_inbox[1] { + assert_eq!(data.timmy_comment.id, v.person_comment_mention.comment_id); + assert_eq!(data.timmy_comment.id, v.comment.id); + assert_eq!(data.timmy_post.id, v.post.id); + assert_eq!(data.timmy.id, v.creator.id); + assert_eq!(data.sara.id, v.recipient.id); + } else { + panic!("wrong type"); + } + + // Sara blocks timmy, and make sure these counts are now empty + let sara_blocks_timmy_form = PersonBlockForm { + person_id: data.sara.id, + target_id: data.timmy.id, + }; + PersonBlock::block(pool, &sara_blocks_timmy_form).await?; + + let sara_unread_mentions_after_block = + InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, true).await?; + assert_eq!(1, sara_unread_mentions_after_block); + + let sara_inbox_after_block = InboxCombinedQuery::default() + .list(pool, data.sara.id) + .await?; + assert_length!(1, sara_inbox_after_block); + + // Make sure the comment mention which timmy made is the hidden one + assert!(matches!( + sara_inbox_after_block[0], + InboxCombinedView::PostMention(_) + )); + + // Unblock user so we can reuse the same person + PersonBlock::unblock(pool, &sara_blocks_timmy_form).await?; + + // Test the type filter + let sara_inbox_post_mentions_only = InboxCombinedQuery { + type_: Some(InboxDataType::PostMention), + ..Default::default() + } + .list(pool, data.sara.id) + .await?; + assert_length!(1, sara_inbox_post_mentions_only); + + assert!(matches!( + sara_inbox_post_mentions_only[0], + InboxCombinedView::PostMention(_) + )); + + // Turn Jessica into a bot account + let person_update_form = PersonUpdateForm { + bot_account: Some(true), + ..Default::default() + }; + Person::update(pool, data.jessica.id, &person_update_form).await?; + + // Make sure sara hides bots + let sara_unread_mentions_after_hide_bots = + InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, false).await?; + assert_eq!(1, sara_unread_mentions_after_hide_bots); + + let sara_inbox_after_hide_bots = InboxCombinedQuery::default() + .list(pool, data.sara.id) + .await?; + assert_length!(1, sara_inbox_after_hide_bots); + + // Make sure the post mention which jessica made is the hidden one + assert!(matches!( + sara_inbox_after_hide_bots[0], + InboxCombinedView::CommentMention(_) + )); + + // Mark them all as read + PersonPostMention::mark_all_as_read(pool, data.sara.id).await?; + PersonCommentMention::mark_all_as_read(pool, data.sara.id).await?; + + // Make sure none come back + let sara_unread_mentions = + InboxCombinedViewInternal::get_unread_count(pool, data.sara.id, false).await?; + assert_eq!(0, sara_unread_mentions); + + let sara_inbox_unread = InboxCombinedQuery { + unread_only: Some(true), + ..Default::default() + } + .list(pool, data.sara.id) + .await?; + assert_length!(0, sara_inbox_unread); + + cleanup(data, pool).await?; + + Ok(()) + } + + /// A helper function to coerce to a private message type for tests + fn map_to_pm(inbox: &[InboxCombinedView]) -> Vec { + inbox + .iter() + // Filter map to collect private messages + .filter_map(|f| { + if let InboxCombinedView::PrivateMessage(v) = f { + Some(v) + } else { + None + } + }) + .cloned() + .collect::>() + } + + #[tokio::test] + #[serial] + async fn read_private_messages() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + setup_private_messages(&data, pool).await?; + + let timmy_messages = map_to_pm( + &InboxCombinedQuery::default() + .list(pool, data.timmy.id) + .await?, + ); + + // The read even shows timmy's sent messages + assert_length!(3, &timmy_messages); + assert_eq!(timmy_messages[0].creator.id, data.jessica.id); + assert_eq!(timmy_messages[0].recipient.id, data.timmy.id); + assert_eq!(timmy_messages[1].creator.id, data.timmy.id); + assert_eq!(timmy_messages[1].recipient.id, data.sara.id); + assert_eq!(timmy_messages[2].creator.id, data.sara.id); + assert_eq!(timmy_messages[2].recipient.id, data.timmy.id); + + let timmy_unread = + InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, false).await?; + assert_eq!(2, timmy_unread); + + let timmy_unread_messages = map_to_pm( + &InboxCombinedQuery { + unread_only: Some(true), + ..Default::default() + } + .list(pool, data.timmy.id) + .await?, + ); + + // The unread hides timmy's sent messages + assert_length!(2, &timmy_unread_messages); + assert_eq!(timmy_unread_messages[0].creator.id, data.jessica.id); + assert_eq!(timmy_unread_messages[0].recipient.id, data.timmy.id); + assert_eq!(timmy_unread_messages[1].creator.id, data.sara.id); + assert_eq!(timmy_unread_messages[1].recipient.id, data.timmy.id); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn ensure_private_message_person_block() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + setup_private_messages(&data, pool).await?; + + // Make sure blocks are working + let timmy_blocks_sara_form = PersonBlockForm { + person_id: data.timmy.id, + target_id: data.sara.id, + }; + + let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form).await?; + + let expected_block = PersonBlock { + person_id: data.timmy.id, + target_id: data.sara.id, + published: inserted_block.published, + }; + assert_eq!(expected_block, inserted_block); + + let timmy_messages = map_to_pm( + &InboxCombinedQuery { + unread_only: Some(true), + ..Default::default() + } + .list(pool, data.timmy.id) + .await?, + ); + + assert_length!(1, &timmy_messages); + + let timmy_unread = + InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, false).await?; + assert_eq!(1, timmy_unread); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn ensure_private_message_instance_block() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + setup_private_messages(&data, pool).await?; + + // Make sure instance_blocks are working + let timmy_blocks_instance_form = InstanceBlockForm { + person_id: data.timmy.id, + instance_id: data.sara.instance_id, + }; + + let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form).await?; + + let expected_instance_block = InstanceBlock { + person_id: data.timmy.id, + instance_id: data.sara.instance_id, + published: inserted_instance_block.published, + }; + assert_eq!(expected_instance_block, inserted_instance_block); + + let timmy_messages = map_to_pm( + &InboxCombinedQuery { + unread_only: Some(true), + ..Default::default() + } + .list(pool, data.timmy.id) + .await?, + ); + + assert_length!(0, &timmy_messages); + + let timmy_unread = + InboxCombinedViewInternal::get_unread_count(pool, data.timmy.id, false).await?; + assert_eq!(0, timmy_unread); + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views_actor/src/lib.rs b/crates/db_views_actor/src/lib.rs index 2ec9652e35..d982c4a1ff 100644 --- a/crates/db_views_actor/src/lib.rs +++ b/crates/db_views_actor/src/lib.rs @@ -9,7 +9,13 @@ pub mod community_person_ban_view; #[cfg(feature = "full")] pub mod community_view; #[cfg(feature = "full")] -pub mod person_mention_view; +pub mod inbox_combined_view; +#[cfg(feature = "full")] +pub mod person_comment_mention_view; +#[cfg(feature = "full")] +pub mod person_post_mention_view; #[cfg(feature = "full")] pub mod person_view; +#[cfg(feature = "full")] +pub mod private_message_view; pub mod structs; diff --git a/crates/db_views_actor/src/person_comment_mention_view.rs b/crates/db_views_actor/src/person_comment_mention_view.rs new file mode 100644 index 0000000000..e260e8908d --- /dev/null +++ b/crates/db_views_actor/src/person_comment_mention_view.rs @@ -0,0 +1,97 @@ +use crate::structs::PersonCommentMentionView; +use diesel::{ + dsl::exists, + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::{PersonCommentMentionId, PersonId}, + schema::{ + comment, + comment_actions, + comment_aggregates, + community, + community_actions, + local_user, + person, + person_actions, + person_comment_mention, + post, + }, + source::community::CommunityFollower, + utils::{actions, actions_alias, get_conn, DbPool}, +}; + +impl PersonCommentMentionView { + pub async fn read( + pool: &mut DbPool<'_>, + person_comment_mention_id: PersonCommentMentionId, + my_person_id: Option, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + let creator_is_admin = exists( + local_user::table.filter( + comment::creator_id + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ); + + person_comment_mention::table + .find(person_comment_mention_id) + .inner_join(comment::table) + .inner_join(person::table.on(comment::creator_id.eq(person::id))) + .inner_join(post::table.on(comment::post_id.eq(post::id))) + .inner_join(community::table.on(post::community_id.eq(community::id))) + .inner_join(aliases::person1) + .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(comment_actions::table, my_person_id, comment::id)) + .left_join(actions( + person_actions::table, + my_person_id, + comment::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + comment::creator_id, + post::community_id, + )) + .select(( + person_comment_mention::all_columns, + comment::all_columns, + person::all_columns, + post::all_columns, + community::all_columns, + aliases::person1.fields(person::all_columns), + comment_aggregates::all_columns, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_is_admin, + CommunityFollower::select_subscribed_type(), + comment_actions::saved.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + comment_actions::like_score.nullable(), + )) + .first(conn) + .await + } +} diff --git a/crates/db_views_actor/src/person_mention_view.rs b/crates/db_views_actor/src/person_mention_view.rs deleted file mode 100644 index 08be67a826..0000000000 --- a/crates/db_views_actor/src/person_mention_view.rs +++ /dev/null @@ -1,383 +0,0 @@ -use crate::structs::PersonMentionView; -use diesel::{ - dsl::{exists, not}, - pg::Pg, - result::Error, - BoolExpressionMethods, - ExpressionMethods, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - aliases::{self, creator_community_actions}, - newtypes::{PersonId, PersonMentionId}, - schema::{ - comment, - comment_actions, - comment_aggregates, - community, - community_actions, - local_user, - person, - person_actions, - person_mention, - post, - }, - source::{community::CommunityFollower, local_user::LocalUser}, - utils::{ - actions, - actions_alias, - get_conn, - limit_and_offset, - DbConn, - DbPool, - ListFn, - Queries, - ReadFn, - }, - CommentSortType, -}; - -fn queries<'a>() -> Queries< - impl ReadFn<'a, PersonMentionView, (PersonMentionId, Option)>, - impl ListFn<'a, PersonMentionView, PersonMentionQuery>, -> { - let creator_is_admin = exists( - local_user::table.filter( - comment::creator_id - .eq(local_user::person_id) - .and(local_user::admin.eq(true)), - ), - ); - - let all_joins = move |query: person_mention::BoxedQuery<'a, Pg>, - my_person_id: Option| { - query - .inner_join(comment::table) - .inner_join(person::table.on(comment::creator_id.eq(person::id))) - .inner_join(post::table.on(comment::post_id.eq(post::id))) - .inner_join(community::table.on(post::community_id.eq(community::id))) - .inner_join(aliases::person1) - .inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id))) - .left_join(actions( - community_actions::table, - my_person_id, - post::community_id, - )) - .left_join(actions(comment_actions::table, my_person_id, comment::id)) - .left_join(actions( - person_actions::table, - my_person_id, - comment::creator_id, - )) - .left_join(actions_alias( - creator_community_actions, - comment::creator_id, - post::community_id, - )) - .select(( - person_mention::all_columns, - comment::all_columns, - person::all_columns, - post::all_columns, - community::all_columns, - aliases::person1.fields(person::all_columns), - comment_aggregates::all_columns, - creator_community_actions - .field(community_actions::received_ban) - .nullable() - .is_not_null(), - community_actions::received_ban.nullable().is_not_null(), - creator_community_actions - .field(community_actions::became_moderator) - .nullable() - .is_not_null(), - creator_is_admin, - CommunityFollower::select_subscribed_type(), - comment_actions::saved.nullable().is_not_null(), - person_actions::blocked.nullable().is_not_null(), - comment_actions::like_score.nullable(), - )) - }; - - let read = - move |mut conn: DbConn<'a>, - (person_mention_id, my_person_id): (PersonMentionId, Option)| async move { - all_joins( - person_mention::table.find(person_mention_id).into_boxed(), - my_person_id, - ) - .first(&mut conn) - .await - }; - - let list = move |mut conn: DbConn<'a>, options: PersonMentionQuery| async move { - // These filters need to be kept in sync with the filters in - // PersonMentionView::get_unread_mentions() - let mut query = all_joins(person_mention::table.into_boxed(), options.my_person_id); - - if let Some(recipient_id) = options.recipient_id { - query = query.filter(person_mention::recipient_id.eq(recipient_id)); - } - - if options.unread_only { - query = query.filter(person_mention::read.eq(false)); - } - - if !options.show_bot_accounts { - query = query.filter(not(person::bot_account)); - }; - - query = match options.sort.unwrap_or(CommentSortType::Hot) { - CommentSortType::Hot => query.then_order_by(comment_aggregates::hot_rank.desc()), - CommentSortType::Controversial => { - query.then_order_by(comment_aggregates::controversy_rank.desc()) - } - CommentSortType::New => query.then_order_by(comment::published.desc()), - CommentSortType::Old => query.then_order_by(comment::published.asc()), - CommentSortType::Top => query.order_by(comment_aggregates::score.desc()), - }; - - // Don't show mentions from blocked persons - query = query.filter(person_actions::blocked.is_null()); - - let (limit, offset) = limit_and_offset(options.page, options.limit)?; - - query - .limit(limit) - .offset(offset) - .load::(&mut conn) - .await - }; - - Queries::new(read, list) -} - -impl PersonMentionView { - pub async fn read( - pool: &mut DbPool<'_>, - person_mention_id: PersonMentionId, - my_person_id: Option, - ) -> Result { - queries() - .read(pool, (person_mention_id, my_person_id)) - .await - } - - /// Gets the number of unread mentions - pub async fn get_unread_mentions( - pool: &mut DbPool<'_>, - local_user: &LocalUser, - ) -> Result { - use diesel::dsl::count; - let conn = &mut get_conn(pool).await?; - - let mut query = person_mention::table - .inner_join(comment::table) - .left_join(actions( - person_actions::table, - Some(local_user.person_id), - comment::creator_id, - )) - .inner_join(person::table.on(comment::creator_id.eq(person::id))) - .into_boxed(); - - // These filters need to be kept in sync with the filters in queries().list() - if !local_user.show_bot_accounts { - query = query.filter(not(person::bot_account)); - } - - query - // Don't count replies from blocked users - .filter(person_actions::blocked.is_null()) - .filter(person_mention::recipient_id.eq(local_user.person_id)) - .filter(person_mention::read.eq(false)) - .filter(comment::deleted.eq(false)) - .filter(comment::removed.eq(false)) - .select(count(person_mention::id)) - .first::(conn) - .await - } -} - -#[derive(Default, Clone)] -pub struct PersonMentionQuery { - pub my_person_id: Option, - pub recipient_id: Option, - pub sort: Option, - pub unread_only: bool, - pub show_bot_accounts: bool, - pub page: Option, - pub limit: Option, -} - -impl PersonMentionQuery { - pub async fn list(self, pool: &mut DbPool<'_>) -> Result, Error> { - queries().list(pool, self).await - } -} - -#[cfg(test)] -mod tests { - - use crate::{person_mention_view::PersonMentionQuery, structs::PersonMentionView}; - use lemmy_db_schema::{ - source::{ - comment::{Comment, CommentInsertForm}, - community::{Community, CommunityInsertForm}, - instance::Instance, - local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, - person::{Person, PersonInsertForm, PersonUpdateForm}, - person_block::{PersonBlock, PersonBlockForm}, - person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm}, - post::{Post, PostInsertForm}, - }, - traits::{Blockable, Crud}, - utils::build_db_pool_for_tests, - }; - use lemmy_db_views::structs::LocalUserView; - use lemmy_utils::error::LemmyResult; - use pretty_assertions::assert_eq; - use serial_test::serial; - - #[tokio::test] - #[serial] - async fn test_crud() -> LemmyResult<()> { - let pool = &build_db_pool_for_tests(); - let pool = &mut pool.into(); - - let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; - - let new_person = PersonInsertForm::test_form(inserted_instance.id, "terrylake"); - - let inserted_person = Person::create(pool, &new_person).await?; - - let recipient_form = PersonInsertForm::test_form(inserted_instance.id, "terrylakes recipient"); - - let inserted_recipient = Person::create(pool, &recipient_form).await?; - let recipient_id = inserted_recipient.id; - - let recipient_local_user = - LocalUser::create(pool, &LocalUserInsertForm::test_form(recipient_id), vec![]).await?; - - let new_community = CommunityInsertForm::new( - inserted_instance.id, - "test community lake".to_string(), - "nada".to_owned(), - "pubkey".to_string(), - ); - let inserted_community = Community::create(pool, &new_community).await?; - - let new_post = PostInsertForm::new( - "A test post".into(), - inserted_person.id, - inserted_community.id, - ); - let inserted_post = Post::create(pool, &new_post).await?; - - let comment_form = CommentInsertForm::new( - inserted_person.id, - inserted_post.id, - "A test comment".into(), - ); - let inserted_comment = Comment::create(pool, &comment_form, None).await?; - - let person_mention_form = PersonMentionInsertForm { - recipient_id: inserted_recipient.id, - comment_id: inserted_comment.id, - read: None, - }; - - let inserted_mention = PersonMention::create(pool, &person_mention_form).await?; - - let expected_mention = PersonMention { - id: inserted_mention.id, - recipient_id: inserted_mention.recipient_id, - comment_id: inserted_mention.comment_id, - read: false, - published: inserted_mention.published, - }; - - let read_mention = PersonMention::read(pool, inserted_mention.id).await?; - - let person_mention_update_form = PersonMentionUpdateForm { read: Some(false) }; - let updated_mention = - PersonMention::update(pool, inserted_mention.id, &person_mention_update_form).await?; - - // Test to make sure counts and blocks work correctly - let unread_mentions = - PersonMentionView::get_unread_mentions(pool, &recipient_local_user).await?; - - let query = PersonMentionQuery { - recipient_id: Some(recipient_id), - my_person_id: Some(recipient_id), - sort: None, - unread_only: false, - show_bot_accounts: true, - page: None, - limit: None, - }; - let mentions = query.clone().list(pool).await?; - assert_eq!(1, unread_mentions); - assert_eq!(1, mentions.len()); - - // Block the person, and make sure these counts are now empty - let block_form = PersonBlockForm { - person_id: recipient_id, - target_id: inserted_person.id, - }; - PersonBlock::block(pool, &block_form).await?; - - let unread_mentions_after_block = - PersonMentionView::get_unread_mentions(pool, &recipient_local_user).await?; - let mentions_after_block = query.clone().list(pool).await?; - assert_eq!(0, unread_mentions_after_block); - assert_eq!(0, mentions_after_block.len()); - - // Unblock user so we can reuse the same person - PersonBlock::unblock(pool, &block_form).await?; - - // Turn Terry into a bot account - let person_update_form = PersonUpdateForm { - bot_account: Some(true), - ..Default::default() - }; - Person::update(pool, inserted_person.id, &person_update_form).await?; - - let recipient_local_user_update_form = LocalUserUpdateForm { - show_bot_accounts: Some(false), - ..Default::default() - }; - LocalUser::update( - pool, - recipient_local_user.id, - &recipient_local_user_update_form, - ) - .await?; - let recipient_local_user_view = LocalUserView::read(pool, recipient_local_user.id).await?; - - let unread_mentions_after_hide_bots = - PersonMentionView::get_unread_mentions(pool, &recipient_local_user_view.local_user).await?; - - let mut query_without_bots = query.clone(); - query_without_bots.show_bot_accounts = false; - let replies_after_hide_bots = query_without_bots.list(pool).await?; - assert_eq!(0, unread_mentions_after_hide_bots); - assert_eq!(0, replies_after_hide_bots.len()); - - Comment::delete(pool, inserted_comment.id).await?; - Post::delete(pool, inserted_post.id).await?; - Community::delete(pool, inserted_community.id).await?; - Person::delete(pool, inserted_person.id).await?; - Person::delete(pool, inserted_recipient.id).await?; - Instance::delete(pool, inserted_instance.id).await?; - - assert_eq!(expected_mention, read_mention); - assert_eq!(expected_mention, inserted_mention); - assert_eq!(expected_mention, updated_mention); - - Ok(()) - } -} diff --git a/crates/db_views_actor/src/person_post_mention_view.rs b/crates/db_views_actor/src/person_post_mention_view.rs new file mode 100644 index 0000000000..fd16a0619f --- /dev/null +++ b/crates/db_views_actor/src/person_post_mention_view.rs @@ -0,0 +1,103 @@ +use crate::structs::PersonPostMentionView; +use diesel::{ + dsl::exists, + result::Error, + BoolExpressionMethods, + ExpressionMethods, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases::{self, creator_community_actions}, + newtypes::{PersonId, PersonPostMentionId}, + schema::{ + community, + community_actions, + image_details, + local_user, + person, + person_actions, + person_post_mention, + post, + post_actions, + post_aggregates, + }, + source::community::CommunityFollower, + utils::{actions, actions_alias, functions::coalesce, get_conn, DbPool}, +}; + +impl PersonPostMentionView { + pub async fn read( + pool: &mut DbPool<'_>, + person_post_mention_id: PersonPostMentionId, + my_person_id: Option, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + let creator_is_admin = exists( + local_user::table.filter( + post::creator_id + .eq(local_user::person_id) + .and(local_user::admin.eq(true)), + ), + ); + + person_post_mention::table + .find(person_post_mention_id) + .inner_join(post::table) + .inner_join(person::table.on(post::creator_id.eq(person::id))) + .inner_join(community::table.on(post::community_id.eq(community::id))) + .inner_join(aliases::person1) + .inner_join(post_aggregates::table.on(post::id.eq(post_aggregates::post_id))) + .left_join(image_details::table.on(post::thumbnail_url.eq(image_details::link.nullable()))) + .left_join(actions( + community_actions::table, + my_person_id, + post::community_id, + )) + .left_join(actions(post_actions::table, my_person_id, post::id)) + .left_join(actions( + person_actions::table, + my_person_id, + post::creator_id, + )) + .left_join(actions_alias( + creator_community_actions, + post::creator_id, + post::community_id, + )) + .select(( + person_post_mention::all_columns, + post::all_columns, + person::all_columns, + community::all_columns, + image_details::all_columns.nullable(), + aliases::person1.fields(person::all_columns), + post_aggregates::all_columns, + creator_community_actions + .field(community_actions::received_ban) + .nullable() + .is_not_null(), + community_actions::received_ban.nullable().is_not_null(), + creator_community_actions + .field(community_actions::became_moderator) + .nullable() + .is_not_null(), + creator_is_admin, + CommunityFollower::select_subscribed_type(), + post_actions::saved.nullable().is_not_null(), + post_actions::read.nullable().is_not_null(), + post_actions::hidden.nullable().is_not_null(), + person_actions::blocked.nullable().is_not_null(), + post_actions::like_score.nullable(), + coalesce( + post_aggregates::comments.nullable() - post_actions::read_comments_amount.nullable(), + post_aggregates::comments, + ), + )) + .first(conn) + .await + } +} diff --git a/crates/db_views_actor/src/private_message_view.rs b/crates/db_views_actor/src/private_message_view.rs new file mode 100644 index 0000000000..2345e7466a --- /dev/null +++ b/crates/db_views_actor/src/private_message_view.rs @@ -0,0 +1,42 @@ +use crate::structs::PrivateMessageView; +use diesel::{result::Error, ExpressionMethods, JoinOnDsl, QueryDsl}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + aliases, + newtypes::PrivateMessageId, + schema::{instance_actions, person, person_actions, private_message}, + utils::{actions, get_conn, DbPool}, +}; + +impl PrivateMessageView { + pub async fn read( + pool: &mut DbPool<'_>, + private_message_id: PrivateMessageId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + + private_message::table + .find(private_message_id) + .inner_join(person::table.on(private_message::creator_id.eq(person::id))) + .inner_join( + aliases::person1.on(private_message::recipient_id.eq(aliases::person1.field(person::id))), + ) + .left_join(actions( + person_actions::table, + Some(aliases::person1.field(person::id)), + private_message::creator_id, + )) + .left_join(actions( + instance_actions::table, + Some(aliases::person1.field(person::id)), + person::instance_id, + )) + .select(( + private_message::all_columns, + person::all_columns, + aliases::person1.fields(person::all_columns), + )) + .first(conn) + .await + } +} diff --git a/crates/db_views_actor/src/structs.rs b/crates/db_views_actor/src/structs.rs index 6b609a7537..b1f75c86d2 100644 --- a/crates/db_views_actor/src/structs.rs +++ b/crates/db_views_actor/src/structs.rs @@ -1,14 +1,17 @@ #[cfg(feature = "full")] use diesel::Queryable; use lemmy_db_schema::{ - aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates}, + aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates, PostAggregates}, source::{ comment::Comment, comment_reply::CommentReply, community::Community, + images::ImageDetails, person::Person, - person_mention::PersonMention, + person_comment_mention::PersonCommentMention, + person_post_mention::PersonPostMention, post::Post, + private_message::PrivateMessage, }, SubscribedType, }; @@ -93,9 +96,9 @@ pub enum CommunitySortType { #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] -/// A person mention view. -pub struct PersonMentionView { - pub person_mention: PersonMention, +/// A person comment mention view. +pub struct PersonCommentMentionView { + pub person_comment_mention: PersonCommentMention, pub comment: Comment, pub creator: Person, pub post: Post, @@ -113,6 +116,35 @@ pub struct PersonMentionView { pub my_vote: Option, } +#[skip_serializing_none] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A person post mention view. +pub struct PersonPostMentionView { + pub person_post_mention: PersonPostMention, + pub post: Post, + pub creator: Person, + pub community: Community, + #[cfg_attr(feature = "full", ts(optional))] + pub image_details: Option, + pub recipient: Person, + pub counts: PostAggregates, + pub creator_banned_from_community: bool, + pub banned_from_community: bool, + pub creator_is_moderator: bool, + pub creator_is_admin: bool, + pub subscribed: SubscribedType, + pub saved: bool, + pub read: bool, + pub hidden: bool, + pub creator_blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub my_vote: Option, + pub unread_comments: i64, +} + #[skip_serializing_none] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] @@ -159,3 +191,69 @@ pub struct PendingFollow { pub is_new_instance: bool, pub subscribed: SubscribedType, } + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS, Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// A private message view. +pub struct PrivateMessageView { + pub private_message: PrivateMessage, + pub creator: Person, + pub recipient: Person, +} + +/// like PaginationCursor but for the report_combined table +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct InboxCombinedPaginationCursor(pub String); + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined inbox view +pub struct InboxCombinedViewInternal { + // Comment reply + pub comment_reply: Option, + // Person comment mention + pub person_comment_mention: Option, + // Person post mention + pub person_post_mention: Option, + pub post_counts: Option, + pub post_unread_comments: Option, + pub post_saved: bool, + pub post_read: bool, + pub post_hidden: bool, + pub my_post_vote: Option, + pub image_details: Option, + // Private message + pub private_message: Option, + // Shared + pub post: Option, + pub community: Option, + pub comment: Option, + pub comment_counts: Option, + pub comment_saved: bool, + pub my_comment_vote: Option, + pub subscribed: SubscribedType, + pub item_creator: Person, + pub item_recipient: Person, + pub item_creator_is_admin: bool, + pub item_creator_is_moderator: bool, + pub item_creator_banned_from_community: bool, + pub item_creator_blocked: bool, + pub banned_from_community: bool, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum InboxCombinedView { + CommentReply(CommentReplyView), + CommentMention(PersonCommentMentionView), + PostMention(PersonPostMentionView), + PrivateMessage(PrivateMessageView), +} diff --git a/crates/db_views_moderator/Cargo.toml b/crates/db_views_moderator/Cargo.toml index df8ec1ea2c..9e0185e4b2 100644 --- a/crates/db_views_moderator/Cargo.toml +++ b/crates/db_views_moderator/Cargo.toml @@ -15,10 +15,19 @@ doctest = false workspace = true [features] -full = ["lemmy_db_schema/full", "diesel", "diesel-async", "ts-rs"] +full = [ + "lemmy_db_schema/full", + "lemmy_utils", + "i-love-jesus", + "diesel", + "diesel-async", + "ts-rs", +] [dependencies] lemmy_db_schema = { workspace = true } +lemmy_utils = { workspace = true, optional = true } +i-love-jesus = { workspace = true, optional = true } diesel = { workspace = true, features = [ "postgres", "chrono", @@ -31,3 +40,8 @@ diesel-async = { workspace = true, features = [ serde = { workspace = true } serde_with = { workspace = true } ts-rs = { workspace = true, optional = true } + +[dev-dependencies] +serial_test = { workspace = true } +tokio = { workspace = true } +pretty_assertions = { workspace = true } diff --git a/crates/db_views_moderator/src/admin_allow_instance.rs b/crates/db_views_moderator/src/admin_allow_instance.rs deleted file mode 100644 index 2a0aaad14e..0000000000 --- a/crates/db_views_moderator/src/admin_allow_instance.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::structs::{AdminAllowInstanceView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{admin_allow_instance, instance, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl AdminAllowInstanceView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = admin_allow_instance::admin_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = admin_allow_instance::table - .left_join(person::table.on(admin_names_join)) - .inner_join(instance::table) - .select(( - admin_allow_instance::all_columns, - instance::all_columns, - person::all_columns.nullable(), - )) - .into_boxed(); - - if let Some(admin_person_id) = params.mod_person_id { - query = query.filter(admin_allow_instance::admin_person_id.eq(admin_person_id)); - }; - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(admin_allow_instance::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/admin_block_instance.rs b/crates/db_views_moderator/src/admin_block_instance.rs deleted file mode 100644 index e9d7c8b0de..0000000000 --- a/crates/db_views_moderator/src/admin_block_instance.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::structs::{AdminBlockInstanceView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{admin_block_instance, instance, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl AdminBlockInstanceView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = admin_block_instance::admin_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = admin_block_instance::table - .left_join(person::table.on(admin_names_join)) - .inner_join(instance::table) - .select(( - admin_block_instance::all_columns, - instance::all_columns, - person::all_columns.nullable(), - )) - .into_boxed(); - - if let Some(admin_person_id) = params.mod_person_id { - query = query.filter(admin_block_instance::admin_person_id.eq(admin_person_id)); - }; - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(admin_block_instance::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/admin_purge_comment_view.rs b/crates/db_views_moderator/src/admin_purge_comment_view.rs deleted file mode 100644 index 4c650b6fa6..0000000000 --- a/crates/db_views_moderator/src/admin_purge_comment_view.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::structs::{AdminPurgeCommentView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{admin_purge_comment, person, post}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl AdminPurgeCommentView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = admin_purge_comment::admin_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - - let mut query = admin_purge_comment::table - .left_join(person::table.on(admin_names_join)) - .inner_join(post::table) - .select(( - admin_purge_comment::all_columns, - person::all_columns.nullable(), - post::all_columns, - )) - .into_boxed(); - - if let Some(admin_person_id) = params.mod_person_id { - query = query.filter(admin_purge_comment::admin_person_id.eq(admin_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(admin_purge_comment::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/admin_purge_community_view.rs b/crates/db_views_moderator/src/admin_purge_community_view.rs deleted file mode 100644 index 5eadb8985e..0000000000 --- a/crates/db_views_moderator/src/admin_purge_community_view.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::structs::{AdminPurgeCommunityView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{admin_purge_community, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl AdminPurgeCommunityView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = admin_purge_community::admin_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - - let mut query = admin_purge_community::table - .left_join(person::table.on(admin_names_join)) - .select(( - admin_purge_community::all_columns, - person::all_columns.nullable(), - )) - .into_boxed(); - - if let Some(admin_person_id) = params.mod_person_id { - query = query.filter(admin_purge_community::admin_person_id.eq(admin_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(admin_purge_community::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/admin_purge_person_view.rs b/crates/db_views_moderator/src/admin_purge_person_view.rs deleted file mode 100644 index b6dd834c50..0000000000 --- a/crates/db_views_moderator/src/admin_purge_person_view.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::structs::{AdminPurgePersonView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{admin_purge_person, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl AdminPurgePersonView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = admin_purge_person::admin_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = admin_purge_person::table - .left_join(person::table.on(admin_names_join)) - .select(( - admin_purge_person::all_columns, - person::all_columns.nullable(), - )) - .into_boxed(); - - if let Some(admin_person_id) = params.mod_person_id { - query = query.filter(admin_purge_person::admin_person_id.eq(admin_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(admin_purge_person::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/admin_purge_post_view.rs b/crates/db_views_moderator/src/admin_purge_post_view.rs deleted file mode 100644 index b77493c251..0000000000 --- a/crates/db_views_moderator/src/admin_purge_post_view.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::structs::{AdminPurgePostView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{admin_purge_post, community, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl AdminPurgePostView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = admin_purge_post::admin_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = admin_purge_post::table - .left_join(person::table.on(admin_names_join)) - .inner_join(community::table) - .select(( - admin_purge_post::all_columns, - person::all_columns.nullable(), - community::all_columns, - )) - .into_boxed(); - - if let Some(admin_person_id) = params.mod_person_id { - query = query.filter(admin_purge_post::admin_person_id.eq(admin_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(admin_purge_post::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/lib.rs b/crates/db_views_moderator/src/lib.rs index 5748707c6e..1cc21da279 100644 --- a/crates/db_views_moderator/src/lib.rs +++ b/crates/db_views_moderator/src/lib.rs @@ -1,35 +1,3 @@ #[cfg(feature = "full")] -pub mod admin_allow_instance; -#[cfg(feature = "full")] -pub mod admin_block_instance; -#[cfg(feature = "full")] -pub mod admin_purge_comment_view; -#[cfg(feature = "full")] -pub mod admin_purge_community_view; -#[cfg(feature = "full")] -pub mod admin_purge_person_view; -#[cfg(feature = "full")] -pub mod admin_purge_post_view; -#[cfg(feature = "full")] -pub mod mod_add_community_view; -#[cfg(feature = "full")] -pub mod mod_add_view; -#[cfg(feature = "full")] -pub mod mod_ban_from_community_view; -#[cfg(feature = "full")] -pub mod mod_ban_view; -#[cfg(feature = "full")] -pub mod mod_feature_post_view; -#[cfg(feature = "full")] -pub mod mod_hide_community_view; -#[cfg(feature = "full")] -pub mod mod_lock_post_view; -#[cfg(feature = "full")] -pub mod mod_remove_comment_view; -#[cfg(feature = "full")] -pub mod mod_remove_community_view; -#[cfg(feature = "full")] -pub mod mod_remove_post_view; -#[cfg(feature = "full")] -pub mod mod_transfer_community_view; +pub mod modlog_combined_view; pub mod structs; diff --git a/crates/db_views_moderator/src/mod_add_community_view.rs b/crates/db_views_moderator/src/mod_add_community_view.rs deleted file mode 100644 index 1068aba759..0000000000 --- a/crates/db_views_moderator/src/mod_add_community_view.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::structs::{ModAddCommunityView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, mod_add_community, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModAddCommunityView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let person_alias_1 = diesel::alias!(person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_add_community::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_add_community::table - .left_join(person::table.on(admin_names_join)) - .inner_join(community::table) - .inner_join( - person_alias_1.on(mod_add_community::other_person_id.eq(person_alias_1.field(person::id))), - ) - .select(( - mod_add_community::all_columns, - person::all_columns.nullable(), - community::all_columns, - person_alias_1.fields(person::all_columns), - )) - .into_boxed(); - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_add_community::mod_person_id.eq(mod_person_id)); - }; - - if let Some(community_id) = params.community_id { - query = query.filter(mod_add_community::community_id.eq(community_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_add_community::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_add_view.rs b/crates/db_views_moderator/src/mod_add_view.rs deleted file mode 100644 index c5612c4adc..0000000000 --- a/crates/db_views_moderator/src/mod_add_view.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::structs::{ModAddView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{mod_add, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModAddView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let person_alias_1 = diesel::alias!(person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_add::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_add::table - .left_join(person::table.on(admin_names_join)) - .inner_join(person_alias_1.on(mod_add::other_person_id.eq(person_alias_1.field(person::id)))) - .select(( - mod_add::all_columns, - person::all_columns.nullable(), - person_alias_1.fields(person::all_columns), - )) - .into_boxed(); - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_add::mod_person_id.eq(mod_person_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_add::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_ban_from_community_view.rs b/crates/db_views_moderator/src/mod_ban_from_community_view.rs deleted file mode 100644 index d2d6038f3b..0000000000 --- a/crates/db_views_moderator/src/mod_ban_from_community_view.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::structs::{ModBanFromCommunityView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, mod_ban_from_community, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModBanFromCommunityView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let person_alias_1 = diesel::alias!(person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_ban_from_community::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_ban_from_community::table - .left_join(person::table.on(admin_names_join)) - .inner_join(community::table) - .inner_join( - person_alias_1 - .on(mod_ban_from_community::other_person_id.eq(person_alias_1.field(person::id))), - ) - .select(( - mod_ban_from_community::all_columns, - person::all_columns.nullable(), - community::all_columns, - person_alias_1.fields(person::all_columns), - )) - .into_boxed(); - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_ban_from_community::mod_person_id.eq(mod_person_id)); - }; - - if let Some(community_id) = params.community_id { - query = query.filter(mod_ban_from_community::community_id.eq(community_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(mod_ban_from_community::other_person_id.eq(other_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_ban_from_community::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_ban_view.rs b/crates/db_views_moderator/src/mod_ban_view.rs deleted file mode 100644 index ca0723e835..0000000000 --- a/crates/db_views_moderator/src/mod_ban_view.rs +++ /dev/null @@ -1,61 +0,0 @@ -use crate::structs::{ModBanView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{mod_ban, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModBanView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let person_alias_1 = diesel::alias!(person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_ban::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_ban::table - .left_join(person::table.on(admin_names_join)) - .inner_join(person_alias_1.on(mod_ban::other_person_id.eq(person_alias_1.field(person::id)))) - .select(( - mod_ban::all_columns, - person::all_columns.nullable(), - person_alias_1.fields(person::all_columns), - )) - .into_boxed(); - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_ban::mod_person_id.eq(mod_person_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_ban::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_feature_post_view.rs b/crates/db_views_moderator/src/mod_feature_post_view.rs deleted file mode 100644 index 4c0fdb4f79..0000000000 --- a/crates/db_views_moderator/src/mod_feature_post_view.rs +++ /dev/null @@ -1,72 +0,0 @@ -use crate::structs::{ModFeaturePostView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, mod_feature_post, person, post}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModFeaturePostView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let person_alias_1 = diesel::alias!(person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_feature_post::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_feature_post::table - .left_join(person::table.on(admin_names_join)) - .inner_join(post::table) - .inner_join(person_alias_1.on(post::creator_id.eq(person_alias_1.field(person::id)))) - .inner_join(community::table.on(post::community_id.eq(community::id))) - .select(( - mod_feature_post::all_columns, - person::all_columns.nullable(), - post::all_columns, - community::all_columns, - )) - .into_boxed(); - - if let Some(community_id) = params.community_id { - query = query.filter(post::community_id.eq(community_id)); - }; - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_feature_post::mod_person_id.eq(mod_person_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); - }; - - if let Some(post_id) = params.post_id { - query = query.filter(post::id.eq(post_id)); - } - - // If a comment ID is given, then don't find any results - if params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_feature_post::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_hide_community_view.rs b/crates/db_views_moderator/src/mod_hide_community_view.rs deleted file mode 100644 index 3c8a7e6271..0000000000 --- a/crates/db_views_moderator/src/mod_hide_community_view.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::structs::{ModHideCommunityView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, mod_hide_community, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModHideCommunityView { - // Pass in mod_id as admin_id because only admins can do this action - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_hide_community::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_hide_community::table - .left_join(person::table.on(admin_names_join)) - .inner_join(community::table.on(mod_hide_community::community_id.eq(community::id))) - .select(( - mod_hide_community::all_columns, - person::all_columns.nullable(), - community::all_columns, - )) - .into_boxed(); - - if let Some(community_id) = params.community_id { - query = query.filter(mod_hide_community::community_id.eq(community_id)); - }; - - if let Some(admin_id) = params.mod_person_id { - query = query.filter(mod_hide_community::mod_person_id.eq(admin_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_hide_community::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_lock_post_view.rs b/crates/db_views_moderator/src/mod_lock_post_view.rs deleted file mode 100644 index 5a6c753d95..0000000000 --- a/crates/db_views_moderator/src/mod_lock_post_view.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::structs::{ModLockPostView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, mod_lock_post, person, post}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModLockPostView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let person_alias_1 = diesel::alias!(person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_lock_post::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_lock_post::table - .left_join(person::table.on(admin_names_join)) - .inner_join(post::table) - .inner_join(community::table.on(post::community_id.eq(community::id))) - .inner_join(person_alias_1.on(post::creator_id.eq(person_alias_1.field(person::id)))) - .select(( - mod_lock_post::all_columns, - person::all_columns.nullable(), - post::all_columns, - community::all_columns, - )) - .into_boxed(); - - if let Some(community_id) = params.community_id { - query = query.filter(post::community_id.eq(community_id)); - }; - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_lock_post::mod_person_id.eq(mod_person_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); - }; - - if let Some(post_id) = params.post_id { - query = query.filter(post::id.eq(post_id)); - } - - // If a comment ID is given, then don't find any results - if params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_lock_post::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_remove_comment_view.rs b/crates/db_views_moderator/src/mod_remove_comment_view.rs deleted file mode 100644 index cf0ed325ca..0000000000 --- a/crates/db_views_moderator/src/mod_remove_comment_view.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::structs::{ModRemoveCommentView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{comment, community, mod_remove_comment, person, post}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModRemoveCommentView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let person_alias_1 = diesel::alias!(lemmy_db_schema::schema::person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_remove_comment::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_remove_comment::table - .left_join(person::table.on(admin_names_join)) - .inner_join(comment::table) - .inner_join(person_alias_1.on(comment::creator_id.eq(person_alias_1.field(person::id)))) - .inner_join(post::table.on(comment::post_id.eq(post::id))) - .inner_join(community::table.on(post::community_id.eq(community::id))) - .select(( - mod_remove_comment::all_columns, - person::all_columns.nullable(), - comment::all_columns, - person_alias_1.fields(person::all_columns), - post::all_columns, - community::all_columns, - )) - .into_boxed(); - - if let Some(community_id) = params.community_id { - query = query.filter(post::community_id.eq(community_id)); - }; - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_remove_comment::mod_person_id.eq(mod_person_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); - }; - - if let Some(comment_id) = params.comment_id { - query = query.filter(comment::id.eq(comment_id)); - } - - // If a post ID is given, then don't find any results - if params.post_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_remove_comment::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_remove_community_view.rs b/crates/db_views_moderator/src/mod_remove_community_view.rs deleted file mode 100644 index ac620ebdb9..0000000000 --- a/crates/db_views_moderator/src/mod_remove_community_view.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::structs::{ModRemoveCommunityView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, mod_remove_community, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModRemoveCommunityView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_remove_community::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_remove_community::table - .left_join(person::table.on(admin_names_join)) - .inner_join(community::table) - .select(( - mod_remove_community::all_columns, - person::all_columns.nullable(), - community::all_columns, - )) - .into_boxed(); - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_remove_community::mod_person_id.eq(mod_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_remove_community::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_remove_post_view.rs b/crates/db_views_moderator/src/mod_remove_post_view.rs deleted file mode 100644 index 98504a8e7f..0000000000 --- a/crates/db_views_moderator/src/mod_remove_post_view.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::structs::{ModRemovePostView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, mod_remove_post, person, post}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModRemovePostView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let person_alias_1 = diesel::alias!(person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_remove_post::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_remove_post::table - .left_join(person::table.on(admin_names_join)) - .inner_join(post::table) - .inner_join(community::table.on(post::community_id.eq(community::id))) - .inner_join(person_alias_1.on(post::creator_id.eq(person_alias_1.field(person::id)))) - .select(( - mod_remove_post::all_columns, - person::all_columns.nullable(), - post::all_columns, - community::all_columns, - )) - .into_boxed(); - - if let Some(community_id) = params.community_id { - query = query.filter(post::community_id.eq(community_id)); - }; - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_remove_post::mod_person_id.eq(mod_person_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); - }; - - if let Some(post_id) = params.post_id { - query = query.filter(post::id.eq(post_id)); - } - - // If a comment ID is given, then don't find any results - if params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_remove_post::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/mod_transfer_community_view.rs b/crates/db_views_moderator/src/mod_transfer_community_view.rs deleted file mode 100644 index 6d62d347a4..0000000000 --- a/crates/db_views_moderator/src/mod_transfer_community_view.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::structs::{ModTransferCommunityView, ModlogListParams}; -use diesel::{ - result::Error, - BoolExpressionMethods, - ExpressionMethods, - IntoSql, - JoinOnDsl, - NullableExpressionMethods, - QueryDsl, -}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::PersonId, - schema::{community, mod_transfer_community, person}, - utils::{get_conn, limit_and_offset, DbPool}, -}; - -impl ModTransferCommunityView { - pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - - let person_alias_1 = diesel::alias!(person as person1); - let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); - let show_mod_names = !params.hide_modlog_names; - let show_mod_names_expr = show_mod_names.as_sql::(); - - let admin_names_join = mod_transfer_community::mod_person_id - .eq(person::id) - .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); - let mut query = mod_transfer_community::table - .left_join(person::table.on(admin_names_join)) - .inner_join(community::table) - .inner_join( - person_alias_1 - .on(mod_transfer_community::other_person_id.eq(person_alias_1.field(person::id))), - ) - .select(( - mod_transfer_community::all_columns, - person::all_columns.nullable(), - community::all_columns, - person_alias_1.fields(person::all_columns), - )) - .into_boxed(); - - if let Some(mod_person_id) = params.mod_person_id { - query = query.filter(mod_transfer_community::mod_person_id.eq(mod_person_id)); - }; - - if let Some(community_id) = params.community_id { - query = query.filter(mod_transfer_community::community_id.eq(community_id)); - }; - - if let Some(other_person_id) = params.other_person_id { - query = query.filter(person_alias_1.field(person::id).eq(other_person_id)); - }; - - // If a post or comment ID is given, then don't find any results - if params.post_id.is_some() || params.comment_id.is_some() { - return Ok(vec![]); - } - - let (limit, offset) = limit_and_offset(params.page, params.limit)?; - - query - .limit(limit) - .offset(offset) - .order_by(mod_transfer_community::when_.desc()) - .load::(conn) - .await - } -} diff --git a/crates/db_views_moderator/src/modlog_combined_view.rs b/crates/db_views_moderator/src/modlog_combined_view.rs new file mode 100644 index 0000000000..3af4c87fe4 --- /dev/null +++ b/crates/db_views_moderator/src/modlog_combined_view.rs @@ -0,0 +1,1405 @@ +use crate::structs::{ + AdminAllowInstanceView, + AdminBlockInstanceView, + AdminPurgeCommentView, + AdminPurgeCommunityView, + AdminPurgePersonView, + AdminPurgePostView, + ModAddCommunityView, + ModAddView, + ModBanFromCommunityView, + ModBanView, + ModFeaturePostView, + ModHideCommunityView, + ModLockPostView, + ModRemoveCommentView, + ModRemoveCommunityView, + ModRemovePostView, + ModTransferCommunityView, + ModlogCombinedPaginationCursor, + ModlogCombinedView, + ModlogCombinedViewInternal, +}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, + SelectableHelper, +}; +use diesel_async::RunQueryDsl; +use i_love_jesus::PaginatedQueryBuilder; +use lemmy_db_schema::{ + aliases, + newtypes::{CommentId, CommunityId, PersonId, PostId}, + schema::{ + admin_allow_instance, + admin_block_instance, + admin_purge_comment, + admin_purge_community, + admin_purge_person, + admin_purge_post, + comment, + community, + instance, + mod_add, + mod_add_community, + mod_ban, + mod_ban_from_community, + mod_feature_post, + mod_hide_community, + mod_lock_post, + mod_remove_comment, + mod_remove_community, + mod_remove_post, + mod_transfer_community, + modlog_combined, + person, + post, + }, + source::combined::modlog::{modlog_combined_keys as key, ModlogCombined}, + utils::{get_conn, DbPool}, + InternalToCombinedView, + ModlogActionType, +}; +use lemmy_utils::error::LemmyResult; + +impl ModlogCombinedPaginationCursor { + // get cursor for page that starts immediately after the given post + pub fn after_post(view: &ModlogCombinedView) -> ModlogCombinedPaginationCursor { + let (prefix, id) = match view { + ModlogCombinedView::AdminAllowInstance(v) => { + ("AdminAllowInstance", v.admin_allow_instance.id.0) + } + ModlogCombinedView::AdminBlockInstance(v) => { + ("AdminBlockInstance", v.admin_block_instance.id.0) + } + ModlogCombinedView::AdminPurgeComment(v) => ("AdminPurgeComment", v.admin_purge_comment.id.0), + ModlogCombinedView::AdminPurgeCommunity(v) => { + ("AdminPurgeCommunity", v.admin_purge_community.id.0) + } + ModlogCombinedView::AdminPurgePerson(v) => ("AdminPurgePerson", v.admin_purge_person.id.0), + ModlogCombinedView::AdminPurgePost(v) => ("AdminPurgePost", v.admin_purge_post.id.0), + ModlogCombinedView::ModAdd(v) => ("ModAdd", v.mod_add.id.0), + ModlogCombinedView::ModAddCommunity(v) => ("ModAddCommunity", v.mod_add_community.id.0), + ModlogCombinedView::ModBan(v) => ("ModBan", v.mod_ban.id.0), + ModlogCombinedView::ModBanFromCommunity(v) => { + ("ModBanFromCommunity", v.mod_ban_from_community.id.0) + } + ModlogCombinedView::ModFeaturePost(v) => ("ModFeaturePost", v.mod_feature_post.id.0), + ModlogCombinedView::ModHideCommunity(v) => ("ModHideCommunity", v.mod_hide_community.id.0), + ModlogCombinedView::ModLockPost(v) => ("ModLockPost", v.mod_lock_post.id.0), + ModlogCombinedView::ModRemoveComment(v) => ("ModRemoveComment", v.mod_remove_comment.id.0), + ModlogCombinedView::ModRemoveCommunity(v) => { + ("ModRemoveCommunity", v.mod_remove_community.id.0) + } + ModlogCombinedView::ModRemovePost(v) => ("ModRemovePost", v.mod_remove_post.id.0), + ModlogCombinedView::ModTransferCommunity(v) => { + ("ModTransferCommunity", v.mod_transfer_community.id.0) + } + }; + // hex encoding to prevent ossification + ModlogCombinedPaginationCursor(format!("{prefix}{id:x}")) + } + + pub async fn read(&self, pool: &mut DbPool<'_>) -> Result { + let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into()); + let mut query = modlog_combined::table + .select(ModlogCombined::as_select()) + .into_boxed(); + let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?; + let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?; + query = match prefix { + "AdminAllowInstance" => query.filter(modlog_combined::admin_allow_instance_id.eq(id)), + "AdminBlockInstance" => query.filter(modlog_combined::admin_block_instance_id.eq(id)), + "AdminPurgeComment" => query.filter(modlog_combined::admin_purge_comment_id.eq(id)), + "AdminPurgeCommunity" => query.filter(modlog_combined::admin_purge_community_id.eq(id)), + "AdminPurgePerson" => query.filter(modlog_combined::admin_purge_person_id.eq(id)), + "AdminPurgePost" => query.filter(modlog_combined::admin_purge_post_id.eq(id)), + "ModAdd" => query.filter(modlog_combined::mod_add_id.eq(id)), + "ModAddCommunity" => query.filter(modlog_combined::mod_add_community_id.eq(id)), + "ModBan" => query.filter(modlog_combined::mod_ban_id.eq(id)), + "ModBanFromCommunity" => query.filter(modlog_combined::mod_ban_from_community_id.eq(id)), + "ModFeaturePost" => query.filter(modlog_combined::mod_feature_post_id.eq(id)), + "ModHideCommunity" => query.filter(modlog_combined::mod_hide_community_id.eq(id)), + "ModLockPost" => query.filter(modlog_combined::mod_lock_post_id.eq(id)), + "ModRemoveComment" => query.filter(modlog_combined::mod_remove_comment_id.eq(id)), + "ModRemoveCommunity" => query.filter(modlog_combined::mod_remove_community_id.eq(id)), + "ModRemovePost" => query.filter(modlog_combined::mod_remove_post_id.eq(id)), + "ModTransferCommunity" => query.filter(modlog_combined::mod_transfer_community_id.eq(id)), + + _ => return Err(err_msg()), + }; + let token = query.first(&mut get_conn(pool).await?).await?; + + Ok(PaginationCursorData(token)) + } +} + +#[derive(Clone)] +pub struct PaginationCursorData(ModlogCombined); + +#[derive(Default)] +/// Querying / filtering the modlog. +pub struct ModlogCombinedQuery { + pub type_: Option, + pub comment_id: Option, + pub post_id: Option, + pub community_id: Option, + pub hide_modlog_names: Option, + pub mod_person_id: Option, + pub modded_person_id: Option, + pub page_after: Option, + pub page_back: Option, +} + +impl ModlogCombinedQuery { + pub async fn list(self, pool: &mut DbPool<'_>) -> LemmyResult> { + let conn = &mut get_conn(pool).await?; + + let mod_person = self.mod_person_id.unwrap_or(PersonId(-1)); + let show_mod_names = !(self.hide_modlog_names.unwrap_or_default()); + let show_mod_names_expr = show_mod_names.as_sql::(); + + let modded_person = aliases::person1.field(person::id); + + // The query for the admin / mod person + // It needs an OR condition to every mod table + // After this you can use person::id to refer to the moderator + let moderator_names_join = show_mod_names_expr.or(person::id.eq(mod_person)).and( + admin_allow_instance::admin_person_id + .eq(person::id) + .or(admin_block_instance::admin_person_id.eq(person::id)) + .or(admin_purge_comment::admin_person_id.eq(person::id)) + .or(admin_purge_community::admin_person_id.eq(person::id)) + .or(admin_purge_person::admin_person_id.eq(person::id)) + .or(admin_purge_post::admin_person_id.eq(person::id)) + .or(mod_add::mod_person_id.eq(person::id)) + .or(mod_add_community::mod_person_id.eq(person::id)) + .or(mod_ban::mod_person_id.eq(person::id)) + .or(mod_ban_from_community::mod_person_id.eq(person::id)) + .or(mod_feature_post::mod_person_id.eq(person::id)) + .or(mod_hide_community::mod_person_id.eq(person::id)) + .or(mod_lock_post::mod_person_id.eq(person::id)) + .or(mod_remove_comment::mod_person_id.eq(person::id)) + .or(mod_remove_community::mod_person_id.eq(person::id)) + .or(mod_remove_post::mod_person_id.eq(person::id)) + .or(mod_transfer_community::mod_person_id.eq(person::id)), + ); + + let modded_person_join = mod_add::other_person_id + .eq(modded_person) + .or(mod_add_community::other_person_id.eq(modded_person)) + .or(mod_ban::other_person_id.eq(modded_person)) + .or(mod_ban_from_community::other_person_id.eq(modded_person)) + // Some tables don't have the modded_person_id directly, so you need to join + .or( + mod_feature_post::id + .is_not_null() + .and(post::creator_id.eq(modded_person)), + ) + .or( + mod_lock_post::id + .is_not_null() + .and(post::creator_id.eq(modded_person)), + ) + .or( + mod_remove_comment::id + .is_not_null() + .and(comment::creator_id.eq(modded_person)), + ) + .or( + mod_remove_post::id + .is_not_null() + .and(post::creator_id.eq(modded_person)), + ) + .or(mod_transfer_community::other_person_id.eq(modded_person)); + + let comment_join = mod_remove_comment::comment_id.eq(comment::id); + + let post_join = admin_purge_comment::post_id + .eq(post::id) + .or(mod_feature_post::post_id.eq(post::id)) + .or(mod_lock_post::post_id.eq(post::id)) + .or( + mod_remove_comment::id + .is_not_null() + .and(comment::post_id.eq(post::id)), + ) + .or(mod_remove_post::post_id.eq(post::id)); + + let community_join = admin_purge_post::community_id + .eq(community::id) + .or(mod_add_community::community_id.eq(community::id)) + .or(mod_ban_from_community::community_id.eq(community::id)) + .or( + mod_feature_post::id + .is_not_null() + .and(post::community_id.eq(community::id)), + ) + .or(mod_hide_community::community_id.eq(community::id)) + .or( + mod_lock_post::id + .is_not_null() + .and(post::community_id.eq(community::id)), + ) + .or( + mod_remove_comment::id + .is_not_null() + .and(post::community_id.eq(community::id)), + ) + .or(mod_remove_community::community_id.eq(community::id)) + .or( + mod_remove_post::id + .is_not_null() + .and(post::community_id.eq(community::id)), + ) + .or(mod_transfer_community::community_id.eq(community::id)); + + let instance_join = admin_allow_instance::instance_id + .eq(instance::id) + .or(admin_block_instance::instance_id.eq(instance::id)); + + let mut query = modlog_combined::table + .left_join(admin_allow_instance::table) + .left_join(admin_block_instance::table) + .left_join(admin_purge_comment::table) + .left_join(admin_purge_community::table) + .left_join(admin_purge_person::table) + .left_join(admin_purge_post::table) + .left_join(mod_add::table) + .left_join(mod_add_community::table) + .left_join(mod_ban::table) + .left_join(mod_ban_from_community::table) + .left_join(mod_feature_post::table) + .left_join(mod_hide_community::table) + .left_join(mod_lock_post::table) + .left_join(mod_remove_comment::table) + .left_join(mod_remove_community::table) + .left_join(mod_remove_post::table) + .left_join(mod_transfer_community::table) + // The moderator + .left_join(person::table.on(moderator_names_join)) + // The comment + .left_join(comment::table.on(comment_join)) + // The post + .left_join(post::table.on(post_join)) + // The community + .left_join(community::table.on(community_join)) + // The instance + .left_join(instance::table.on(instance_join)) + // The modded person + .left_join(aliases::person1.on(modded_person_join)) + .select(( + admin_allow_instance::all_columns.nullable(), + admin_block_instance::all_columns.nullable(), + admin_purge_comment::all_columns.nullable(), + admin_purge_community::all_columns.nullable(), + admin_purge_person::all_columns.nullable(), + admin_purge_post::all_columns.nullable(), + mod_add::all_columns.nullable(), + mod_add_community::all_columns.nullable(), + mod_ban::all_columns.nullable(), + mod_ban_from_community::all_columns.nullable(), + mod_feature_post::all_columns.nullable(), + mod_hide_community::all_columns.nullable(), + mod_lock_post::all_columns.nullable(), + mod_remove_comment::all_columns.nullable(), + mod_remove_community::all_columns.nullable(), + mod_remove_post::all_columns.nullable(), + mod_transfer_community::all_columns.nullable(), + // Shared + person::all_columns.nullable(), + aliases::person1.fields(person::all_columns).nullable(), + instance::all_columns.nullable(), + community::all_columns.nullable(), + post::all_columns.nullable(), + comment::all_columns.nullable(), + )) + .into_boxed(); + + if let Some(mod_person_id) = self.mod_person_id { + query = query.filter(person::id.eq(mod_person_id)); + }; + + if let Some(modded_person_id) = self.modded_person_id { + query = query.filter(modded_person.eq(modded_person_id)); + }; + + if let Some(community_id) = self.community_id { + query = query.filter(community::id.eq(community_id)) + } + + if let Some(post_id) = self.post_id { + query = query.filter(post::id.eq(post_id)) + } + + if let Some(comment_id) = self.comment_id { + query = query.filter(comment::id.eq(comment_id)) + } + + if let Some(type_) = self.type_ { + query = match type_ { + ModlogActionType::All => query, + ModlogActionType::ModRemovePost => { + query.filter(modlog_combined::mod_remove_post_id.is_not_null()) + } + ModlogActionType::ModLockPost => { + query.filter(modlog_combined::mod_lock_post_id.is_not_null()) + } + ModlogActionType::ModFeaturePost => { + query.filter(modlog_combined::mod_feature_post_id.is_not_null()) + } + ModlogActionType::ModRemoveComment => { + query.filter(modlog_combined::mod_remove_comment_id.is_not_null()) + } + ModlogActionType::ModRemoveCommunity => { + query.filter(modlog_combined::mod_remove_community_id.is_not_null()) + } + ModlogActionType::ModBanFromCommunity => { + query.filter(modlog_combined::mod_ban_from_community_id.is_not_null()) + } + ModlogActionType::ModAddCommunity => { + query.filter(modlog_combined::mod_add_community_id.is_not_null()) + } + ModlogActionType::ModTransferCommunity => { + query.filter(modlog_combined::mod_transfer_community_id.is_not_null()) + } + ModlogActionType::ModAdd => query.filter(modlog_combined::mod_add_id.is_not_null()), + ModlogActionType::ModBan => query.filter(modlog_combined::mod_ban_id.is_not_null()), + ModlogActionType::ModHideCommunity => { + query.filter(modlog_combined::mod_hide_community_id.is_not_null()) + } + ModlogActionType::AdminPurgePerson => { + query.filter(modlog_combined::admin_purge_person_id.is_not_null()) + } + ModlogActionType::AdminPurgeCommunity => { + query.filter(modlog_combined::admin_purge_community_id.is_not_null()) + } + ModlogActionType::AdminPurgePost => { + query.filter(modlog_combined::admin_purge_post_id.is_not_null()) + } + ModlogActionType::AdminPurgeComment => { + query.filter(modlog_combined::admin_purge_comment_id.is_not_null()) + } + ModlogActionType::AdminBlockInstance => { + query.filter(modlog_combined::admin_block_instance_id.is_not_null()) + } + ModlogActionType::AdminAllowInstance => { + query.filter(modlog_combined::admin_allow_instance_id.is_not_null()) + } + } + } + + let mut query = PaginatedQueryBuilder::new(query); + + let page_after = self.page_after.map(|c| c.0); + + if self.page_back.unwrap_or_default() { + query = query.before(page_after).limit_and_offset_from_end(); + } else { + query = query.after(page_after); + } + + // Tie breaker + query = query.then_desc(key::published).then_desc(key::id); + + let res = query.load::(conn).await?; + + // Map the query results to the enum + let out = res.into_iter().filter_map(|u| u.map_to_enum()).collect(); + + Ok(out) + } +} + +impl InternalToCombinedView for ModlogCombinedViewInternal { + type CombinedView = ModlogCombinedView; + + fn map_to_enum(&self) -> Option { + // Use for a short alias + let v = self.clone(); + + if let (Some(admin_allow_instance), Some(instance)) = + (v.admin_allow_instance, v.instance.clone()) + { + Some(ModlogCombinedView::AdminAllowInstance( + AdminAllowInstanceView { + admin_allow_instance, + instance, + admin: v.moderator, + }, + )) + } else if let (Some(admin_block_instance), Some(instance)) = + (v.admin_block_instance, v.instance) + { + Some(ModlogCombinedView::AdminBlockInstance( + AdminBlockInstanceView { + admin_block_instance, + instance, + admin: v.moderator, + }, + )) + } else if let (Some(admin_purge_comment), Some(post)) = (v.admin_purge_comment, v.post.clone()) + { + Some(ModlogCombinedView::AdminPurgeComment( + AdminPurgeCommentView { + admin_purge_comment, + post, + admin: v.moderator, + }, + )) + } else if let Some(admin_purge_community) = v.admin_purge_community { + Some(ModlogCombinedView::AdminPurgeCommunity( + AdminPurgeCommunityView { + admin_purge_community, + admin: v.moderator, + }, + )) + } else if let Some(admin_purge_person) = v.admin_purge_person { + Some(ModlogCombinedView::AdminPurgePerson(AdminPurgePersonView { + admin_purge_person, + admin: v.moderator, + })) + } else if let (Some(admin_purge_post), Some(community)) = + (v.admin_purge_post, v.community.clone()) + { + Some(ModlogCombinedView::AdminPurgePost(AdminPurgePostView { + admin_purge_post, + admin: v.moderator, + community, + })) + } else if let (Some(mod_add), Some(modded_person)) = (v.mod_add, v.modded_person.clone()) { + Some(ModlogCombinedView::ModAdd(ModAddView { + mod_add, + moderator: v.moderator, + modded_person, + })) + } else if let (Some(mod_add_community), Some(modded_person), Some(community)) = ( + v.mod_add_community, + v.modded_person.clone(), + v.community.clone(), + ) { + Some(ModlogCombinedView::ModAddCommunity(ModAddCommunityView { + mod_add_community, + moderator: v.moderator, + modded_person, + community, + })) + } else if let (Some(mod_ban), Some(modded_person)) = (v.mod_ban, v.modded_person.clone()) { + Some(ModlogCombinedView::ModBan(ModBanView { + mod_ban, + moderator: v.moderator, + modded_person, + })) + } else if let (Some(mod_ban_from_community), Some(modded_person), Some(community)) = ( + v.mod_ban_from_community, + v.modded_person.clone(), + v.community.clone(), + ) { + Some(ModlogCombinedView::ModBanFromCommunity( + ModBanFromCommunityView { + mod_ban_from_community, + moderator: v.moderator, + modded_person, + community, + }, + )) + } else if let (Some(mod_feature_post), Some(modded_person), Some(community), Some(post)) = ( + v.mod_feature_post, + v.modded_person.clone(), + v.community.clone(), + v.post.clone(), + ) { + Some(ModlogCombinedView::ModFeaturePost(ModFeaturePostView { + mod_feature_post, + moderator: v.moderator, + modded_person, + community, + post, + })) + } else if let (Some(mod_hide_community), Some(community)) = + (v.mod_hide_community, v.community.clone()) + { + Some(ModlogCombinedView::ModHideCommunity(ModHideCommunityView { + mod_hide_community, + admin: v.moderator, + community, + })) + } else if let (Some(mod_lock_post), Some(modded_person), Some(community), Some(post)) = ( + v.mod_lock_post, + v.modded_person.clone(), + v.community.clone(), + v.post.clone(), + ) { + Some(ModlogCombinedView::ModLockPost(ModLockPostView { + mod_lock_post, + moderator: v.moderator, + modded_person, + community, + post, + })) + } else if let ( + Some(mod_remove_comment), + Some(modded_person), + Some(community), + Some(post), + Some(comment), + ) = ( + v.mod_remove_comment, + v.modded_person.clone(), + v.community.clone(), + v.post.clone(), + v.comment, + ) { + Some(ModlogCombinedView::ModRemoveComment(ModRemoveCommentView { + mod_remove_comment, + moderator: v.moderator, + modded_person, + community, + post, + comment, + })) + } else if let (Some(mod_remove_community), Some(community)) = + (v.mod_remove_community, v.community.clone()) + { + Some(ModlogCombinedView::ModRemoveCommunity( + ModRemoveCommunityView { + mod_remove_community, + moderator: v.moderator, + community, + }, + )) + } else if let (Some(mod_remove_post), Some(modded_person), Some(community), Some(post)) = ( + v.mod_remove_post, + v.modded_person.clone(), + v.community.clone(), + v.post.clone(), + ) { + Some(ModlogCombinedView::ModRemovePost(ModRemovePostView { + mod_remove_post, + moderator: v.moderator, + modded_person, + community, + post, + })) + } else if let (Some(mod_transfer_community), Some(modded_person), Some(community)) = ( + v.mod_transfer_community, + v.modded_person.clone(), + v.community.clone(), + ) { + Some(ModlogCombinedView::ModTransferCommunity( + ModTransferCommunityView { + mod_transfer_community, + moderator: v.moderator, + modded_person, + community, + }, + )) + } else { + None + } + } +} + +#[cfg(test)] +#[expect(clippy::indexing_slicing)] +mod tests { + + use crate::{modlog_combined_view::ModlogCombinedQuery, structs::ModlogCombinedView}; + use lemmy_db_schema::{ + newtypes::PersonId, + source::{ + comment::{Comment, CommentInsertForm}, + community::{Community, CommunityInsertForm}, + instance::Instance, + mod_log::{ + admin::{ + AdminAllowInstance, + AdminAllowInstanceForm, + AdminBlockInstance, + AdminBlockInstanceForm, + AdminPurgeComment, + AdminPurgeCommentForm, + AdminPurgeCommunity, + AdminPurgeCommunityForm, + AdminPurgePerson, + AdminPurgePersonForm, + AdminPurgePost, + AdminPurgePostForm, + }, + moderator::{ + ModAdd, + ModAddCommunity, + ModAddCommunityForm, + ModAddForm, + ModBan, + ModBanForm, + ModBanFromCommunity, + ModBanFromCommunityForm, + ModFeaturePost, + ModFeaturePostForm, + ModHideCommunity, + ModHideCommunityForm, + ModLockPost, + ModLockPostForm, + ModRemoveComment, + ModRemoveCommentForm, + ModRemoveCommunity, + ModRemoveCommunityForm, + ModRemovePost, + ModRemovePostForm, + ModTransferCommunity, + ModTransferCommunityForm, + }, + }, + person::{Person, PersonInsertForm}, + post::{Post, PostInsertForm}, + }, + traits::Crud, + utils::{build_db_pool_for_tests, DbPool}, + ModlogActionType, + }; + use lemmy_utils::error::LemmyResult; + use pretty_assertions::assert_eq; + use serial_test::serial; + + struct Data { + instance: Instance, + timmy: Person, + sara: Person, + jessica: Person, + community: Community, + community_2: Community, + post: Post, + post_2: Post, + comment: Comment, + comment_2: Comment, + } + + async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult { + let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?; + + let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_rcv"); + let timmy = Person::create(pool, &timmy_form).await?; + + let sara_form = PersonInsertForm::test_form(instance.id, "sara_rcv"); + let sara = Person::create(pool, &sara_form).await?; + + let jessica_form = PersonInsertForm::test_form(instance.id, "jessica_mrv"); + let jessica = Person::create(pool, &jessica_form).await?; + + let community_form = CommunityInsertForm::new( + instance.id, + "test community crv".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community = Community::create(pool, &community_form).await?; + + let community_form_2 = CommunityInsertForm::new( + instance.id, + "test community crv 2".to_string(), + "nada".to_owned(), + "pubkey".to_string(), + ); + let community_2 = Community::create(pool, &community_form_2).await?; + + let post_form = PostInsertForm::new("A test post crv".into(), timmy.id, community.id); + let post = Post::create(pool, &post_form).await?; + + let new_post_2 = PostInsertForm::new("A test post crv 2".into(), sara.id, community_2.id); + let post_2 = Post::create(pool, &new_post_2).await?; + + // Timmy creates a comment + let comment_form = CommentInsertForm::new(timmy.id, post.id, "A test comment rv".into()); + let comment = Comment::create(pool, &comment_form, None).await?; + + // jessica creates a comment + let comment_form_2 = + CommentInsertForm::new(jessica.id, post_2.id, "A test comment rv 2".into()); + let comment_2 = Comment::create(pool, &comment_form_2, None).await?; + + Ok(Data { + instance, + timmy, + sara, + jessica, + community, + community_2, + post, + post_2, + comment, + comment_2, + }) + } + + async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> { + Instance::delete(pool, data.instance.id).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn admin_types() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + let form = AdminAllowInstanceForm { + instance_id: data.instance.id, + admin_person_id: data.timmy.id, + allowed: true, + reason: None, + }; + AdminAllowInstance::create(pool, &form).await?; + + let form = AdminBlockInstanceForm { + instance_id: data.instance.id, + admin_person_id: data.timmy.id, + blocked: true, + reason: None, + }; + AdminBlockInstance::create(pool, &form).await?; + + let form = AdminPurgeCommentForm { + admin_person_id: data.timmy.id, + post_id: data.post.id, + reason: None, + }; + AdminPurgeComment::create(pool, &form).await?; + + let form = AdminPurgeCommunityForm { + admin_person_id: data.timmy.id, + reason: None, + }; + AdminPurgeCommunity::create(pool, &form).await?; + + let form = AdminPurgePersonForm { + admin_person_id: data.timmy.id, + reason: None, + }; + AdminPurgePerson::create(pool, &form).await?; + + let form = AdminPurgePostForm { + admin_person_id: data.timmy.id, + community_id: data.community.id, + reason: None, + }; + AdminPurgePost::create(pool, &form).await?; + + let form = ModHideCommunityForm { + mod_person_id: data.timmy.id, + community_id: data.community.id, + hidden: Some(true), + reason: None, + }; + ModHideCommunity::create(pool, &form).await?; + + // A 2nd mod hide community, but to a different community, and with jessica + let form = ModHideCommunityForm { + mod_person_id: data.jessica.id, + community_id: data.community_2.id, + hidden: Some(true), + reason: None, + }; + ModHideCommunity::create(pool, &form).await?; + + let modlog = ModlogCombinedQuery::default().list(pool).await?; + assert_eq!(8, modlog.len()); + + if let ModlogCombinedView::ModHideCommunity(v) = &modlog[0] { + assert_eq!(data.community_2.id, v.mod_hide_community.community_id); + assert_eq!(data.community_2.id, v.community.id); + assert_eq!( + data.jessica.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModHideCommunity(v) = &modlog[1] { + assert_eq!(data.community.id, v.mod_hide_community.community_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::AdminPurgePost(v) = &modlog[2] { + assert_eq!(data.community.id, v.admin_purge_post.community_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::AdminPurgePerson(v) = &modlog[3] { + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::AdminPurgeCommunity(v) = &modlog[4] { + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::AdminPurgeComment(v) = &modlog[5] { + assert_eq!(data.post.id, v.admin_purge_comment.post_id); + assert_eq!(data.post.id, v.post.id); + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + // Make sure the report types are correct + if let ModlogCombinedView::AdminBlockInstance(v) = &modlog[6] { + assert_eq!(data.instance.id, v.admin_block_instance.instance_id); + assert_eq!(data.instance.id, v.instance.id); + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::AdminAllowInstance(v) = &modlog[7] { + assert_eq!(data.instance.id, v.admin_allow_instance.instance_id); + assert_eq!(data.instance.id, v.instance.id); + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + // Filter by admin + let modlog_admin_filter = ModlogCombinedQuery { + mod_person_id: Some(data.timmy.id), + ..Default::default() + } + .list(pool) + .await?; + // Only one is jessica + assert_eq!(7, modlog_admin_filter.len()); + + // Filter by community + let modlog_community_filter = ModlogCombinedQuery { + community_id: Some(data.community.id), + ..Default::default() + } + .list(pool) + .await?; + + // Should be 2, and not jessicas + assert_eq!(2, modlog_community_filter.len()); + + // Filter by type + let modlog_type_filter = ModlogCombinedQuery { + type_: Some(ModlogActionType::ModHideCommunity), + ..Default::default() + } + .list(pool) + .await?; + + // 2 of these, one is jessicas + assert_eq!(2, modlog_type_filter.len()); + + if let ModlogCombinedView::ModHideCommunity(v) = &modlog_type_filter[0] { + assert_eq!(data.community_2.id, v.mod_hide_community.community_id); + assert_eq!(data.community_2.id, v.community.id); + assert_eq!( + data.jessica.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModHideCommunity(v) = &modlog_type_filter[1] { + assert_eq!(data.community.id, v.mod_hide_community.community_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn mod_types() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + let form = ModAddForm { + mod_person_id: data.timmy.id, + other_person_id: data.jessica.id, + removed: Some(false), + }; + ModAdd::create(pool, &form).await?; + + let form = ModAddCommunityForm { + mod_person_id: data.timmy.id, + other_person_id: data.jessica.id, + community_id: data.community.id, + removed: Some(false), + }; + ModAddCommunity::create(pool, &form).await?; + + let form = ModBanForm { + mod_person_id: data.timmy.id, + other_person_id: data.jessica.id, + banned: Some(true), + reason: None, + expires: None, + }; + ModBan::create(pool, &form).await?; + + let form = ModBanFromCommunityForm { + mod_person_id: data.timmy.id, + other_person_id: data.jessica.id, + community_id: data.community.id, + banned: Some(true), + reason: None, + expires: None, + }; + ModBanFromCommunity::create(pool, &form).await?; + + let form = ModFeaturePostForm { + mod_person_id: data.timmy.id, + post_id: data.post.id, + featured: Some(true), + is_featured_community: None, + }; + ModFeaturePost::create(pool, &form).await?; + + let form = ModLockPostForm { + mod_person_id: data.timmy.id, + post_id: data.post.id, + locked: Some(true), + }; + ModLockPost::create(pool, &form).await?; + + let form = ModRemoveCommentForm { + mod_person_id: data.timmy.id, + comment_id: data.comment.id, + removed: Some(true), + reason: None, + }; + ModRemoveComment::create(pool, &form).await?; + + let form = ModRemoveCommunityForm { + mod_person_id: data.timmy.id, + community_id: data.community.id, + removed: Some(true), + reason: None, + }; + ModRemoveCommunity::create(pool, &form).await?; + + let form = ModRemovePostForm { + mod_person_id: data.timmy.id, + post_id: data.post.id, + removed: Some(true), + reason: None, + }; + ModRemovePost::create(pool, &form).await?; + + let form = ModTransferCommunityForm { + mod_person_id: data.timmy.id, + other_person_id: data.jessica.id, + community_id: data.community.id, + }; + ModTransferCommunity::create(pool, &form).await?; + + // A few extra ones to test different filters + let form = ModTransferCommunityForm { + mod_person_id: data.jessica.id, + other_person_id: data.sara.id, + community_id: data.community_2.id, + }; + ModTransferCommunity::create(pool, &form).await?; + + let form = ModRemovePostForm { + mod_person_id: data.jessica.id, + post_id: data.post_2.id, + removed: Some(true), + reason: None, + }; + ModRemovePost::create(pool, &form).await?; + + let form = ModRemoveCommentForm { + mod_person_id: data.jessica.id, + comment_id: data.comment_2.id, + removed: Some(true), + reason: None, + }; + ModRemoveComment::create(pool, &form).await?; + + // The all view + let modlog = ModlogCombinedQuery::default().list(pool).await?; + assert_eq!(13, modlog.len()); + + if let ModlogCombinedView::ModRemoveComment(v) = &modlog[0] { + assert_eq!(data.comment_2.id, v.mod_remove_comment.comment_id); + assert_eq!(data.comment_2.id, v.comment.id); + assert_eq!(data.post_2.id, v.post.id); + assert_eq!(data.community_2.id, v.community.id); + assert_eq!( + data.jessica.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.jessica.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModRemovePost(v) = &modlog[1] { + assert_eq!(data.post_2.id, v.mod_remove_post.post_id); + assert_eq!(data.post_2.id, v.post.id); + assert_eq!(data.sara.id, v.post.creator_id); + assert_eq!(data.community_2.id, v.community.id); + assert_eq!( + data.jessica.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.sara.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModTransferCommunity(v) = &modlog[2] { + assert_eq!(data.community_2.id, v.mod_transfer_community.community_id); + assert_eq!(data.community_2.id, v.community.id); + assert_eq!( + data.jessica.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.sara.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModTransferCommunity(v) = &modlog[3] { + assert_eq!(data.community.id, v.mod_transfer_community.community_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.jessica.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModRemovePost(v) = &modlog[4] { + assert_eq!(data.post.id, v.mod_remove_post.post_id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.timmy.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModRemoveCommunity(v) = &modlog[5] { + assert_eq!(data.community.id, v.mod_remove_community.community_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModRemoveComment(v) = &modlog[6] { + assert_eq!(data.comment.id, v.mod_remove_comment.comment_id); + assert_eq!(data.comment.id, v.comment.id); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.timmy.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModLockPost(v) = &modlog[7] { + assert_eq!(data.post.id, v.mod_lock_post.post_id); + assert!(v.mod_lock_post.locked); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.timmy.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModFeaturePost(v) = &modlog[8] { + assert_eq!(data.post.id, v.mod_feature_post.post_id); + assert!(v.mod_feature_post.featured); + assert_eq!(data.post.id, v.post.id); + assert_eq!(data.timmy.id, v.post.creator_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.timmy.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModBanFromCommunity(v) = &modlog[9] { + assert_eq!(data.community.id, v.mod_ban_from_community.community_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.jessica.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModBan(v) = &modlog[10] { + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.jessica.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModAddCommunity(v) = &modlog[11] { + assert_eq!(data.community.id, v.mod_add_community.community_id); + assert_eq!(data.community.id, v.community.id); + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.jessica.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + if let ModlogCombinedView::ModAdd(v) = &modlog[12] { + assert_eq!( + data.timmy.id, + v.moderator.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + assert_eq!(data.jessica.id, v.modded_person.id); + } else { + panic!("wrong type"); + } + + // Filter by moderator + let modlog_mod_timmy_filter = ModlogCombinedQuery { + mod_person_id: Some(data.timmy.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(10, modlog_mod_timmy_filter.len()); + + let modlog_mod_jessica_filter = ModlogCombinedQuery { + mod_person_id: Some(data.jessica.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(3, modlog_mod_jessica_filter.len()); + + // Filter by other_person + // Gets a little complicated because things aren't directly linked, + // you have to go into the item to see who created it. + + let modlog_modded_timmy_filter = ModlogCombinedQuery { + modded_person_id: Some(data.timmy.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(4, modlog_modded_timmy_filter.len()); + + let modlog_modded_jessica_filter = ModlogCombinedQuery { + modded_person_id: Some(data.jessica.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(6, modlog_modded_jessica_filter.len()); + + let modlog_modded_sara_filter = ModlogCombinedQuery { + modded_person_id: Some(data.sara.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(2, modlog_modded_sara_filter.len()); + + // Filter by community + let modlog_community_filter = ModlogCombinedQuery { + community_id: Some(data.community.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(8, modlog_community_filter.len()); + + let modlog_community_2_filter = ModlogCombinedQuery { + community_id: Some(data.community_2.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(3, modlog_community_2_filter.len()); + + // Filter by post + let modlog_post_filter = ModlogCombinedQuery { + post_id: Some(data.post.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(4, modlog_post_filter.len()); + + let modlog_post_2_filter = ModlogCombinedQuery { + post_id: Some(data.post_2.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(2, modlog_post_2_filter.len()); + + // Filter by comment + let modlog_comment_filter = ModlogCombinedQuery { + comment_id: Some(data.comment.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(1, modlog_comment_filter.len()); + + let modlog_comment_2_filter = ModlogCombinedQuery { + comment_id: Some(data.comment_2.id), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(1, modlog_comment_2_filter.len()); + + // Filter by type + let modlog_type_filter = ModlogCombinedQuery { + type_: Some(ModlogActionType::ModRemoveComment), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(2, modlog_type_filter.len()); + + // Assert that the types are correct + assert!(matches!( + modlog_type_filter[0], + ModlogCombinedView::ModRemoveComment(_) + )); + assert!(matches!( + modlog_type_filter[1], + ModlogCombinedView::ModRemoveComment(_) + )); + + cleanup(data, pool).await?; + + Ok(()) + } + + #[tokio::test] + #[serial] + async fn hide_modlog_names() -> LemmyResult<()> { + let pool = &build_db_pool_for_tests(); + let pool = &mut pool.into(); + let data = init_data(pool).await?; + + let form = AdminAllowInstanceForm { + instance_id: data.instance.id, + admin_person_id: data.timmy.id, + allowed: true, + reason: None, + }; + AdminAllowInstance::create(pool, &form).await?; + + let modlog = ModlogCombinedQuery::default().list(pool).await?; + assert_eq!(1, modlog.len()); + + if let ModlogCombinedView::AdminAllowInstance(v) = &modlog[0] { + assert_eq!( + data.timmy.id, + v.admin.as_ref().map(|a| a.id).unwrap_or(PersonId(-1)) + ); + } else { + panic!("wrong type"); + } + + // Filter out the names + let modlog_hide_names_filter = ModlogCombinedQuery { + hide_modlog_names: Some(true), + ..Default::default() + } + .list(pool) + .await?; + assert_eq!(1, modlog_hide_names_filter.len()); + + if let ModlogCombinedView::AdminAllowInstance(v) = &modlog_hide_names_filter[0] { + assert!(v.admin.is_none()) + } else { + panic!("wrong type"); + } + + cleanup(data, pool).await?; + + Ok(()) + } +} diff --git a/crates/db_views_moderator/src/structs.rs b/crates/db_views_moderator/src/structs.rs index 06e9f099a4..96aa27b597 100644 --- a/crates/db_views_moderator/src/structs.rs +++ b/crates/db_views_moderator/src/structs.rs @@ -1,37 +1,34 @@ #[cfg(feature = "full")] use diesel::Queryable; -use lemmy_db_schema::{ - newtypes::{CommentId, CommunityId, PersonId, PostId}, - source::{ - comment::Comment, - community::Community, - instance::Instance, - mod_log::{ - admin::{ - AdminAllowInstance, - AdminBlockInstance, - AdminPurgeComment, - AdminPurgeCommunity, - AdminPurgePerson, - AdminPurgePost, - }, - moderator::{ - ModAdd, - ModAddCommunity, - ModBan, - ModBanFromCommunity, - ModFeaturePost, - ModHideCommunity, - ModLockPost, - ModRemoveComment, - ModRemoveCommunity, - ModRemovePost, - ModTransferCommunity, - }, +use lemmy_db_schema::source::{ + comment::Comment, + community::Community, + instance::Instance, + mod_log::{ + admin::{ + AdminAllowInstance, + AdminBlockInstance, + AdminPurgeComment, + AdminPurgeCommunity, + AdminPurgePerson, + AdminPurgePost, + }, + moderator::{ + ModAdd, + ModAddCommunity, + ModBan, + ModBanFromCommunity, + ModFeaturePost, + ModHideCommunity, + ModLockPost, + ModRemoveComment, + ModRemoveCommunity, + ModRemovePost, + ModTransferCommunity, }, - person::Person, - post::Post, }, + person::Person, + post::Post, }; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -39,7 +36,7 @@ use serde_with::skip_serializing_none; use ts_rs::TS; #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -53,7 +50,7 @@ pub struct ModAddCommunityView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -66,7 +63,7 @@ pub struct ModAddView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -76,11 +73,11 @@ pub struct ModBanFromCommunityView { #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, pub community: Community, - pub banned_person: Person, + pub modded_person: Person, } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -89,11 +86,11 @@ pub struct ModBanView { pub mod_ban: ModBan, #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, - pub banned_person: Person, + pub modded_person: Person, } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -106,7 +103,7 @@ pub struct ModHideCommunityView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -115,12 +112,13 @@ pub struct ModLockPostView { pub mod_lock_post: ModLockPost, #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, + pub modded_person: Person, pub post: Post, pub community: Community, } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -129,14 +127,14 @@ pub struct ModRemoveCommentView { pub mod_remove_comment: ModRemoveComment, #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, + pub modded_person: Person, pub comment: Comment, - pub commenter: Person, pub post: Post, pub community: Community, } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -149,7 +147,7 @@ pub struct ModRemoveCommunityView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -158,12 +156,13 @@ pub struct ModRemovePostView { pub mod_remove_post: ModRemovePost, #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, + pub modded_person: Person, pub post: Post, pub community: Community, } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -172,12 +171,13 @@ pub struct ModFeaturePostView { pub mod_feature_post: ModFeaturePost, #[cfg_attr(feature = "full", ts(optional))] pub moderator: Option, + pub modded_person: Person, pub post: Post, pub community: Community, } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -191,7 +191,7 @@ pub struct ModTransferCommunityView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -204,7 +204,7 @@ pub struct AdminPurgeCommentView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -216,7 +216,7 @@ pub struct AdminPurgeCommunityView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -228,7 +228,7 @@ pub struct AdminPurgePersonView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -241,7 +241,7 @@ pub struct AdminPurgePostView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] @@ -254,38 +254,79 @@ pub struct AdminBlockInstanceView { } #[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", ts(export))] /// When an admin purges a post. pub struct AdminAllowInstanceView { - pub admin_block_instance: AdminAllowInstance, + pub admin_allow_instance: AdminAllowInstance, pub instance: Instance, #[cfg_attr(feature = "full", ts(optional))] pub admin: Option, } -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -#[cfg_attr(feature = "full", derive(TS, Queryable))] +/// like PaginationCursor but for the modlog_combined +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ModlogCombinedPaginationCursor(pub String); + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(Queryable))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +/// A combined modlog view +pub struct ModlogCombinedViewInternal { + // Specific + pub admin_allow_instance: Option, + pub admin_block_instance: Option, + pub admin_purge_comment: Option, + pub admin_purge_community: Option, + pub admin_purge_person: Option, + pub admin_purge_post: Option, + pub mod_add: Option, + pub mod_add_community: Option, + pub mod_ban: Option, + pub mod_ban_from_community: Option, + pub mod_feature_post: Option, + pub mod_hide_community: Option, + pub mod_lock_post: Option, + pub mod_remove_comment: Option, + pub mod_remove_community: Option, + pub mod_remove_post: Option, + pub mod_transfer_community: Option, + // Specific fields + + // Shared + pub moderator: Option, + pub modded_person: Option, + pub instance: Option, + pub community: Option, + pub post: Option, + pub comment: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -/// Querying / filtering the modlog. -pub struct ModlogListParams { - #[cfg_attr(feature = "full", ts(optional))] - pub community_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub mod_person_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub other_person_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub post_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub comment_id: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub page: Option, - #[cfg_attr(feature = "full", ts(optional))] - pub limit: Option, - pub hide_modlog_names: bool, +// Use serde's internal tagging, to work easier with javascript libraries +#[serde(tag = "type_")] +pub enum ModlogCombinedView { + AdminAllowInstance(AdminAllowInstanceView), + AdminBlockInstance(AdminBlockInstanceView), + AdminPurgeComment(AdminPurgeCommentView), + AdminPurgeCommunity(AdminPurgeCommunityView), + AdminPurgePerson(AdminPurgePersonView), + AdminPurgePost(AdminPurgePostView), + ModAdd(ModAddView), + ModAddCommunity(ModAddCommunityView), + ModBan(ModBanView), + ModBanFromCommunity(ModBanFromCommunityView), + ModFeaturePost(ModFeaturePostView), + ModHideCommunity(ModHideCommunityView), + ModLockPost(ModLockPostView), + ModRemoveComment(ModRemoveCommentView), + ModRemoveCommunity(ModRemoveCommunityView), + ModRemovePost(ModRemovePostView), + ModTransferCommunity(ModTransferCommunityView), } diff --git a/crates/routes/src/feeds.rs b/crates/routes/src/feeds.rs index 55e9cc7f3c..810daf4c15 100644 --- a/crates/routes/src/feeds.rs +++ b/crates/routes/src/feeds.rs @@ -6,7 +6,6 @@ use lemmy_api_common::{context::LemmyContext, utils::check_private_instance}; use lemmy_db_schema::{ source::{community::Community, person::Person}, traits::ApubActor, - CommentSortType, CommunityVisibility, ListingType, PostSortType, @@ -15,11 +14,7 @@ use lemmy_db_views::{ post_view::PostQuery, structs::{PostView, SiteView}, }; -use lemmy_db_views_actor::{ - comment_reply_view::CommentReplyQuery, - person_mention_view::PersonMentionQuery, - structs::{CommentReplyView, PersonMentionView}, -}; +use lemmy_db_views_actor::{inbox_combined_view::InboxCombinedQuery, structs::InboxCombinedView}; use lemmy_utils::{ cache_header::cache_1hour, error::{LemmyError, LemmyErrorType, LemmyResult}, @@ -360,37 +355,20 @@ async fn get_feed_front( async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult { let site_view = SiteView::read_local(&mut context.pool()).await?; let local_user = local_user_view_from_jwt(jwt, context).await?; - let person_id = local_user.local_user.person_id; - let show_bot_accounts = local_user.local_user.show_bot_accounts; - - let sort = CommentSortType::New; + let my_person_id = local_user.person.id; + let show_bot_accounts = Some(local_user.local_user.show_bot_accounts); check_private_instance(&Some(local_user.clone()), &site_view.local_site)?; - let replies = CommentReplyQuery { - recipient_id: (Some(person_id)), - my_person_id: (Some(person_id)), - show_bot_accounts: (show_bot_accounts), - sort: (Some(sort)), - limit: (Some(RSS_FETCH_LIMIT)), - ..Default::default() - } - .list(&mut context.pool()) - .await?; - - let mentions = PersonMentionQuery { - recipient_id: (Some(person_id)), - my_person_id: (Some(person_id)), - show_bot_accounts: (show_bot_accounts), - sort: (Some(sort)), - limit: (Some(RSS_FETCH_LIMIT)), + let inbox = InboxCombinedQuery { + show_bot_accounts, ..Default::default() } - .list(&mut context.pool()) + .list(&mut context.pool(), my_person_id) .await?; let protocol_and_hostname = context.settings().get_protocol_and_hostname(); - let items = create_reply_and_mention_items(replies, mentions, &protocol_and_hostname)?; + let items = create_reply_and_mention_items(inbox, &protocol_and_hostname)?; let mut channel = Channel { namespaces: RSS_NAMESPACE.clone(), @@ -409,39 +387,55 @@ async fn get_feed_inbox(context: &LemmyContext, jwt: &str) -> LemmyResult, - mentions: Vec, + inbox: Vec, protocol_and_hostname: &str, ) -> LemmyResult> { - let mut reply_items: Vec = replies + let reply_items: Vec = inbox .iter() - .map(|r| { - let reply_url = format!("{}/comment/{}", protocol_and_hostname, r.comment.id); - build_item( - &r.creator.name, - &r.comment.published, - &reply_url, - &r.comment.content, - protocol_and_hostname, - ) - }) - .collect::>>()?; - - let mut mention_items: Vec = mentions - .iter() - .map(|m| { - let mention_url = format!("{}/comment/{}", protocol_and_hostname, m.comment.id); - build_item( - &m.creator.name, - &m.comment.published, - &mention_url, - &m.comment.content, - protocol_and_hostname, - ) + .map(|r| match r { + InboxCombinedView::CommentReply(v) => { + let reply_url = format!("{}/comment/{}", protocol_and_hostname, v.comment.id); + build_item( + &v.creator.name, + &v.comment.published, + &reply_url, + &v.comment.content, + protocol_and_hostname, + ) + } + InboxCombinedView::CommentMention(v) => { + let mention_url = format!("{}/comment/{}", protocol_and_hostname, v.comment.id); + build_item( + &v.creator.name, + &v.comment.published, + &mention_url, + &v.comment.content, + protocol_and_hostname, + ) + } + InboxCombinedView::PostMention(v) => { + let mention_url = format!("{}/post/{}", protocol_and_hostname, v.post.id); + build_item( + &v.creator.name, + &v.post.published, + &mention_url, + &v.post.body.clone().unwrap_or_default(), + protocol_and_hostname, + ) + } + InboxCombinedView::PrivateMessage(v) => { + let inbox_url = format!("{}/inbox", protocol_and_hostname); + build_item( + &v.creator.name, + &v.private_message.published, + &inbox_url, + &v.private_message.content, + protocol_and_hostname, + ) + } }) .collect::>>()?; - reply_items.append(&mut mention_items); Ok(reply_items) } diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index f45bc271f6..d9db32428a 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -106,7 +106,7 @@ pub enum LemmyErrorType { CouldntHidePost, CouldntUpdateCommunity, CouldntUpdateReplies, - CouldntUpdatePersonMentions, + CouldntUpdatePersonCommentMentions, CouldntCreatePost, CouldntCreatePrivateMessage, CouldntUpdatePrivate, diff --git a/migrations/2024-12-02-181601_add_report_combined_table/down.sql b/migrations/2024-12-02-181601_add_report_combined_table/down.sql new file mode 100644 index 0000000000..b27ba9bc43 --- /dev/null +++ b/migrations/2024-12-02-181601_add_report_combined_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE report_combined; + diff --git a/migrations/2024-12-02-181601_add_report_combined_table/up.sql b/migrations/2024-12-02-181601_add_report_combined_table/up.sql new file mode 100644 index 0000000000..8efb2a0742 --- /dev/null +++ b/migrations/2024-12-02-181601_add_report_combined_table/up.sql @@ -0,0 +1,42 @@ +-- Creates combined tables for +-- Reports: (comment, post, and private_message) +CREATE TABLE report_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + post_report_id int UNIQUE REFERENCES post_report ON UPDATE CASCADE ON DELETE CASCADE, + comment_report_id int UNIQUE REFERENCES comment_report ON UPDATE CASCADE ON DELETE CASCADE, + private_message_report_id int UNIQUE REFERENCES private_message_report ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK (num_nonnulls (post_report_id, comment_report_id, private_message_report_id) = 1) +); + +CREATE INDEX idx_report_combined_published ON report_combined (published DESC, id DESC); + +CREATE INDEX idx_report_combined_published_asc ON report_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO report_combined (published, post_report_id, comment_report_id, private_message_report_id) +SELECT + published, + id, + NULL::int, + NULL::int +FROM + post_report +UNION ALL +SELECT + published, + NULL::int, + id, + NULL::int +FROM + comment_report +UNION ALL +SELECT + published, + NULL::int, + NULL::int, + id +FROM + private_message_report; + diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql new file mode 100644 index 0000000000..0733315a71 --- /dev/null +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/down.sql @@ -0,0 +1,4 @@ +DROP TABLE person_content_combined; + +DROP TABLE person_saved_combined; + diff --git a/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql new file mode 100644 index 0000000000..cbef85ecca --- /dev/null +++ b/migrations/2024-12-05-233704_add_person_content_combined_table/up.sql @@ -0,0 +1,71 @@ +-- Creates combined tables for +-- person_content: (comment, post) +-- person_saved: (comment, post) +CREATE TABLE person_content_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK (num_nonnulls (post_id, comment_id) = 1) +); + +CREATE INDEX idx_person_content_combined_published ON person_content_combined (published DESC, id DESC); + +CREATE INDEX idx_person_content_combined_published_asc ON person_content_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO person_content_combined (published, post_id, comment_id) +SELECT + published, + id, + NULL::int +FROM + post +UNION ALL +SELECT + published, + NULL::int, + id +FROM + comment; + +-- This one is special, because you use the saved date, not the ordinary published +CREATE TABLE person_saved_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + person_id int NOT NULL REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE, + post_id int UNIQUE REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE, + comment_id int UNIQUE REFERENCES COMMENT ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK (num_nonnulls (post_id, comment_id) = 1) +); + +CREATE INDEX idx_person_saved_combined_published ON person_saved_combined (published DESC, id DESC); + +CREATE INDEX idx_person_saved_combined_published_asc ON person_saved_combined (reverse_timestamp_sort (published) DESC, id DESC); + +CREATE INDEX idx_person_saved_combined ON person_saved_combined (person_id); + +-- Updating the history +INSERT INTO person_saved_combined (published, person_id, post_id, comment_id) +SELECT + saved, + person_id, + post_id, + NULL::int +FROM + post_actions +WHERE + saved IS NOT NULL +UNION ALL +SELECT + saved, + person_id, + NULL::int, + comment_id +FROM + comment_actions +WHERE + saved IS NOT NULL; + diff --git a/migrations/2024-12-08-165614_add_modlog_combined_table/down.sql b/migrations/2024-12-08-165614_add_modlog_combined_table/down.sql new file mode 100644 index 0000000000..4b555e002a --- /dev/null +++ b/migrations/2024-12-08-165614_add_modlog_combined_table/down.sql @@ -0,0 +1,2 @@ +DROP TABLE modlog_combined; + diff --git a/migrations/2024-12-08-165614_add_modlog_combined_table/up.sql b/migrations/2024-12-08-165614_add_modlog_combined_table/up.sql new file mode 100644 index 0000000000..cb9e95c8b4 --- /dev/null +++ b/migrations/2024-12-08-165614_add_modlog_combined_table/up.sql @@ -0,0 +1,168 @@ +-- Creates combined tables for +-- modlog: (17 tables) +-- admin_allow_instance +-- admin_block_instance +-- admin_purge_comment +-- admin_purge_community +-- admin_purge_person +-- admin_purge_post +-- mod_add +-- mod_add_community +-- mod_ban +-- mod_ban_from_community +-- mod_feature_post +-- mod_hide_community +-- mod_lock_post +-- mod_remove_comment +-- mod_remove_community +-- mod_remove_post +-- mod_transfer_community +CREATE TABLE modlog_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + admin_allow_instance_id int UNIQUE REFERENCES admin_allow_instance ON UPDATE CASCADE ON DELETE CASCADE, + admin_block_instance_id int UNIQUE REFERENCES admin_block_instance ON UPDATE CASCADE ON DELETE CASCADE, + admin_purge_comment_id int UNIQUE REFERENCES admin_purge_comment ON UPDATE CASCADE ON DELETE CASCADE, + admin_purge_community_id int UNIQUE REFERENCES admin_purge_community ON UPDATE CASCADE ON DELETE CASCADE, + admin_purge_person_id int UNIQUE REFERENCES admin_purge_person ON UPDATE CASCADE ON DELETE CASCADE, + admin_purge_post_id int UNIQUE REFERENCES admin_purge_post ON UPDATE CASCADE ON DELETE CASCADE, + mod_add_id int UNIQUE REFERENCES mod_add ON UPDATE CASCADE ON DELETE CASCADE, + mod_add_community_id int UNIQUE REFERENCES mod_add_community ON UPDATE CASCADE ON DELETE CASCADE, + mod_ban_id int UNIQUE REFERENCES mod_ban ON UPDATE CASCADE ON DELETE CASCADE, + mod_ban_from_community_id int UNIQUE REFERENCES mod_ban_from_community ON UPDATE CASCADE ON DELETE CASCADE, + mod_feature_post_id int UNIQUE REFERENCES mod_feature_post ON UPDATE CASCADE ON DELETE CASCADE, + mod_hide_community_id int UNIQUE REFERENCES mod_hide_community ON UPDATE CASCADE ON DELETE CASCADE, + mod_lock_post_id int UNIQUE REFERENCES mod_lock_post ON UPDATE CASCADE ON DELETE CASCADE, + mod_remove_comment_id int UNIQUE REFERENCES mod_remove_comment ON UPDATE CASCADE ON DELETE CASCADE, + mod_remove_community_id int UNIQUE REFERENCES mod_remove_community ON UPDATE CASCADE ON DELETE CASCADE, + mod_remove_post_id int UNIQUE REFERENCES mod_remove_post ON UPDATE CASCADE ON DELETE CASCADE, + mod_transfer_community_id int UNIQUE REFERENCES mod_transfer_community ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK (num_nonnulls (admin_allow_instance_id, admin_block_instance_id, admin_purge_comment_id, admin_purge_community_id, admin_purge_person_id, admin_purge_post_id, mod_add_id, mod_add_community_id, mod_ban_id, mod_ban_from_community_id, mod_feature_post_id, mod_hide_community_id, mod_lock_post_id, mod_remove_comment_id, mod_remove_community_id, mod_remove_post_id, mod_transfer_community_id) = 1) +); + +CREATE INDEX idx_modlog_combined_published ON modlog_combined (published DESC, id DESC); + +CREATE INDEX idx_modlog_combined_published_asc ON modlog_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +-- Not doing a union all here, because there's way too many null columns +INSERT INTO modlog_combined (published, admin_allow_instance_id) +SELECT + when_, + id +FROM + admin_allow_instance; + +INSERT INTO modlog_combined (published, admin_block_instance_id) +SELECT + when_, + id +FROM + admin_block_instance; + +INSERT INTO modlog_combined (published, admin_purge_comment_id) +SELECT + when_, + id +FROM + admin_purge_comment; + +INSERT INTO modlog_combined (published, admin_purge_community_id) +SELECT + when_, + id +FROM + admin_purge_community; + +INSERT INTO modlog_combined (published, admin_purge_person_id) +SELECT + when_, + id +FROM + admin_purge_person; + +INSERT INTO modlog_combined (published, admin_purge_post_id) +SELECT + when_, + id +FROM + admin_purge_post; + +INSERT INTO modlog_combined (published, mod_add_id) +SELECT + when_, + id +FROM + mod_add; + +INSERT INTO modlog_combined (published, mod_add_community_id) +SELECT + when_, + id +FROM + mod_add_community; + +INSERT INTO modlog_combined (published, mod_ban_id) +SELECT + when_, + id +FROM + mod_ban; + +INSERT INTO modlog_combined (published, mod_ban_from_community_id) +SELECT + when_, + id +FROM + mod_ban_from_community; + +INSERT INTO modlog_combined (published, mod_feature_post_id) +SELECT + when_, + id +FROM + mod_feature_post; + +INSERT INTO modlog_combined (published, mod_hide_community_id) +SELECT + when_, + id +FROM + mod_hide_community; + +INSERT INTO modlog_combined (published, mod_lock_post_id) +SELECT + when_, + id +FROM + mod_lock_post; + +INSERT INTO modlog_combined (published, mod_remove_comment_id) +SELECT + when_, + id +FROM + mod_remove_comment; + +INSERT INTO modlog_combined (published, mod_remove_community_id) +SELECT + when_, + id +FROM + mod_remove_community; + +INSERT INTO modlog_combined (published, mod_remove_post_id) +SELECT + when_, + id +FROM + mod_remove_post; + +INSERT INTO modlog_combined (published, mod_transfer_community_id) +SELECT + when_, + id +FROM + mod_transfer_community; + diff --git a/migrations/2024-12-10-193418_add_inbox_combined_table/down.sql b/migrations/2024-12-10-193418_add_inbox_combined_table/down.sql new file mode 100644 index 0000000000..d701d74720 --- /dev/null +++ b/migrations/2024-12-10-193418_add_inbox_combined_table/down.sql @@ -0,0 +1,6 @@ +-- Rename the person_mention table to person_comment_mention +ALTER TABLE person_comment_mention RENAME TO person_mention; + +-- Drop the new tables +DROP TABLE person_post_mention, inbox_combined; + diff --git a/migrations/2024-12-10-193418_add_inbox_combined_table/up.sql b/migrations/2024-12-10-193418_add_inbox_combined_table/up.sql new file mode 100644 index 0000000000..921d19004c --- /dev/null +++ b/migrations/2024-12-10-193418_add_inbox_combined_table/up.sql @@ -0,0 +1,69 @@ +-- Creates combined tables for +-- Inbox: (replies, comment mentions, post mentions, and private_messages) +-- Also add post mentions, since these didn't exist before. +-- Rename the person_mention table to person_comment_mention +ALTER TABLE person_mention RENAME TO person_comment_mention; + +-- Create the new post_mention table +CREATE TABLE person_post_mention ( + id serial PRIMARY KEY, + recipient_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + post_id int REFERENCES post ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + read boolean DEFAULT FALSE NOT NULL, + published timestamptz NOT NULL DEFAULT now(), + UNIQUE (recipient_id, post_id) +); + +CREATE TABLE inbox_combined ( + id serial PRIMARY KEY, + published timestamptz NOT NULL, + comment_reply_id int UNIQUE REFERENCES comment_reply ON UPDATE CASCADE ON DELETE CASCADE, + person_comment_mention_id int UNIQUE REFERENCES person_comment_mention ON UPDATE CASCADE ON DELETE CASCADE, + person_post_mention_id int UNIQUE REFERENCES person_post_mention ON UPDATE CASCADE ON DELETE CASCADE, + private_message_id int UNIQUE REFERENCES private_message ON UPDATE CASCADE ON DELETE CASCADE, + -- Make sure only one of the columns is not null + CHECK (num_nonnulls (comment_reply_id, person_comment_mention_id, person_post_mention_id, private_message_id) = 1) +); + +CREATE INDEX idx_inbox_combined_published ON inbox_combined (published DESC, id DESC); + +CREATE INDEX idx_inbox_combined_published_asc ON inbox_combined (reverse_timestamp_sort (published) DESC, id DESC); + +-- Updating the history +INSERT INTO inbox_combined (published, comment_reply_id, person_comment_mention_id, person_post_mention_id, private_message_id) +SELECT + published, + id, + NULL::int, + NULL::int, + NULL::int +FROM + comment_reply +UNION ALL +SELECT + published, + NULL::int, + id, + NULL::int, + NULL::int +FROM + person_comment_mention +UNION ALL +SELECT + published, + NULL::int, + NULL::int, + id, + NULL::int +FROM + person_post_mention +UNION ALL +SELECT + published, + NULL::int, + NULL::int, + NULL::int, + id +FROM + private_message; + diff --git a/src/api_routes_v3.rs b/src/api_routes_v3.rs index eefaf5b871..d9e225c25b 100644 --- a/src/api_routes_v3.rs +++ b/src/api_routes_v3.rs @@ -6,11 +6,6 @@ use lemmy_api::{ list_comment_likes::list_comment_likes, save::save_comment, }, - comment_report::{ - create::create_comment_report, - list::list_comment_reports, - resolve::resolve_comment_report, - }, community::{ add_mod::add_mod_to_community, ban::ban_from_community, @@ -33,10 +28,9 @@ use lemmy_api::{ login::login, logout::logout, notifications::{ - list_mentions::list_mentions, - list_replies::list_replies, mark_all_read::mark_all_notifications_read, - mark_mention_read::mark_person_mention_as_read, + mark_comment_mention_read::mark_comment_mention_as_read, + mark_post_mention_read::mark_post_mention_as_read, mark_reply_read::mark_reply_as_read, unread_count::unread_count, }, @@ -58,16 +52,11 @@ use lemmy_api::{ mark_read::mark_post_as_read, save::save_post, }, - post_report::{ - create::create_post_report, - list::list_post_reports, - resolve::resolve_post_report, - }, private_message::mark_read::mark_pm_as_read, - private_message_report::{ - create::create_pm_report, - list::list_pm_reports, - resolve::resolve_pm_report, + reports::{ + comment_report::{create::create_comment_report, resolve::resolve_comment_report}, + post_report::{create::create_post_report, resolve::resolve_post_report}, + private_message_report::{create::create_pm_report, resolve::resolve_pm_report}, }, site::{ federated_instances::get_federated_instances, @@ -119,7 +108,6 @@ use lemmy_api_crud::{ private_message::{ create::create_private_message, delete::delete_private_message, - read::get_private_message, update::update_private_message, }, site::{create::create_site, read::get_site_v3, update::update_site}, @@ -222,7 +210,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_post)) .route("/report", post().to(create_post_report)) .route("/report/resolve", put().to(resolve_post_report)) - .route("/report/list", get().to(list_post_reports)) .route("/site_metadata", get().to(get_link_metadata)), ) // Comment @@ -247,21 +234,18 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_comment)) .route("/list", get().to(list_comments)) .route("/report", post().to(create_comment_report)) - .route("/report/resolve", put().to(resolve_comment_report)) - .route("/report/list", get().to(list_comment_reports)), + .route("/report/resolve", put().to(resolve_comment_report)), ) // Private Message .service( scope("/private_message") .wrap(rate_limit.message()) - .route("/list", get().to(get_private_message)) .route("", post().to(create_private_message)) .route("", put().to(update_private_message)) .route("/delete", post().to(delete_private_message)) .route("/mark_as_read", post().to(mark_pm_as_read)) .route("/report", post().to(create_pm_report)) - .route("/report/resolve", put().to(resolve_pm_report)) - .route("/report/list", get().to(list_pm_reports)), + .route("/report/resolve", put().to(resolve_pm_report)), ) // User .service( @@ -315,12 +299,14 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { scope("/user") .wrap(rate_limit.message()) .route("", get().to(read_person)) - .route("/mention", get().to(list_mentions)) .route( - "/mention/mark_as_read", - post().to(mark_person_mention_as_read), + "/mention/comment/mark_as_read", + post().to(mark_comment_mention_as_read), + ) + .route( + "/mention/post/mark_as_read", + post().to(mark_post_mention_as_read), ) - .route("/replies", get().to(list_replies)) // Admin action. I don't like that it's in /user .route("/ban", post().to(ban_from_site)) .route("/banned", get().to(list_banned_users)) diff --git a/src/api_routes_v4.rs b/src/api_routes_v4.rs index a9f71c9da9..6045521920 100644 --- a/src/api_routes_v4.rs +++ b/src/api_routes_v4.rs @@ -6,11 +6,6 @@ use lemmy_api::{ list_comment_likes::list_comment_likes, save::save_comment, }, - comment_report::{ - create::create_comment_report, - list::list_comment_reports, - resolve::resolve_comment_report, - }, community::{ add_mod::add_mod_to_community, ban::ban_from_community, @@ -36,13 +31,14 @@ use lemmy_api::{ list_banned::list_banned_users, list_logins::list_logins, list_media::list_media, + list_saved::list_person_saved, login::login, logout::logout, notifications::{ - list_mentions::list_mentions, - list_replies::list_replies, + list_inbox::list_inbox, mark_all_read::mark_all_notifications_read, - mark_mention_read::mark_person_mention_as_read, + mark_comment_mention_read::mark_comment_mention_as_read, + mark_post_mention_read::mark_post_mention_as_read, mark_reply_read::mark_reply_as_read, unread_count::unread_count, }, @@ -65,16 +61,12 @@ use lemmy_api::{ mark_read::mark_post_as_read, save::save_post, }, - post_report::{ - create::create_post_report, - list::list_post_reports, - resolve::resolve_post_report, - }, private_message::mark_read::mark_pm_as_read, - private_message_report::{ - create::create_pm_report, - list::list_pm_reports, - resolve::resolve_pm_report, + reports::{ + comment_report::{create::create_comment_report, resolve::resolve_comment_report}, + post_report::{create::create_post_report, resolve::resolve_post_report}, + private_message_report::{create::create_pm_report, resolve::resolve_pm_report}, + report_combined::list::list_reports, }, site::{ admin_allow_instance::admin_allow_instance, @@ -134,7 +126,6 @@ use lemmy_api_crud::{ private_message::{ create::create_private_message, delete::delete_private_message, - read::get_private_message, update::update_private_message, }, site::{create::create_site, read::get_site_v4, update::update_site}, @@ -152,6 +143,7 @@ use lemmy_api_crud::{ }; use lemmy_apub::api::{ list_comments::list_comments, + list_person_content::list_person_content, list_posts::list_posts, read_community::get_community, read_person::read_person, @@ -235,7 +227,6 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_post)) .route("/report", post().to(create_post_report)) .route("/report/resolve", put().to(resolve_post_report)) - .route("/report/list", get().to(list_post_reports)) .route("/site_metadata", get().to(get_link_metadata)), ) // Comment @@ -259,20 +250,23 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/save", put().to(save_comment)) .route("/list", get().to(list_comments)) .route("/report", post().to(create_comment_report)) - .route("/report/resolve", put().to(resolve_comment_report)) - .route("/report/list", get().to(list_comment_reports)), + .route("/report/resolve", put().to(resolve_comment_report)), ) // Private Message .service( scope("/private_message") - .route("/list", get().to(get_private_message)) .route("", post().to(create_private_message)) .route("", put().to(update_private_message)) .route("/delete", post().to(delete_private_message)) .route("/mark_as_read", post().to(mark_pm_as_read)) .route("/report", post().to(create_pm_report)) - .route("/report/resolve", put().to(resolve_pm_report)) - .route("/report/list", get().to(list_pm_reports)), + .route("/report/resolve", put().to(resolve_pm_report)), + ) + // Reports + .service( + scope("/report") + .wrap(rate_limit.message()) + .route("/list", get().to(list_reports)), ) // User .service( @@ -288,7 +282,8 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { .route("/change_password", put().to(change_password)) .route("/totp/generate", post().to(generate_totp_secret)) .route("/totp/update", post().to(update_totp)) - .route("/verify_email", post().to(verify_email)), + .route("/verify_email", post().to(verify_email)) + .route("/saved", get().to(list_person_saved)), ) .route("/account/settings/save", put().to(save_user_settings)) .service( @@ -301,12 +296,15 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { scope("/account") .route("", get().to(get_my_user)) .route("/list_media", get().to(list_media)) - .route("/mention", get().to(list_mentions)) - .route("/replies", get().to(list_replies)) + .route("/inbox", get().to(list_inbox)) .route("/delete", post().to(delete_account)) .route( - "/mention/mark_as_read", - post().to(mark_person_mention_as_read), + "/mention/comment/mark_as_read", + post().to(mark_comment_mention_as_read), + ) + .route( + "/mention/post/mark_as_read", + post().to(mark_post_mention_as_read), ) .route( "/mention/mark_as_read/all", @@ -324,7 +322,11 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { ), ) // User actions - .route("/person", get().to(read_person)) + .service( + scope("/person") + .route("", get().to(read_person)) + .route("/content", get().to(list_person_content)), + ) // Admin Actions .service( scope("/admin")