mirror of
https://github.com/LemmyNet/lemmy.git
synced 2025-01-03 16:51:35 +00:00
parent
cfa866a534
commit
d252be2113
16 changed files with 208 additions and 217 deletions
|
@ -28,7 +28,7 @@
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.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.20.0-api-v4.16",
|
"lemmy-js-client": "0.20.0-image-api-rework.3",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
|
|
|
@ -30,8 +30,8 @@ importers:
|
||||||
specifier: ^29.5.0
|
specifier: ^29.5.0
|
||||||
version: 29.7.0(@types/node@22.9.0)
|
version: 29.7.0(@types/node@22.9.0)
|
||||||
lemmy-js-client:
|
lemmy-js-client:
|
||||||
specifier: 0.20.0-api-v4.16
|
specifier: 0.20.0-image-api-rework.3
|
||||||
version: 0.20.0-api-v4.16
|
version: 0.20.0-image-api-rework.3
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.2.5
|
specifier: ^3.2.5
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
|
@ -1167,8 +1167,8 @@ packages:
|
||||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
lemmy-js-client@0.20.0-api-v4.16:
|
lemmy-js-client@0.20.0-image-api-rework.3:
|
||||||
resolution: {integrity: sha512-9Wn7b8YT2KnEA286+RV1B3mLmecAynvAERoC0ZZiccfSgkEvd3rG9A5X9ejiPqp+JzDZJeisO57+Ut4QHr5oTw==}
|
resolution: {integrity: sha512-SB20z+WD2S821q05OxzI2Lkwq1BWBNWM6Xd1l1bqKL310CRSAG4lln26+j8bjWxMgl/fYTqre8KG6l1YDiV3+Q==}
|
||||||
|
|
||||||
leven@3.1.0:
|
leven@3.1.0:
|
||||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||||
|
@ -3077,7 +3077,7 @@ snapshots:
|
||||||
|
|
||||||
kleur@3.0.3: {}
|
kleur@3.0.3: {}
|
||||||
|
|
||||||
lemmy-js-client@0.20.0-api-v4.16: {}
|
lemmy-js-client@0.20.0-image-api-rework.3: {}
|
||||||
|
|
||||||
leven@3.1.0: {}
|
leven@3.1.0: {}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ killall -s1 lemmy_server || true
|
||||||
popd
|
popd
|
||||||
|
|
||||||
pnpm i
|
pnpm i
|
||||||
pnpm api-test-image || true
|
pnpm api-test || true
|
||||||
|
|
||||||
killall -s1 lemmy_server || true
|
killall -s1 lemmy_server || true
|
||||||
killall -s1 pict-rs || true
|
killall -s1 pict-rs || true
|
||||||
|
|
|
@ -2,9 +2,9 @@ jest.setTimeout(120000);
|
||||||
|
|
||||||
import {
|
import {
|
||||||
UploadImage,
|
UploadImage,
|
||||||
DeleteImage,
|
|
||||||
PurgePerson,
|
PurgePerson,
|
||||||
PurgePost,
|
PurgePost,
|
||||||
|
DeleteImageParams,
|
||||||
} from "lemmy-js-client";
|
} from "lemmy-js-client";
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
|
@ -54,13 +54,12 @@ test("Upload image and delete it", async () => {
|
||||||
image: Buffer.from("test"),
|
image: Buffer.from("test"),
|
||||||
};
|
};
|
||||||
const upload = await alphaImage.uploadImage(upload_form);
|
const upload = await alphaImage.uploadImage(upload_form);
|
||||||
expect(upload.files![0].file).toBeDefined();
|
expect(upload.image_url).toBeDefined();
|
||||||
expect(upload.files![0].delete_token).toBeDefined();
|
expect(upload.filename).toBeDefined();
|
||||||
expect(upload.url).toBeDefined();
|
expect(upload.delete_token).toBeDefined();
|
||||||
expect(upload.delete_url).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
// ensure that image download is working. theres probably a better way to do this
|
||||||
const response = await fetch(upload.url ?? "");
|
const response = await fetch(upload.image_url ?? "");
|
||||||
const content = await response.text();
|
const content = await response.text();
|
||||||
expect(content.length).toBeGreaterThan(0);
|
expect(content.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
@ -77,26 +76,21 @@ test("Upload image and delete it", async () => {
|
||||||
const previousThumbnails = 1;
|
const previousThumbnails = 1;
|
||||||
expect(listAllMediaRes.images.length).toBe(previousThumbnails);
|
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.local_image.pictrs_delete_token}/${firstImage.local_image.pictrs_alias}`;
|
|
||||||
expect(deleteUrl).toBe(upload.delete_url);
|
|
||||||
|
|
||||||
// Make sure the uploader is correct
|
// Make sure the uploader is correct
|
||||||
expect(firstImage.person.actor_id).toBe(
|
expect(listMediaRes.images[0].person.actor_id).toBe(
|
||||||
`http://lemmy-alpha:8541/u/lemmy_alpha`,
|
`http://lemmy-alpha:8541/u/lemmy_alpha`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// delete image
|
// delete image
|
||||||
const delete_form: DeleteImage = {
|
const delete_form: DeleteImageParams = {
|
||||||
token: upload.files![0].delete_token,
|
token: upload.delete_token,
|
||||||
filename: upload.files![0].file,
|
filename: upload.filename,
|
||||||
};
|
};
|
||||||
const delete_ = await alphaImage.deleteImage(delete_form);
|
const delete_ = await alphaImage.deleteImage(delete_form);
|
||||||
expect(delete_).toBe(true);
|
expect(delete_.success).toBe(true);
|
||||||
|
|
||||||
// ensure that image is deleted
|
// ensure that image is deleted
|
||||||
const response2 = await fetch(upload.url ?? "");
|
const response2 = await fetch(upload.image_url ?? "");
|
||||||
const content2 = await response2.text();
|
const content2 = await response2.text();
|
||||||
expect(content2).toBe("");
|
expect(content2).toBe("");
|
||||||
|
|
||||||
|
@ -119,13 +113,12 @@ test("Purge user, uploaded image removed", async () => {
|
||||||
image: Buffer.from("test"),
|
image: Buffer.from("test"),
|
||||||
};
|
};
|
||||||
const upload = await user.uploadImage(upload_form);
|
const upload = await user.uploadImage(upload_form);
|
||||||
expect(upload.files![0].file).toBeDefined();
|
expect(upload.filename).toBeDefined();
|
||||||
expect(upload.files![0].delete_token).toBeDefined();
|
expect(upload.delete_token).toBeDefined();
|
||||||
expect(upload.url).toBeDefined();
|
expect(upload.image_url).toBeDefined();
|
||||||
expect(upload.delete_url).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
// ensure that image download is working. theres probably a better way to do this
|
||||||
const response = await fetch(upload.url ?? "");
|
const response = await fetch(upload.image_url ?? "");
|
||||||
const content = await response.text();
|
const content = await response.text();
|
||||||
expect(content.length).toBeGreaterThan(0);
|
expect(content.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
@ -138,7 +131,7 @@ test("Purge user, uploaded image removed", async () => {
|
||||||
expect(delete_.success).toBe(true);
|
expect(delete_.success).toBe(true);
|
||||||
|
|
||||||
// ensure that image is deleted
|
// ensure that image is deleted
|
||||||
const response2 = await fetch(upload.url ?? "");
|
const response2 = await fetch(upload.image_url ?? "");
|
||||||
const content2 = await response2.text();
|
const content2 = await response2.text();
|
||||||
expect(content2).toBe("");
|
expect(content2).toBe("");
|
||||||
});
|
});
|
||||||
|
@ -151,13 +144,12 @@ test("Purge post, linked image removed", async () => {
|
||||||
image: Buffer.from("test"),
|
image: Buffer.from("test"),
|
||||||
};
|
};
|
||||||
const upload = await user.uploadImage(upload_form);
|
const upload = await user.uploadImage(upload_form);
|
||||||
expect(upload.files![0].file).toBeDefined();
|
expect(upload.filename).toBeDefined();
|
||||||
expect(upload.files![0].delete_token).toBeDefined();
|
expect(upload.delete_token).toBeDefined();
|
||||||
expect(upload.url).toBeDefined();
|
expect(upload.image_url).toBeDefined();
|
||||||
expect(upload.delete_url).toBeDefined();
|
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
// ensure that image download is working. theres probably a better way to do this
|
||||||
const response = await fetch(upload.url ?? "");
|
const response = await fetch(upload.image_url ?? "");
|
||||||
const content = await response.text();
|
const content = await response.text();
|
||||||
expect(content.length).toBeGreaterThan(0);
|
expect(content.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
@ -165,9 +157,9 @@ test("Purge post, linked image removed", async () => {
|
||||||
let post = await createPost(
|
let post = await createPost(
|
||||||
user,
|
user,
|
||||||
community.community!.community.id,
|
community.community!.community.id,
|
||||||
upload.url,
|
upload.image_url,
|
||||||
);
|
);
|
||||||
expect(post.post_view.post.url).toBe(upload.url);
|
expect(post.post_view.post.url).toBe(upload.image_url);
|
||||||
expect(post.post_view.image_details).toBeDefined();
|
expect(post.post_view.image_details).toBeDefined();
|
||||||
|
|
||||||
// purge post
|
// purge post
|
||||||
|
@ -178,7 +170,7 @@ test("Purge post, linked image removed", async () => {
|
||||||
expect(delete_.success).toBe(true);
|
expect(delete_.success).toBe(true);
|
||||||
|
|
||||||
// ensure that image is deleted
|
// ensure that image is deleted
|
||||||
const response2 = await fetch(upload.url ?? "");
|
const response2 = await fetch(upload.image_url ?? "");
|
||||||
const content2 = await response2.text();
|
const content2 = await response2.text();
|
||||||
expect(content2).toBe("");
|
expect(content2).toBe("");
|
||||||
});
|
});
|
||||||
|
@ -200,11 +192,11 @@ test("Images in remote image post are proxied if setting enabled", async () => {
|
||||||
// remote image gets proxied after upload
|
// remote image gets proxied after upload
|
||||||
expect(
|
expect(
|
||||||
post.thumbnail_url?.startsWith(
|
post.thumbnail_url?.startsWith(
|
||||||
"http://lemmy-gamma:8561/api/v4/image_proxy?url",
|
"http://lemmy-gamma:8561/api/v4/image/proxy?url",
|
||||||
),
|
),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
post.body?.startsWith("![](http://lemmy-gamma:8561/api/v4/image_proxy?url"),
|
post.body?.startsWith("![](http://lemmy-gamma:8561/api/v4/image/proxy?url"),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
// Make sure that it ends with jpg, to be sure its an image
|
// Make sure that it ends with jpg, to be sure its an image
|
||||||
|
@ -223,12 +215,12 @@ test("Images in remote image post are proxied if setting enabled", async () => {
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
epsilonPost.thumbnail_url?.startsWith(
|
epsilonPost.thumbnail_url?.startsWith(
|
||||||
"http://lemmy-epsilon:8581/api/v4/image_proxy?url",
|
"http://lemmy-epsilon:8581/api/v4/image/proxy?url",
|
||||||
),
|
),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
epsilonPost.body?.startsWith(
|
epsilonPost.body?.startsWith(
|
||||||
"![](http://lemmy-epsilon:8581/api/v4/image_proxy?url",
|
"![](http://lemmy-epsilon:8581/api/v4/image/proxy?url",
|
||||||
),
|
),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
|
@ -250,7 +242,7 @@ test("Thumbnail of remote image link is proxied if setting enabled", async () =>
|
||||||
// remote image gets proxied after upload
|
// remote image gets proxied after upload
|
||||||
expect(
|
expect(
|
||||||
post.thumbnail_url?.startsWith(
|
post.thumbnail_url?.startsWith(
|
||||||
"http://lemmy-gamma:8561/api/v4/image_proxy?url",
|
"http://lemmy-gamma:8561/api/v4/image/proxy?url",
|
||||||
),
|
),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
|
@ -268,7 +260,7 @@ test("Thumbnail of remote image link is proxied if setting enabled", async () =>
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
epsilonPost.thumbnail_url?.startsWith(
|
epsilonPost.thumbnail_url?.startsWith(
|
||||||
"http://lemmy-epsilon:8581/api/v4/image_proxy?url",
|
"http://lemmy-epsilon:8581/api/v4/image/proxy?url",
|
||||||
),
|
),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
|
@ -292,14 +284,14 @@ test("No image proxying if setting is disabled", async () => {
|
||||||
let post = await createPost(
|
let post = await createPost(
|
||||||
alpha,
|
alpha,
|
||||||
community.community_view.community.id,
|
community.community_view.community.id,
|
||||||
upload.url,
|
upload.image_url,
|
||||||
`![](${sampleImage})`,
|
`![](${sampleImage})`,
|
||||||
);
|
);
|
||||||
expect(post.post_view.post).toBeDefined();
|
expect(post.post_view.post).toBeDefined();
|
||||||
|
|
||||||
// remote image doesn't get proxied after upload
|
// remote image doesn't get proxied after upload
|
||||||
expect(
|
expect(
|
||||||
post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
|
post.post_view.post.url?.startsWith("http://lemmy-beta:8551/api/v4/image/"),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(post.post_view.post.body).toBe(`![](${sampleImage})`);
|
expect(post.post_view.post.body).toBe(`![](${sampleImage})`);
|
||||||
|
|
||||||
|
@ -312,7 +304,7 @@ test("No image proxying if setting is disabled", async () => {
|
||||||
|
|
||||||
// remote image doesn't get proxied after federation
|
// remote image doesn't get proxied after federation
|
||||||
expect(
|
expect(
|
||||||
betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
|
betaPost.post.url?.startsWith("http://lemmy-beta:8551/api/v4/image/"),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(betaPost.post.body).toBe(`![](${sampleImage})`);
|
expect(betaPost.post.body).toBe(`![](${sampleImage})`);
|
||||||
// Make sure the alt text got federated
|
// Make sure the alt text got federated
|
||||||
|
@ -334,7 +326,7 @@ test("Make regular post, and give it a custom thumbnail", async () => {
|
||||||
alphaImage,
|
alphaImage,
|
||||||
community.community_view.community.id,
|
community.community_view.community.id,
|
||||||
wikipediaUrl,
|
wikipediaUrl,
|
||||||
upload1.url!,
|
upload1.image_url!,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Wait for the metadata to get fetched, since this is backgrounded now
|
// Wait for the metadata to get fetched, since this is backgrounded now
|
||||||
|
@ -344,7 +336,7 @@ test("Make regular post, and give it a custom thumbnail", async () => {
|
||||||
);
|
);
|
||||||
expect(post.post_view.post.url).toBe(wikipediaUrl);
|
expect(post.post_view.post.url).toBe(wikipediaUrl);
|
||||||
// Make sure it uses custom thumbnail
|
// Make sure it uses custom thumbnail
|
||||||
expect(post.post_view.post.thumbnail_url).toBe(upload1.url);
|
expect(post.post_view.post.thumbnail_url).toBe(upload1.image_url);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Create an image post, and make sure a custom thumbnail doesn't overwrite it", async () => {
|
test("Create an image post, and make sure a custom thumbnail doesn't overwrite it", async () => {
|
||||||
|
@ -363,14 +355,14 @@ test("Create an image post, and make sure a custom thumbnail doesn't overwrite i
|
||||||
let post = await createPostWithThumbnail(
|
let post = await createPostWithThumbnail(
|
||||||
alphaImage,
|
alphaImage,
|
||||||
community.community_view.community.id,
|
community.community_view.community.id,
|
||||||
upload1.url!,
|
upload1.image_url!,
|
||||||
upload2.url!,
|
upload2.image_url!,
|
||||||
);
|
);
|
||||||
post = await waitUntil(
|
post = await waitUntil(
|
||||||
() => getPost(alphaImage, post.post_view.post.id),
|
() => getPost(alphaImage, post.post_view.post.id),
|
||||||
p => p.post_view.post.thumbnail_url != undefined,
|
p => p.post_view.post.thumbnail_url != undefined,
|
||||||
);
|
);
|
||||||
expect(post.post_view.post.url).toBe(upload1.url);
|
expect(post.post_view.post.url).toBe(upload1.image_url);
|
||||||
// Make sure the custom thumbnail is ignored
|
// Make sure the custom thumbnail is ignored
|
||||||
expect(post.post_view.post.thumbnail_url == upload2.url).toBe(false);
|
expect(post.post_view.post.thumbnail_url == upload2.image_url).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
# or
|
# or
|
||||||
|
|
||||||
# If enabled, all images from remote domains are rewritten to pass through
|
# If enabled, all images from remote domains are rewritten to pass through
|
||||||
# `/api/v4/image_proxy`, including embedded images in markdown. Images are stored temporarily
|
# `/api/v4/image/proxy`, including embedded images in markdown. Images are stored temporarily
|
||||||
# in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
|
# in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
|
||||||
# servers, and decreases load on other servers. However it increases bandwidth use for the
|
# servers, and decreases load on other servers. However it increases bandwidth use for the
|
||||||
# local server.
|
# local server.
|
||||||
|
|
42
crates/api_common/src/image.rs
Normal file
42
crates/api_common/src/image.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_with::skip_serializing_none;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
use ts_rs::TS;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct ImageGetParams {
|
||||||
|
pub file_type: Option<String>,
|
||||||
|
pub max_size: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct DeleteImageParams {
|
||||||
|
pub filename: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct ImageProxyParams {
|
||||||
|
pub url: String,
|
||||||
|
pub file_type: Option<String>,
|
||||||
|
pub max_size: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
pub struct UploadImageResponse {
|
||||||
|
pub image_url: Url,
|
||||||
|
pub filename: String,
|
||||||
|
pub delete_token: String,
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ pub mod community;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod custom_emoji;
|
pub mod custom_emoji;
|
||||||
|
pub mod image;
|
||||||
pub mod oauth_provider;
|
pub mod oauth_provider;
|
||||||
pub mod person;
|
pub mod person;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
|
|
|
@ -263,9 +263,15 @@ pub struct PictrsFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PictrsFile {
|
impl PictrsFile {
|
||||||
pub fn thumbnail_url(&self, protocol_and_hostname: &str) -> Result<Url, url::ParseError> {
|
pub fn image_url(&self, protocol_and_hostname: &str) -> Result<Url, url::ParseError> {
|
||||||
Url::parse(&format!(
|
Url::parse(&format!(
|
||||||
"{protocol_and_hostname}/pictrs/image/{}",
|
"{protocol_and_hostname}/api/v4/image/{}",
|
||||||
|
self.file
|
||||||
|
))
|
||||||
|
}
|
||||||
|
pub fn delete_url(&self, protocol_and_hostname: &str) -> Result<Url, url::ParseError> {
|
||||||
|
Url::parse(&format!(
|
||||||
|
"{protocol_and_hostname}/api/v4/image/{}",
|
||||||
self.file
|
self.file
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -402,7 +408,7 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
|
||||||
pictrs_delete_token: image.delete_token.clone(),
|
pictrs_delete_token: image.delete_token.clone(),
|
||||||
};
|
};
|
||||||
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
||||||
let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?;
|
let thumbnail_url = image.image_url(&protocol_and_hostname)?;
|
||||||
|
|
||||||
// Also store the details for the image
|
// Also store the details for the image
|
||||||
let details_form = image.details.build_image_details_form(&thumbnail_url);
|
let details_form = image.details.build_image_details_form(&thumbnail_url);
|
||||||
|
|
|
@ -1177,7 +1177,7 @@ fn build_proxied_image_url(
|
||||||
protocol_and_hostname: &str,
|
protocol_and_hostname: &str,
|
||||||
) -> Result<Url, url::ParseError> {
|
) -> Result<Url, url::ParseError> {
|
||||||
Url::parse(&format!(
|
Url::parse(&format!(
|
||||||
"{}/api/v4/image_proxy?url={}",
|
"{}/api/v4/image/proxy?url={}",
|
||||||
protocol_and_hostname,
|
protocol_and_hostname,
|
||||||
encode(link.as_str())
|
encode(link.as_str())
|
||||||
))
|
))
|
||||||
|
@ -1256,7 +1256,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png",
|
"https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png",
|
||||||
proxied.as_str()
|
proxied.as_str()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,17 @@
|
||||||
use actix_web::{
|
use actix_web::{body::BoxBody, web::*, HttpRequest, HttpResponse, Responder};
|
||||||
body::{BodyStream, BoxBody},
|
use lemmy_api_common::{
|
||||||
http::StatusCode,
|
context::LemmyContext,
|
||||||
web::*,
|
image::{DeleteImageParams, ImageGetParams, ImageProxyParams, UploadImageResponse},
|
||||||
HttpRequest,
|
SuccessResponse,
|
||||||
HttpResponse,
|
|
||||||
Responder,
|
|
||||||
};
|
};
|
||||||
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
|
|
||||||
use lemmy_db_schema::source::{
|
use lemmy_db_schema::source::{
|
||||||
images::{LocalImage, RemoteImage},
|
images::{LocalImage, RemoteImage},
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
};
|
};
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
use lemmy_utils::error::LemmyResult;
|
use lemmy_utils::error::LemmyResult;
|
||||||
use serde::Deserialize;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use utils::{
|
use utils::{do_get_image, do_upload_image, file_type, UploadType, PICTRS_CLIENT};
|
||||||
adapt_request,
|
|
||||||
convert_header,
|
|
||||||
do_upload_image,
|
|
||||||
PictrsGetParams,
|
|
||||||
ProcessUrl,
|
|
||||||
UploadType,
|
|
||||||
PICTRS_CLIENT,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub mod person;
|
pub mod person;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
@ -31,18 +19,22 @@ mod utils;
|
||||||
pub async fn upload_image(
|
pub async fn upload_image(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
body: Payload,
|
body: Payload,
|
||||||
// require login
|
|
||||||
local_user_view: LocalUserView,
|
local_user_view: LocalUserView,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
) -> LemmyResult<HttpResponse> {
|
) -> LemmyResult<Json<UploadImageResponse>> {
|
||||||
let image = do_upload_image(req, body, UploadType::Other, &local_user_view, &context).await?;
|
let image = do_upload_image(req, body, UploadType::Other, &local_user_view, &context).await?;
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().json(image))
|
let image_url = image.image_url(&context.settings().get_protocol_and_hostname())?;
|
||||||
|
Ok(Json(UploadImageResponse {
|
||||||
|
image_url,
|
||||||
|
filename: image.file,
|
||||||
|
delete_token: image.delete_token,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_full_res_image(
|
pub async fn get_image(
|
||||||
filename: Path<String>,
|
filename: Path<String>,
|
||||||
Query(params): Query<PictrsGetParams>,
|
Query(params): Query<ImageGetParams>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
local_user_view: Option<LocalUserView>,
|
local_user_view: Option<LocalUserView>,
|
||||||
|
@ -55,43 +47,20 @@ pub async fn get_full_res_image(
|
||||||
let name = &filename.into_inner();
|
let name = &filename.into_inner();
|
||||||
|
|
||||||
// If there are no query params, the URL is original
|
// If there are no query params, the URL is original
|
||||||
let pictrs_config = context.settings().pictrs_config()?;
|
let pictrs_url = context.settings().pictrs_config()?.url;
|
||||||
|
let processed_url = if params.file_type.is_none() && params.max_size.is_none() {
|
||||||
|
format!("{}image/original/{}", pictrs_url, name)
|
||||||
|
} else {
|
||||||
|
let file_type = file_type(params.file_type, name);
|
||||||
|
let mut url = format!("{}image/process.{}?src={}", pictrs_url, file_type, name);
|
||||||
|
|
||||||
let processed_url = params.process_url(name, &pictrs_config.url);
|
if let Some(size) = params.max_size {
|
||||||
|
url = format!("{url}&thumbnail={size}",);
|
||||||
|
}
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
image(processed_url, req).await
|
do_get_image(processed_url, req).await
|
||||||
}
|
|
||||||
|
|
||||||
async fn image(url: String, req: HttpRequest) -> LemmyResult<HttpResponse> {
|
|
||||||
let mut client_req = adapt_request(&req, url);
|
|
||||||
|
|
||||||
if let Some(addr) = req.head().peer_addr {
|
|
||||||
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(addr) = req.head().peer_addr {
|
|
||||||
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = client_req.send().await?;
|
|
||||||
|
|
||||||
if res.status() == http::StatusCode::NOT_FOUND {
|
|
||||||
return Ok(HttpResponse::NotFound().finish());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut client_res = HttpResponse::build(StatusCode::from_u16(res.status().as_u16())?);
|
|
||||||
|
|
||||||
for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
|
|
||||||
client_res.insert_header(convert_header(name, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(client_res.body(BodyStream::new(res.bytes_stream())))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
pub struct DeleteImageParams {
|
|
||||||
file: String,
|
|
||||||
token: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_image(
|
pub async fn delete_image(
|
||||||
|
@ -99,55 +68,27 @@ pub async fn delete_image(
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
// require login
|
// require login
|
||||||
_local_user_view: LocalUserView,
|
_local_user_view: LocalUserView,
|
||||||
) -> LemmyResult<SuccessResponse> {
|
) -> LemmyResult<Json<SuccessResponse>> {
|
||||||
let pictrs_config = context.settings().pictrs_config()?;
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}image/delete/{}/{}",
|
"{}image/delete/{}/{}",
|
||||||
pictrs_config.url, &data.token, &data.file
|
pictrs_config.url, &data.token, &data.filename
|
||||||
);
|
);
|
||||||
|
|
||||||
PICTRS_CLIENT.delete(url).send().await?.error_for_status()?;
|
PICTRS_CLIENT.delete(url).send().await?.error_for_status()?;
|
||||||
|
|
||||||
LocalImage::delete_by_alias(&mut context.pool(), &data.file).await?;
|
LocalImage::delete_by_alias(&mut context.pool(), &data.filename).await?;
|
||||||
|
|
||||||
Ok(SuccessResponse::default())
|
Ok(Json(SuccessResponse::default()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pictrs_healthz(context: Data<LemmyContext>) -> LemmyResult<SuccessResponse> {
|
pub async fn pictrs_health(context: Data<LemmyContext>) -> LemmyResult<Json<SuccessResponse>> {
|
||||||
let pictrs_config = context.settings().pictrs_config()?;
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
let url = format!("{}healthz", pictrs_config.url);
|
let url = format!("{}healthz", pictrs_config.url);
|
||||||
|
|
||||||
PICTRS_CLIENT.get(url).send().await?.error_for_status()?;
|
PICTRS_CLIENT.get(url).send().await?.error_for_status()?;
|
||||||
|
|
||||||
Ok(SuccessResponse::default())
|
Ok(Json(SuccessResponse::default()))
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
pub struct ImageProxyParams {
|
|
||||||
url: String,
|
|
||||||
format: Option<String>,
|
|
||||||
thumbnail: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcessUrl for ImageProxyParams {
|
|
||||||
fn process_url(&self, proxy_url: &str, pictrs_url: &Url) -> String {
|
|
||||||
if self.format.is_none() && self.thumbnail.is_none() {
|
|
||||||
format!("{}image/original?proxy={}", pictrs_url, proxy_url)
|
|
||||||
} else {
|
|
||||||
// Take file type from name, or jpg if nothing is given
|
|
||||||
let format = self
|
|
||||||
.clone()
|
|
||||||
.format
|
|
||||||
.unwrap_or_else(|| proxy_url.split('.').last().unwrap_or("jpg").to_string());
|
|
||||||
|
|
||||||
let mut url = format!("{}image/process.{}?proxy={}", pictrs_url, format, proxy_url);
|
|
||||||
|
|
||||||
if let Some(size) = self.thumbnail {
|
|
||||||
url = format!("{url}&thumbnail={size}",);
|
|
||||||
}
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn image_proxy(
|
pub async fn image_proxy(
|
||||||
|
@ -162,7 +103,20 @@ pub async fn image_proxy(
|
||||||
RemoteImage::validate(&mut context.pool(), url.clone().into()).await?;
|
RemoteImage::validate(&mut context.pool(), url.clone().into()).await?;
|
||||||
|
|
||||||
let pictrs_config = context.settings().pictrs_config()?;
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
let processed_url = params.process_url(¶ms.url, &pictrs_config.url);
|
let processed_url = if params.file_type.is_none() && params.max_size.is_none() {
|
||||||
|
format!("{}image/original?proxy={}", pictrs_config.url, params.url)
|
||||||
|
} else {
|
||||||
|
let file_type = file_type(params.file_type, url.as_str());
|
||||||
|
let mut url = format!(
|
||||||
|
"{}image/process.{}?proxy={}",
|
||||||
|
pictrs_config.url, file_type, url
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(size) = params.max_size {
|
||||||
|
url = format!("{url}&thumbnail={size}",);
|
||||||
|
}
|
||||||
|
url
|
||||||
|
};
|
||||||
|
|
||||||
let bypass_proxy = pictrs_config
|
let bypass_proxy = pictrs_config
|
||||||
.proxy_bypass_domains
|
.proxy_bypass_domains
|
||||||
|
@ -173,6 +127,6 @@ pub async fn image_proxy(
|
||||||
Ok(Either::Left(Redirect::to(url.to_string()).respond_to(&req)))
|
Ok(Either::Left(Redirect::to(url.to_string()).respond_to(&req)))
|
||||||
} else {
|
} else {
|
||||||
// Proxy the image data through Lemmy
|
// Proxy the image data through Lemmy
|
||||||
Ok(Either::Right(image(processed_url, req).await?))
|
Ok(Either::Right(do_get_image(processed_url, req).await?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
|
body::BodyStream,
|
||||||
http::{
|
http::{
|
||||||
header::{HeaderName, ACCEPT_ENCODING, HOST},
|
header::{HeaderName, ACCEPT_ENCODING, HOST},
|
||||||
Method,
|
Method,
|
||||||
|
@ -6,6 +7,7 @@ use actix_web::{
|
||||||
},
|
},
|
||||||
web::{Data, Payload},
|
web::{Data, Payload},
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
|
HttpResponse,
|
||||||
};
|
};
|
||||||
use futures::stream::{Stream, StreamExt};
|
use futures::stream::{Stream, StreamExt};
|
||||||
use http::HeaderValue;
|
use http::HeaderValue;
|
||||||
|
@ -23,7 +25,6 @@ use lemmy_utils::{error::LemmyResult, settings::SETTINGS, REQWEST_TIMEOUT};
|
||||||
use reqwest::Body;
|
use reqwest::Body;
|
||||||
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
|
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, RequestBuilder};
|
||||||
use reqwest_tracing::TracingMiddleware;
|
use reqwest_tracing::TracingMiddleware;
|
||||||
use serde::Deserialize;
|
|
||||||
use std::{sync::LazyLock, time::Duration};
|
use std::{sync::LazyLock, time::Duration};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -39,40 +40,7 @@ pub(super) static PICTRS_CLIENT: LazyLock<ClientWithMiddleware> = LazyLock::new(
|
||||||
.build()
|
.build()
|
||||||
});
|
});
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
fn adapt_request(request: &HttpRequest, url: String) -> RequestBuilder {
|
||||||
pub struct PictrsGetParams {
|
|
||||||
format: Option<String>,
|
|
||||||
thumbnail: Option<i32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) trait ProcessUrl {
|
|
||||||
/// If thumbnail or format is given, this uses the pictrs process endpoint.
|
|
||||||
/// Otherwise, it uses the normal pictrs url (IE image/original).
|
|
||||||
fn process_url(&self, image_url: &str, pictrs_url: &Url) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProcessUrl for PictrsGetParams {
|
|
||||||
fn process_url(&self, src: &str, pictrs_url: &Url) -> String {
|
|
||||||
if self.format.is_none() && self.thumbnail.is_none() {
|
|
||||||
format!("{}image/original/{}", pictrs_url, src)
|
|
||||||
} else {
|
|
||||||
// Take file type from name, or jpg if nothing is given
|
|
||||||
let format = self
|
|
||||||
.clone()
|
|
||||||
.format
|
|
||||||
.unwrap_or_else(|| src.split('.').last().unwrap_or("jpg").to_string());
|
|
||||||
|
|
||||||
let mut url = format!("{}image/process.{}?src={}", pictrs_url, format, src);
|
|
||||||
|
|
||||||
if let Some(size) = self.thumbnail {
|
|
||||||
url = format!("{url}&thumbnail={size}",);
|
|
||||||
}
|
|
||||||
url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn adapt_request(request: &HttpRequest, url: String) -> RequestBuilder {
|
|
||||||
// remove accept-encoding header so that pictrs doesn't compress the response
|
// remove accept-encoding header so that pictrs doesn't compress the response
|
||||||
const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST];
|
const INVALID_HEADERS: &[HeaderName] = &[ACCEPT_ENCODING, HOST];
|
||||||
|
|
||||||
|
@ -141,10 +109,7 @@ pub(super) fn convert_method(method: &Method) -> http::Method {
|
||||||
http::Method::from_bytes(method.as_str().as_bytes()).expect("method can be converted")
|
http::Method::from_bytes(method.as_str().as_bytes()).expect("method can be converted")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn convert_header<'a>(
|
fn convert_header<'a>(name: &'a http::HeaderName, value: &'a HeaderValue) -> (&'a str, &'a [u8]) {
|
||||||
name: &'a http::HeaderName,
|
|
||||||
value: &'a HeaderValue,
|
|
||||||
) -> (&'a str, &'a [u8]) {
|
|
||||||
(name.as_str(), value.as_bytes())
|
(name.as_str(), value.as_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,7 +168,7 @@ pub(super) async fn do_upload_image(
|
||||||
};
|
};
|
||||||
|
|
||||||
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
||||||
let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?;
|
let thumbnail_url = image.image_url(&protocol_and_hostname)?;
|
||||||
|
|
||||||
// Also store the details for the image
|
// Also store the details for the image
|
||||||
let details_form = image.details.build_image_details_form(&thumbnail_url);
|
let details_form = image.details.build_image_details_form(&thumbnail_url);
|
||||||
|
@ -217,6 +182,32 @@ pub(super) async fn do_upload_image(
|
||||||
Ok(image)
|
Ok(image)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) async fn do_get_image(url: String, req: HttpRequest) -> LemmyResult<HttpResponse> {
|
||||||
|
let mut client_req = adapt_request(&req, url);
|
||||||
|
|
||||||
|
if let Some(addr) = req.head().peer_addr {
|
||||||
|
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(addr) = req.head().peer_addr {
|
||||||
|
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = client_req.send().await?;
|
||||||
|
|
||||||
|
if res.status() == http::StatusCode::NOT_FOUND {
|
||||||
|
return Ok(HttpResponse::NotFound().finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut client_res = HttpResponse::build(StatusCode::from_u16(res.status().as_u16())?);
|
||||||
|
|
||||||
|
for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
|
||||||
|
client_res.insert_header(convert_header(name, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(client_res.body(BodyStream::new(res.bytes_stream())))
|
||||||
|
}
|
||||||
|
|
||||||
/// When adding a new avatar, banner or similar image, delete the old one.
|
/// When adding a new avatar, banner or similar image, delete the old one.
|
||||||
pub(super) async fn delete_old_image(
|
pub(super) async fn delete_old_image(
|
||||||
old_image: &Option<DbUrl>,
|
old_image: &Option<DbUrl>,
|
||||||
|
@ -232,3 +223,10 @@ pub(super) async fn delete_old_image(
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take file type from param, name, or use jpg if nothing is given
|
||||||
|
pub(super) fn file_type(file_type: Option<String>, name: &str) -> String {
|
||||||
|
file_type
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| name.split('.').last().unwrap_or("jpg").to_string())
|
||||||
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@ pub enum PictrsImageMode {
|
||||||
#[default]
|
#[default]
|
||||||
StoreLinkPreviews,
|
StoreLinkPreviews,
|
||||||
/// If enabled, all images from remote domains are rewritten to pass through
|
/// If enabled, all images from remote domains are rewritten to pass through
|
||||||
/// `/api/v4/image_proxy`, including embedded images in markdown. Images are stored temporarily
|
/// `/api/v4/image/proxy`, including embedded images in markdown. Images are stored temporarily
|
||||||
/// in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
|
/// in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
|
||||||
/// servers, and decreases load on other servers. However it increases bandwidth use for the
|
/// servers, and decreases load on other servers. However it increases bandwidth use for the
|
||||||
/// local server.
|
/// local server.
|
||||||
|
|
|
@ -141,7 +141,7 @@ mod tests {
|
||||||
(
|
(
|
||||||
"remote image proxied",
|
"remote image proxied",
|
||||||
"![link](http://example.com/image.jpg)",
|
"![link](http://example.com/image.jpg)",
|
||||||
"![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)",
|
"![link](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"local image unproxied",
|
"local image unproxied",
|
||||||
|
@ -151,7 +151,7 @@ mod tests {
|
||||||
(
|
(
|
||||||
"multiple image links",
|
"multiple image links",
|
||||||
"![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)",
|
"![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)",
|
||||||
"![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)",
|
"![link](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"empty link handled",
|
"empty link handled",
|
||||||
|
@ -161,7 +161,7 @@ mod tests {
|
||||||
(
|
(
|
||||||
"empty label handled",
|
"empty label handled",
|
||||||
"![](http://example.com/image.jpg)",
|
"![](http://example.com/image.jpg)",
|
||||||
"![](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)"
|
"![](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)"
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"invalid image link removed",
|
"invalid image link removed",
|
||||||
|
@ -171,12 +171,12 @@ mod tests {
|
||||||
(
|
(
|
||||||
"label with nested markdown handled",
|
"label with nested markdown handled",
|
||||||
"![a *b* c](http://example.com/image.jpg)",
|
"![a *b* c](http://example.com/image.jpg)",
|
||||||
"![a *b* c](https://lemmy-alpha/api/v4/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)"
|
"![a *b* c](https://lemmy-alpha/api/v4/image/proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)"
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"custom emoji support",
|
"custom emoji support",
|
||||||
r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#,
|
r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#,
|
||||||
r#"![party-blob](https://lemmy-alpha/api/v4/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#
|
r#"![party-blob](https://lemmy-alpha/api/v4/image/proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -134,13 +134,7 @@ use lemmy_apub::api::{
|
||||||
search::search,
|
search::search,
|
||||||
user_settings_backup::{export_settings, import_settings},
|
user_settings_backup::{export_settings, import_settings},
|
||||||
};
|
};
|
||||||
use lemmy_routes::images::{
|
use lemmy_routes::images::{delete_image, get_image, image_proxy, pictrs_health, upload_image};
|
||||||
delete_image,
|
|
||||||
get_full_res_image,
|
|
||||||
image_proxy,
|
|
||||||
pictrs_healthz,
|
|
||||||
upload_image,
|
|
||||||
};
|
|
||||||
use lemmy_utils::rate_limit::RateLimitCell;
|
use lemmy_utils::rate_limit::RateLimitCell;
|
||||||
|
|
||||||
// Deprecated, use api v4 instead.
|
// Deprecated, use api v4 instead.
|
||||||
|
@ -153,9 +147,9 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
.wrap(rate_limit.image())
|
.wrap(rate_limit.image())
|
||||||
.route(post().to(upload_image)),
|
.route(post().to(upload_image)),
|
||||||
)
|
)
|
||||||
.service(resource("/pictrs/image/{filename}").route(get().to(get_full_res_image)))
|
.service(resource("/pictrs/image/{filename}").route(get().to(get_image)))
|
||||||
.service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete_image)))
|
.service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete_image)))
|
||||||
.service(resource("/pictrs/healthz").route(get().to(pictrs_healthz)))
|
.service(resource("/pictrs/healthz").route(get().to(pictrs_health)))
|
||||||
.service(
|
.service(
|
||||||
scope("/api/v3")
|
scope("/api/v3")
|
||||||
.route("/image_proxy", get().to(image_proxy))
|
.route("/image_proxy", get().to(image_proxy))
|
||||||
|
|
|
@ -160,7 +160,12 @@ use lemmy_apub::api::{
|
||||||
user_settings_backup::{export_settings, import_settings},
|
user_settings_backup::{export_settings, import_settings},
|
||||||
};
|
};
|
||||||
use lemmy_routes::images::{
|
use lemmy_routes::images::{
|
||||||
delete_image, get_full_res_image, image_proxy, person::upload_avatar, pictrs_healthz, upload_image
|
delete_image,
|
||||||
|
get_image,
|
||||||
|
image_proxy,
|
||||||
|
person::upload_avatar,
|
||||||
|
pictrs_health,
|
||||||
|
upload_image,
|
||||||
};
|
};
|
||||||
use lemmy_utils::rate_limit::RateLimitCell;
|
use lemmy_utils::rate_limit::RateLimitCell;
|
||||||
|
|
||||||
|
@ -289,8 +294,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
.route("/change_password", put().to(change_password))
|
.route("/change_password", put().to(change_password))
|
||||||
.route("/totp/generate", post().to(generate_totp_secret))
|
.route("/totp/generate", post().to(generate_totp_secret))
|
||||||
.route("/totp/update", post().to(update_totp))
|
.route("/totp/update", post().to(update_totp))
|
||||||
.route("/verify_email", post().to(verify_email))
|
.route("/verify_email", post().to(verify_email)),
|
||||||
.route("/avatar", post().to(upload_avatar)),
|
|
||||||
)
|
)
|
||||||
.route("/account/settings/save", put().to(save_user_settings))
|
.route("/account/settings/save", put().to(save_user_settings))
|
||||||
.service(
|
.service(
|
||||||
|
@ -318,6 +322,7 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
.route("/unread_count", get().to(unread_count))
|
.route("/unread_count", get().to(unread_count))
|
||||||
.route("/list_logins", get().to(list_logins))
|
.route("/list_logins", get().to(list_logins))
|
||||||
.route("/validate_auth", get().to(validate_auth))
|
.route("/validate_auth", get().to(validate_auth))
|
||||||
|
.route("/avatar", post().to(upload_avatar))
|
||||||
.service(
|
.service(
|
||||||
scope("/block")
|
scope("/block")
|
||||||
.route("/person", post().to(user_block_person))
|
.route("/person", post().to(user_block_person))
|
||||||
|
@ -395,13 +400,12 @@ pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) {
|
||||||
.service(
|
.service(
|
||||||
resource("")
|
resource("")
|
||||||
.wrap(rate_limit.image())
|
.wrap(rate_limit.image())
|
||||||
.route(post().to(upload_image)),
|
.route(post().to(upload_image))
|
||||||
|
.route(delete().to(delete_image)),
|
||||||
)
|
)
|
||||||
.route("/proxy", get().to(image_proxy))
|
.route("/proxy", get().to(image_proxy))
|
||||||
.route("/image/{filename}", get().to(get_full_res_image))
|
.route("/{filename}", get().to(get_image))
|
||||||
// TODO: params are a bit strange like this
|
.route("/health", get().to(pictrs_health)),
|
||||||
.route("{token}/{filename}", delete().to(delete_image))
|
|
||||||
.route("/healthz", get().to(pictrs_healthz)),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ use lemmy_apub::{
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{source::secret::Secret, utils::build_db_pool};
|
use lemmy_db_schema::{source::secret::Secret, utils::build_db_pool};
|
||||||
use lemmy_federate::{Opts, SendManager};
|
use lemmy_federate::{Opts, SendManager};
|
||||||
use lemmy_routes::{feeds, images, nodeinfo, webfinger};
|
use lemmy_routes::{feeds, nodeinfo, webfinger};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::{LemmyErrorType, LemmyResult},
|
error::{LemmyErrorType, LemmyResult},
|
||||||
rate_limit::RateLimitCell,
|
rate_limit::RateLimitCell,
|
||||||
|
|
Loading…
Reference in a new issue