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

- Fixes #4445
delete-old-avatar
Dessalines 2024-03-05 13:00:10 -05:00
parent 6778279bb6
commit 3c51eaeb88
10 changed files with 109 additions and 26 deletions

View File

@ -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"

View File

@ -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

View File

@ -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,

View File

@ -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<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 list_banned;
pub mod list_logins;
pub mod list_media;
pub mod login;
pub mod logout;
pub mod notifications;

View File

@ -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)

View File

@ -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<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::{
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<Self, Error> {
let conn = &mut get_conn(pool).await?;
insert_into(local_image)
insert_into(local_image::table)
.values(form)
.get_result::<Self>(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<Vec<Self>, 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::<LocalImage>(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<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, 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::<LocalImage>(conn)
.await
}
pub async fn delete_by_alias(pool: &mut DbPool<'_>, alias: &str) -> Result<usize, Error> {
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::<Vec<_>>();
insert_into(remote_image)
insert_into(remote_image::table)
.values(forms)
.on_conflict_do_nothing()
.execute(conn)
@ -66,7 +82,9 @@ 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_))))
let exists = select(exists(
remote_image::table.filter(remote_image::link.eq(link_)),
))
.get_result::<bool>(conn)
.await?;
if exists {

View File

@ -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,

View File

@ -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(