Adding listMedia endpoint, to view all your local image uploads. (#4509)

* Adding listMedia endpoint, to view all your local image uploads.

- Fixes #4445

* Fix ts import.

* Forgot to order by published desc

* Adding an endpoint to list all images, for admins only.

* Forgot to add file.

* Add additional test.

* Use better logic for no-limit version.

* Better call sites.

* Adding another test.

* Fix tests.

* Moving list_media to /account action.

* Addressing PR comments.

* Removing pointless comment.

---------

Co-authored-by: SleeplessOne1917 <28871516+SleeplessOne1917@users.noreply.github.com>
This commit is contained in:
Dessalines 2024-03-26 12:06:11 -04:00 committed by GitHub
parent 945064726f
commit 6bfbb9332d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 252 additions and 114 deletions

View file

@ -27,7 +27,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.19.4-alpha.8", "lemmy-js-client": "0.19.4-alpha.13",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.4.2" "typescript": "^5.4.2"

View file

@ -30,8 +30,8 @@ devDependencies:
specifier: ^29.5.0 specifier: ^29.5.0
version: 29.7.0(@types/node@20.11.27) version: 29.7.0(@types/node@20.11.27)
lemmy-js-client: lemmy-js-client:
specifier: 0.19.4-alpha.8 specifier: 0.19.4-alpha.13
version: 0.19.4-alpha.8 version: 0.19.4-alpha.13
prettier: prettier:
specifier: ^3.2.5 specifier: ^3.2.5
version: 3.2.5 version: 3.2.5
@ -1044,10 +1044,6 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: true
/babel-jest@29.7.0(@babel/core@7.23.9): /babel-jest@29.7.0(@babel/core@7.23.9):
resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@ -1261,13 +1257,6 @@ packages:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
dev: true dev: true
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: true
/concat-map@0.0.1: /concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
dev: true dev: true
@ -1295,14 +1284,6 @@ packages:
- ts-node - ts-node
dev: true dev: true
/cross-fetch@4.0.0:
resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
dev: true
/cross-spawn@7.0.3: /cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -1342,11 +1323,6 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: true
/detect-newline@3.1.0: /detect-newline@3.1.0:
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -1646,15 +1622,6 @@ packages:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
dev: true dev: true
/form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: true
/fs.realpath@1.0.0: /fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
dev: true dev: true
@ -2390,13 +2357,8 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true dev: true
/lemmy-js-client@0.19.4-alpha.8: /lemmy-js-client@0.19.4-alpha.13:
resolution: {integrity: sha512-8vjqUYVOhyUTcmG9FvPLjrWziVwNa2/Zi+kSflTrajJsK0V+5DclJ5dhdVMUQ4DEA70gb0OuNMDlipPG2FoS5A==} resolution: {integrity: sha512-ru1dCqPSfOJdsGq7am5J7P7f+/hpyHGhNbCEV/JAZP2U1lGHul32gLpBkilDnStDNdeq52scjKx+3WskRJFGFA==}
dependencies:
cross-fetch: 4.0.0
form-data: 4.0.0
transitivePeerDependencies:
- encoding
dev: true dev: true
/leven@3.1.0: /leven@3.1.0:
@ -2485,18 +2447,6 @@ packages:
picomatch: 2.3.1 picomatch: 2.3.1
dev: true dev: true
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: true
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: true
/mimic-fn@2.1.0: /mimic-fn@2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -2523,18 +2473,6 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true dev: true
/node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: true
/node-int64@0.4.0: /node-int64@0.4.0:
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
dev: true dev: true
@ -2952,10 +2890,6 @@ packages:
is-number: 7.0.0 is-number: 7.0.0
dev: true dev: true
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: true
/ts-api-utils@1.3.0(typescript@5.4.2): /ts-api-utils@1.3.0(typescript@5.4.2):
resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
@ -3067,17 +3001,6 @@ packages:
makeerror: 1.0.12 makeerror: 1.0.12
dev: true dev: true
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: true
/whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: true
/which@2.0.2: /which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}

View file

@ -5,18 +5,18 @@ import {
setupLogins, setupLogins,
resolveBetaCommunity, resolveBetaCommunity,
followCommunity, followCommunity,
unfollowRemotes,
getSite, getSite,
waitUntil, waitUntil,
beta, beta,
betaUrl, betaUrl,
registerUser, registerUser,
unfollows,
} from "./shared"; } from "./shared";
beforeAll(setupLogins); beforeAll(setupLogins);
afterAll(() => { afterAll(() => {
unfollowRemotes(alpha); unfollows();
}); });
test("Follow local community", async () => { test("Follow local community", async () => {

View file

@ -14,25 +14,30 @@ import {
betaUrl, betaUrl,
createCommunity, createCommunity,
createPost, createPost,
deleteAllImages,
delta, delta,
epsilon, epsilon,
gamma, gamma,
getSite, getSite,
imageFetchLimit,
registerUser, registerUser,
resolveBetaCommunity, resolveBetaCommunity,
resolvePost, resolvePost,
setupLogins, setupLogins,
unfollowRemotes, unfollows,
} from "./shared"; } from "./shared";
const downloadFileSync = require("download-file-sync"); const downloadFileSync = require("download-file-sync");
beforeAll(setupLogins); beforeAll(setupLogins);
afterAll(() => { afterAll(() => {
unfollowRemotes(alphaImage); unfollows();
}); });
test("Upload image and delete it", async () => { test("Upload image and delete it", async () => {
// Before running this test, you need to delete all previous images in the DB
await deleteAllImages(alpha);
// Upload test image. We use a simple string buffer as pictrs doesnt require an actual image // Upload test image. We use a simple string buffer as pictrs doesnt require an actual image
// in testing mode. // in testing mode.
const upload_form: UploadImage = { const upload_form: UploadImage = {
@ -48,6 +53,24 @@ test("Upload image and delete it", async () => {
const content = downloadFileSync(upload.url); const content = downloadFileSync(upload.url);
expect(content.length).toBeGreaterThan(0); expect(content.length).toBeGreaterThan(0);
// Ensure that it comes back with the list_media endpoint
const listMediaRes = await alphaImage.listMedia();
expect(listMediaRes.images.length).toBe(1);
// Ensure that it also comes back with the admin all images
const listAllMediaRes = await alphaImage.listAllMedia({
limit: imageFetchLimit,
});
// This number comes from all the previous thumbnails fetched in other tests.
const previousThumbnails = 1;
expect(listAllMediaRes.images.length).toBe(previousThumbnails);
// The deleteUrl is a combination of the endpoint, delete token, and alias
let firstImage = listMediaRes.images[0];
let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.pictrs_delete_token}/${firstImage.pictrs_alias}`;
expect(deleteUrl).toBe(upload.delete_url);
// delete image // delete image
const delete_form: DeleteImage = { const delete_form: DeleteImage = {
token: upload.files![0].delete_token, token: upload.files![0].delete_token,
@ -59,6 +82,16 @@ test("Upload image and delete it", async () => {
// ensure that image is deleted // ensure that image is deleted
const content2 = downloadFileSync(upload.url); const content2 = downloadFileSync(upload.url);
expect(content2).toBe(""); expect(content2).toBe("");
// Ensure that it shows the image is deleted
const deletedListMediaRes = await alphaImage.listMedia();
expect(deletedListMediaRes.images.length).toBe(0);
// Ensure that the admin shows its deleted
const deletedListAllMediaRes = await alphaImage.listAllMedia({
limit: imageFetchLimit,
});
expect(deletedListAllMediaRes.images.length).toBe(previousThumbnails - 1);
}); });
test("Purge user, uploaded image removed", async () => { test("Purge user, uploaded image removed", async () => {
@ -80,10 +113,10 @@ test("Purge user, uploaded image removed", async () => {
// purge user // purge user
let site = await getSite(user); let site = await getSite(user);
const purge_form: PurgePerson = { const purgeForm: PurgePerson = {
person_id: site.my_user!.local_user_view.person.id, person_id: site.my_user!.local_user_view.person.id,
}; };
const delete_ = await alphaImage.purgePerson(purge_form); const delete_ = await alphaImage.purgePerson(purgeForm);
expect(delete_.success).toBe(true); expect(delete_.success).toBe(true);
// ensure that image is deleted // ensure that image is deleted
@ -117,10 +150,11 @@ test("Purge post, linked image removed", async () => {
expect(post.post_view.post.url).toBe(upload.url); expect(post.post_view.post.url).toBe(upload.url);
// purge post // purge post
const purge_form: PurgePost = {
const purgeForm: PurgePost = {
post_id: post.post_view.post.id, post_id: post.post_view.post.id,
}; };
const delete_ = await beta.purgePost(purge_form); const delete_ = await beta.purgePost(purgeForm);
expect(delete_.success).toBe(true); expect(delete_.success).toBe(true);
// ensure that image is deleted // ensure that image is deleted

View file

@ -8,9 +8,9 @@ import {
editPrivateMessage, editPrivateMessage,
listPrivateMessages, listPrivateMessages,
deletePrivateMessage, deletePrivateMessage,
unfollowRemotes,
waitUntil, waitUntil,
reportPrivateMessage, reportPrivateMessage,
unfollows,
} from "./shared"; } from "./shared";
let recipient_id: number; let recipient_id: number;
@ -22,7 +22,7 @@ beforeAll(async () => {
}); });
afterAll(() => { afterAll(() => {
unfollowRemotes(alpha); unfollows();
}); });
test("Create a private message", async () => { test("Create a private message", async () => {

View file

@ -5,6 +5,7 @@ import {
BlockInstanceResponse, BlockInstanceResponse,
CommunityId, CommunityId,
CreatePrivateMessageReport, CreatePrivateMessageReport,
DeleteImage,
EditCommunity, EditCommunity,
GetReplies, GetReplies,
GetRepliesResponse, GetRepliesResponse,
@ -79,6 +80,7 @@ import { GetPersonDetails } from "lemmy-js-client/dist/types/GetPersonDetails";
import { ListingType } from "lemmy-js-client/dist/types/ListingType"; import { ListingType } from "lemmy-js-client/dist/types/ListingType";
export const fetchFunction = fetch; export const fetchFunction = fetch;
export const imageFetchLimit = 50;
export let alphaUrl = "http://127.0.0.1:8541"; export let alphaUrl = "http://127.0.0.1:8541";
export let betaUrl = "http://127.0.0.1:8551"; export let betaUrl = "http://127.0.0.1:8551";
@ -865,9 +867,25 @@ export function randomString(length: number): string {
return result; return result;
} }
export async function deleteAllImages(api: LemmyHttp) {
const imagesRes = await api.listAllMedia({
limit: imageFetchLimit,
});
imagesRes.images;
for (const image of imagesRes.images) {
const form: DeleteImage = {
token: image.pictrs_delete_token,
filename: image.pictrs_alias,
};
await api.deleteImage(form);
}
}
export async function unfollows() { export async function unfollows() {
await Promise.all([ await Promise.all([
unfollowRemotes(alpha), unfollowRemotes(alpha),
unfollowRemotes(beta),
unfollowRemotes(gamma), unfollowRemotes(gamma),
unfollowRemotes(delta), unfollowRemotes(delta),
unfollowRemotes(epsilon), unfollowRemotes(epsilon),

View file

@ -0,0 +1,26 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
person::{ListMedia, ListMediaResponse},
};
use lemmy_db_schema::source::images::LocalImage;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))]
pub async fn list_media(
data: Query<ListMedia>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<ListMediaResponse>, LemmyError> {
let page = data.page;
let limit = data.limit;
let images = LocalImage::get_all_paged_by_local_user_id(
&mut context.pool(),
local_user_view.local_user.id,
page,
limit,
)
.await?;
Ok(Json(ListMediaResponse { images }))
}

View file

@ -10,6 +10,7 @@ pub mod generate_totp_secret;
pub mod get_captcha; pub mod get_captcha;
pub mod list_banned; pub mod list_banned;
pub mod list_logins; pub mod list_logins;
pub mod list_media;
pub mod login; pub mod login;
pub mod logout; pub mod logout;
pub mod notifications; pub mod notifications;

View file

@ -0,0 +1,24 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
person::{ListMedia, ListMediaResponse},
utils::is_admin,
};
use lemmy_db_schema::source::images::LocalImage;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))]
pub async fn list_all_media(
data: Query<ListMedia>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<ListMediaResponse>, LemmyError> {
// Only let admins view all media
is_admin(&local_user_view)?;
let page = data.page;
let limit = data.limit;
let images = LocalImage::get_all(&mut context.pool(), page, limit).await?;
Ok(Json(ListMediaResponse { images }))
}

View file

@ -1,6 +1,7 @@
pub mod block; pub mod block;
pub mod federated_instances; pub mod federated_instances;
pub mod leave_admin; pub mod leave_admin;
pub mod list_all_media;
pub mod mod_log; pub mod mod_log;
pub mod purge; pub mod purge;
pub mod registration_applications; pub mod registration_applications;

View file

@ -32,7 +32,7 @@ pub async fn purge_person(
// Read the local user to get their images, and delete them // Read the local user to get their images, and delete them
if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), data.person_id).await { if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), data.person_id).await {
let pictrs_uploads = let pictrs_uploads =
LocalImage::get_all_by_local_user_id(&mut context.pool(), &local_user.local_user.id).await?; LocalImage::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id).await?;
for upload in pictrs_uploads { for upload in pictrs_uploads {
delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, &context) delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, &context)

View file

@ -1,7 +1,7 @@
use crate::sensitive::Sensitive; use crate::sensitive::Sensitive;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
source::site::Site, source::{images::LocalImage, site::Site},
CommentSortType, CommentSortType,
ListingType, ListingType,
PostListingMode, PostListingMode,
@ -422,3 +422,20 @@ pub struct UpdateTotp {
pub struct UpdateTotpResponse { pub struct UpdateTotpResponse {
pub enabled: bool, pub enabled: bool,
} }
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Get your user's image / media uploads.
pub struct ListMedia {
pub page: Option<i64>,
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ListMediaResponse {
pub images: Vec<LocalImage>,
}

View file

@ -1,11 +1,8 @@
use crate::{ use crate::{
newtypes::{DbUrl, LocalUserId}, newtypes::{DbUrl, LocalUserId},
schema::{ schema::{local_image, remote_image},
local_image::dsl::{local_image, local_user_id, pictrs_alias},
remote_image::dsl::{link, remote_image},
},
source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm}, source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm},
utils::{get_conn, DbPool}, utils::{get_conn, limit_and_offset, DbPool},
}; };
use diesel::{ use diesel::{
dsl::exists, dsl::exists,
@ -15,7 +12,6 @@ use diesel::{
ExpressionMethods, ExpressionMethods,
NotFound, NotFound,
QueryDsl, QueryDsl,
Table,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use url::Url; use url::Url;
@ -23,27 +19,64 @@ use url::Url;
impl LocalImage { impl LocalImage {
pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result<Self, Error> { pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
insert_into(local_image) insert_into(local_image::table)
.values(form) .values(form)
.get_result::<Self>(conn) .get_result::<Self>(conn)
.await .await
} }
pub async fn get_all_paged_by_local_user_id(
pool: &mut DbPool<'_>,
user_id: LocalUserId,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
Self::get_all_helper(pool, Some(user_id), page, limit, false).await
}
pub async fn get_all_by_local_user_id( pub async fn get_all_by_local_user_id(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
user_id: &LocalUserId, user_id: LocalUserId,
) -> Result<Vec<Self>, Error> {
Self::get_all_helper(pool, Some(user_id), None, None, true).await
}
pub async fn get_all(
pool: &mut DbPool<'_>,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
Self::get_all_helper(pool, None, page, limit, false).await
}
async fn get_all_helper(
pool: &mut DbPool<'_>,
user_id: Option<LocalUserId>,
page: Option<i64>,
limit: Option<i64>,
ignore_page_limits: bool,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
local_image let mut query = local_image::table
.filter(local_user_id.eq(user_id)) .select(local_image::all_columns)
.select(local_image::all_columns()) .order_by(local_image::published.desc())
.load::<LocalImage>(conn) .into_boxed();
.await
if let Some(user_id) = user_id {
query = query.filter(local_image::local_user_id.eq(user_id))
}
if !ignore_page_limits {
let (limit, offset) = limit_and_offset(page, limit)?;
query = query.limit(limit).offset(offset);
}
query.load::<LocalImage>(conn).await
} }
pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<usize, Error> { pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
diesel::delete(local_image.filter(pictrs_alias.eq(alias))) diesel::delete(local_image::table.filter(local_image::pictrs_alias.eq(alias)))
.execute(conn) .execute(conn)
.await .await
} }
@ -56,7 +89,7 @@ impl RemoteImage {
.into_iter() .into_iter()
.map(|url| RemoteImageForm { link: url.into() }) .map(|url| RemoteImageForm { link: url.into() })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
insert_into(remote_image) insert_into(remote_image::table)
.values(forms) .values(forms)
.on_conflict_do_nothing() .on_conflict_do_nothing()
.execute(conn) .execute(conn)
@ -66,9 +99,11 @@ impl RemoteImage {
pub async fn validate(pool: &mut DbPool<'_>, link_: DbUrl) -> Result<(), Error> { pub async fn validate(pool: &mut DbPool<'_>, link_: DbUrl) -> Result<(), Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
let exists = select(exists(remote_image.filter((link).eq(link_)))) let exists = select(exists(
.get_result::<bool>(conn) remote_image::table.filter(remote_image::link.eq(link_)),
.await?; ))
.get_result::<bool>(conn)
.await?;
if exists { if exists {
Ok(()) Ok(())
} else { } else {

View file

@ -2,16 +2,27 @@ use crate::newtypes::{DbUrl, LocalUserId};
#[cfg(feature = "full")] #[cfg(feature = "full")]
use crate::schema::{local_image, remote_image}; use crate::schema::{local_image, remote_image};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use std::fmt::Debug; use std::fmt::Debug;
#[cfg(feature = "full")]
use ts_rs::TS;
use typed_builder::TypedBuilder; use typed_builder::TypedBuilder;
#[derive(PartialEq, Eq, Debug, Clone)] #[skip_serializing_none]
#[cfg_attr(feature = "full", derive(Queryable, Associations))] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Identifiable, Associations, TS)
)]
#[cfg_attr(feature = "full", ts(export))]
#[cfg_attr(feature = "full", diesel(table_name = local_image))] #[cfg_attr(feature = "full", diesel(table_name = local_image))]
#[cfg_attr( #[cfg_attr(
feature = "full", feature = "full",
diesel(belongs_to(crate::source::local_user::LocalUser)) diesel(belongs_to(crate::source::local_user::LocalUser))
)] )]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", diesel(primary_key(local_user_id)))]
pub struct LocalImage { pub struct LocalImage {
pub local_user_id: Option<LocalUserId>, pub local_user_id: Option<LocalUserId>,
pub pictrs_alias: String, pub pictrs_alias: String,

View file

@ -47,6 +47,16 @@ fn queries<'a>() -> Queries<
), ),
); );
let is_local_user_banned_from_community = |person_id| {
exists(
community_person_ban::table.filter(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(person_id)),
),
)
};
let is_saved = |person_id| { let is_saved = |person_id| {
exists( exists(
comment_saved::table.filter( comment_saved::table.filter(
@ -107,6 +117,14 @@ fn queries<'a>() -> Queries<
let all_joins = move |query: comment_reply::BoxedQuery<'a, Pg>, let all_joins = move |query: comment_reply::BoxedQuery<'a, Pg>,
my_person_id: Option<PersonId>| { my_person_id: Option<PersonId>| {
let is_local_user_banned_from_community_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>,
> = if let Some(person_id) = my_person_id {
Box::new(is_local_user_banned_from_community(person_id))
} else {
Box::new(false.into_sql::<sql_types::Bool>())
};
let score_selection: Box< let score_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::SmallInt>>, dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::SmallInt>>,
> = if let Some(person_id) = my_person_id { > = if let Some(person_id) = my_person_id {
@ -153,6 +171,7 @@ fn queries<'a>() -> Queries<
aliases::person1.fields(person::all_columns), aliases::person1.fields(person::all_columns),
comment_aggregates::all_columns, comment_aggregates::all_columns,
is_creator_banned_from_community, is_creator_banned_from_community,
is_local_user_banned_from_community_selection,
creator_is_moderator, creator_is_moderator,
creator_is_admin, creator_is_admin,
subscribed_type_selection, subscribed_type_selection,

View file

@ -47,6 +47,16 @@ fn queries<'a>() -> Queries<
), ),
); );
let is_local_user_banned_from_community = |person_id| {
exists(
community_person_ban::table.filter(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(person_id)),
),
)
};
let is_saved = |person_id| { let is_saved = |person_id| {
exists( exists(
comment_saved::table.filter( comment_saved::table.filter(
@ -107,6 +117,13 @@ fn queries<'a>() -> Queries<
let all_joins = move |query: person_mention::BoxedQuery<'a, Pg>, let all_joins = move |query: person_mention::BoxedQuery<'a, Pg>,
my_person_id: Option<PersonId>| { my_person_id: Option<PersonId>| {
let is_local_user_banned_from_community_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>,
> = if let Some(person_id) = my_person_id {
Box::new(is_local_user_banned_from_community(person_id))
} else {
Box::new(false.into_sql::<sql_types::Bool>())
};
let score_selection: Box< let score_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::SmallInt>>, dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::SmallInt>>,
> = if let Some(person_id) = my_person_id { > = if let Some(person_id) = my_person_id {
@ -153,6 +170,7 @@ fn queries<'a>() -> Queries<
aliases::person1.fields(person::all_columns), aliases::person1.fields(person::all_columns),
comment_aggregates::all_columns, comment_aggregates::all_columns,
is_creator_banned_from_community, is_creator_banned_from_community,
is_local_user_banned_from_community_selection,
creator_is_moderator, creator_is_moderator,
creator_is_admin, creator_is_admin,
subscribed_type_selection, subscribed_type_selection,

View file

@ -108,6 +108,7 @@ pub struct PersonMentionView {
pub recipient: Person, pub recipient: Person,
pub counts: CommentAggregates, pub counts: CommentAggregates,
pub creator_banned_from_community: bool, pub creator_banned_from_community: bool,
pub banned_from_community: bool,
pub creator_is_moderator: bool, pub creator_is_moderator: bool,
pub creator_is_admin: bool, pub creator_is_admin: bool,
pub subscribed: SubscribedType, pub subscribed: SubscribedType,
@ -131,6 +132,7 @@ pub struct CommentReplyView {
pub recipient: Person, pub recipient: Person,
pub counts: CommentAggregates, pub counts: CommentAggregates,
pub creator_banned_from_community: bool, pub creator_banned_from_community: bool,
pub banned_from_community: bool,
pub creator_is_moderator: bool, pub creator_is_moderator: bool,
pub creator_is_admin: bool, pub creator_is_admin: bool,
pub subscribed: SubscribedType, pub subscribed: SubscribedType,

View file

@ -29,6 +29,7 @@ use lemmy_api::{
get_captcha::get_captcha, get_captcha::get_captcha,
list_banned::list_banned_users, list_banned::list_banned_users,
list_logins::list_logins, list_logins::list_logins,
list_media::list_media,
login::login, login::login,
logout::logout, logout::logout,
notifications::{ notifications::{
@ -71,6 +72,7 @@ use lemmy_api::{
block::block_instance, block::block_instance,
federated_instances::get_federated_instances, federated_instances::get_federated_instances,
leave_admin::leave_admin, leave_admin::leave_admin,
list_all_media::list_all_media,
mod_log::get_mod_log, mod_log::get_mod_log,
purge::{ purge::{
comment::purge_comment, comment::purge_comment,
@ -282,6 +284,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
.wrap(rate_limit.import_user_settings()) .wrap(rate_limit.import_user_settings())
.route(web::post().to(import_settings)), .route(web::post().to(import_settings)),
) )
// TODO, all the current account related actions under /user need to get moved here eventually
.service(
web::scope("/account")
.wrap(rate_limit.message())
.route("/list_media", web::get().to(list_media)),
)
// User actions // User actions
.service( .service(
web::scope("/user") web::scope("/user")
@ -339,6 +347,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
"/registration_application/approve", "/registration_application/approve",
web::put().to(approve_registration_application), web::put().to(approve_registration_application),
) )
.route("/list_all_media", web::get().to(list_all_media))
.service( .service(
web::scope("/purge") web::scope("/purge")
.route("/person", web::post().to(purge_person)) .route("/person", web::post().to(purge_person))