diff --git a/api_tests/package.json b/api_tests/package.json index 9e7042f2d..00fe252ae 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -27,7 +27,7 @@ "eslint": "^8.57.0", "eslint-plugin-prettier": "^5.0.1", "jest": "^29.5.0", - "lemmy-js-client": "0.19.4-alpha.6", + "lemmy-js-client": "0.19.4-alpha.7", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.3.3" diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 213111ab1..fbd2ed3dd 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -30,8 +30,8 @@ devDependencies: specifier: ^29.5.0 version: 29.7.0(@types/node@20.11.22) lemmy-js-client: - specifier: 0.19.4-alpha.6 - version: 0.19.4-alpha.6 + specifier: 0.19.4-alpha.7 + version: 0.19.4-alpha.7 prettier: specifier: ^3.2.5 version: 3.2.5 @@ -2390,8 +2390,8 @@ packages: engines: {node: '>=6'} dev: true - /lemmy-js-client@0.19.4-alpha.6: - resolution: {integrity: sha512-x4htMlpoZ7hzrhrIk82aompVxbpu2ZDWtmWNGraM0+27nUCDf6gYxJH5nb5R/o39BQe5KSHq6zoBdliBwAY40w==} + /lemmy-js-client@0.19.4-alpha.7: + resolution: {integrity: sha512-1xvSDlhJmU3IzhT2+pvqPWKHo0P/aYTlpObL3hLy1RgaZLapvn3W7XC48cOydas+MAm2WBFsiFX9bi5X+5FWFA==} dependencies: cross-fetch: 4.0.0 form-data: 4.0.0 diff --git a/api_tests/src/image.spec.ts b/api_tests/src/image.spec.ts index 6414a8913..65b60326a 100644 --- a/api_tests/src/image.spec.ts +++ b/api_tests/src/image.spec.ts @@ -48,6 +48,15 @@ test("Upload image and delete it", async () => { const content = downloadFileSync(upload.url); 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); + + // 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 const delete_form: DeleteImage = { token: upload.files![0].delete_token, diff --git a/crates/api/src/local_user/list_media.rs b/crates/api/src/local_user/list_media.rs new file mode 100644 index 000000000..446ce00a5 --- /dev/null +++ b/crates/api/src/local_user/list_media.rs @@ -0,0 +1,28 @@ +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; + +/// 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_media( + data: Query, + context: Data, + local_user_view: LocalUserView, +) -> Result, 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 })) +} diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index 8bf2e5327..c00a4516e 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -10,6 +10,7 @@ pub mod generate_totp_secret; pub mod get_captcha; pub mod list_banned; pub mod list_logins; +pub mod list_media; pub mod login; pub mod logout; pub mod notifications; diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 130d04552..658846eab 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -32,7 +32,7 @@ pub async fn purge_person( // 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 { 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 { delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, &context) diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs index c5011d2f9..f46bda1cc 100644 --- a/crates/api_common/src/person.rs +++ b/crates/api_common/src/person.rs @@ -1,7 +1,7 @@ use crate::sensitive::Sensitive; use lemmy_db_schema::{ newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, - source::site::Site, + source::{images::LocalImage, site::Site}, CommentSortType, ListingType, PostListingMode, @@ -418,3 +418,20 @@ pub struct UpdateTotp { pub struct UpdateTotpResponse { 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, + pub limit: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct ListMediaResponse { + pub images: Vec, +} diff --git a/crates/db_schema/src/impls/images.rs b/crates/db_schema/src/impls/images.rs index a5982bd98..f34a595f2 100644 --- a/crates/db_schema/src/impls/images.rs +++ b/crates/db_schema/src/impls/images.rs @@ -1,11 +1,8 @@ use crate::{ newtypes::{DbUrl, LocalUserId}, - schema::{ - local_image::dsl::{local_image, local_user_id, pictrs_alias}, - remote_image::dsl::{link, remote_image}, - }, + schema::{local_image, remote_image}, source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm}, - utils::{get_conn, DbPool}, + utils::{get_conn, limit_and_offset, DbPool}, }; use diesel::{ dsl::exists, @@ -15,7 +12,6 @@ use diesel::{ ExpressionMethods, NotFound, QueryDsl, - Table, }; use diesel_async::RunQueryDsl; use url::Url; @@ -23,27 +19,47 @@ use url::Url; impl LocalImage { pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result { let conn = &mut get_conn(pool).await?; - insert_into(local_image) + insert_into(local_image::table) .values(form) .get_result::(conn) .await } + /// This should only be used in the internal API, since it has no page and limit pub async fn get_all_by_local_user_id( pool: &mut DbPool<'_>, - user_id: &LocalUserId, + user_id: LocalUserId, ) -> Result, Error> { let conn = &mut get_conn(pool).await?; - local_image - .filter(local_user_id.eq(user_id)) - .select(local_image::all_columns()) + local_image::table + .filter(local_image::local_user_id.eq(user_id)) + .select(local_image::all_columns) + .load::(conn) + .await + } + + /// This is okay for API use. + pub async fn get_all_paged_by_local_user_id( + pool: &mut DbPool<'_>, + user_id: LocalUserId, + page: Option, + limit: Option, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let (limit, offset) = limit_and_offset(page, limit)?; + + local_image::table + .filter(local_image::local_user_id.eq(user_id)) + .select(local_image::all_columns) + .limit(limit) + .offset(offset) .load::(conn) .await } pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result { 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) .await } @@ -56,7 +72,7 @@ impl RemoteImage { .into_iter() .map(|url| RemoteImageForm { link: url.into() }) .collect::>(); - insert_into(remote_image) + insert_into(remote_image::table) .values(forms) .on_conflict_do_nothing() .execute(conn) @@ -66,9 +82,11 @@ impl RemoteImage { pub async fn validate(pool: &mut DbPool<'_>, link_: DbUrl) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; - let exists = select(exists(remote_image.filter((link).eq(link_)))) - .get_result::(conn) - .await?; + let exists = select(exists( + remote_image::table.filter(remote_image::link.eq(link_)), + )) + .get_result::(conn) + .await?; if exists { Ok(()) } else { diff --git a/crates/db_schema/src/source/images.rs b/crates/db_schema/src/source/images.rs index f8befb856..39d7b1996 100644 --- a/crates/db_schema/src/source/images.rs +++ b/crates/db_schema/src/source/images.rs @@ -2,18 +2,26 @@ use crate::newtypes::{DbUrl, LocalUserId}; #[cfg(feature = "full")] use crate::schema::{local_image, remote_image}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use std::fmt::Debug; +use ts_rs::TS; use typed_builder::TypedBuilder; #[skip_serializing_none] -#[derive(PartialEq, Eq, Debug, Clone)] -#[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(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 local_user_id: LocalUserId, pub pictrs_alias: String, diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 966862fa5..c67f21c44 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -29,6 +29,7 @@ use lemmy_api::{ get_captcha::get_captcha, list_banned::list_banned_users, list_logins::list_logins, + list_media::list_media, login::login, logout::logout, notifications::{ @@ -320,7 +321,8 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { .route("/totp/generate", web::post().to(generate_totp_secret)) .route("/totp/update", web::post().to(update_totp)) .route("/list_logins", web::get().to(list_logins)) - .route("/validate_auth", web::get().to(validate_auth)), + .route("/validate_auth", web::get().to(validate_auth)) + .route("/list_media", web::get().to(list_media)), ) // Admin Actions .service(