Skip to content
This repository was archived by the owner on Dec 24, 2025. It is now read-only.

Commit 6b0213d

Browse files
committed
implement oembed for images
1 parent ff6417c commit 6b0213d

File tree

4 files changed

+144
-2
lines changed

4 files changed

+144
-2
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ reqwest = { version = "0.12.11", features = ["json"] }
2525
serde = { version = "1", features = ["derive"] }
2626
tokio = { version = "1", features = ["net", "rt-multi-thread"] }
2727
uuid = { version = "1", features = ["serde", "v4"] }
28+
bytes = "1.9.0"
2829

2930
[dependencies.axum]
3031
version = "0.7"

src/endpoints/image.rs

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use axum::{
22
body::Bytes,
3-
extract::{Path, State},
3+
extract::{Path, Query, State},
4+
response::Html,
45
Json,
56
};
67
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine};
8+
use bytes::Buf;
79
use chrono::{DateTime, Utc};
810
use reqwest::StatusCode;
9-
use serde::Serialize;
11+
use serde::{Deserialize, Serialize};
1012
use sqlx::{query, PgPool};
1113
use uuid::Uuid;
1214

@@ -60,6 +62,10 @@ pub async fn post(
6062
Path(filename): Path<String>,
6163
body: Bytes,
6264
) -> Result<String, ApiError> {
65+
let png = PngInfo::create(&body).await;
66+
if png.is_none() {
67+
return Err(StatusCode::BAD_REQUEST)?;
68+
}
6369
let id = Id::new();
6470
query!(
6571
"INSERT INTO images (id, player, filename, file) VALUES ($1, $2, $3, $4)",
@@ -80,3 +86,135 @@ pub async fn evict_expired(database: &PgPool) -> Result<(), TaskError> {
8086
.await?;
8187
Ok(())
8288
}
89+
90+
pub async fn get_view(
91+
State(ApiState { database, .. }): State<ApiState>,
92+
Path(id): Path<Id>,
93+
) -> Result<Html<String>, ApiError> {
94+
let image = query!("SELECT player, filename, file, timestamp FROM images WHERE id = $1", id as _)
95+
.fetch_optional(&database)
96+
.await?
97+
.ok_or(StatusCode::NOT_FOUND)?;
98+
99+
let filename = String::from_utf8(image.filename).unwrap();
100+
let base_url = "https://api.axolotlclient.com/v1/";
101+
let image_url = base_url.to_string() + "image/" + &id.to_string() + "/";
102+
103+
Ok(Html(format!(
104+
r#"
105+
<html>
106+
<head>
107+
<title>{filename}</title>
108+
<link rel="alternate" type="application/json+oembed"
109+
href="{}oembed?format=json"
110+
title="{filename}" />
111+
<style>
112+
.title {{
113+
text-align: center;
114+
}}
115+
img {{
116+
width: 100%;
117+
max-height: 85%;
118+
}}
119+
</style>
120+
</head>
121+
<body>
122+
<div class="title">
123+
<h2>{filename}</h2>
124+
</div>
125+
<img src="{}raw">
126+
</body>
127+
</html>
128+
"#,
129+
&image_url, &image_url
130+
)))
131+
}
132+
133+
#[derive(Serialize)]
134+
pub struct OEmbed {
135+
version: &'static str,
136+
#[serde(rename(serialize = "type"))]
137+
_type: &'static str,
138+
title: String,
139+
url: String,
140+
width: i32,
141+
height: i32,
142+
provider_name: &'static str,
143+
provider_url: &'static str,
144+
}
145+
146+
impl OEmbed {
147+
fn create(title: String, url: String, png: PngInfo) -> OEmbed {
148+
OEmbed {
149+
version: "1.0",
150+
_type: "photo",
151+
title,
152+
url,
153+
width: png.width,
154+
height: png.height,
155+
provider_name: "AxolotlClient",
156+
provider_url: "https://axolotlclient.com",
157+
}
158+
}
159+
}
160+
161+
#[derive(Deserialize)]
162+
pub struct OEmbedQuery {
163+
format: String,
164+
}
165+
166+
pub async fn get_oembed(
167+
State(ApiState { database, .. }): State<ApiState>,
168+
Path(id): Path<Id>,
169+
Query(OEmbedQuery { format }): Query<OEmbedQuery>,
170+
) -> Result<Json<OEmbed>, ApiError> {
171+
let image = query!("SELECT filename, file FROM images WHERE id = $1", id as _)
172+
.fetch_optional(&database)
173+
.await?
174+
.ok_or(StatusCode::NOT_FOUND)?;
175+
let png = PngInfo::create(&Bytes::from(image.file)).await;
176+
177+
if png.is_none() {
178+
return Err(StatusCode::BAD_REQUEST)?;
179+
}
180+
181+
let filename = String::from_utf8(image.filename).unwrap();
182+
183+
let embed = OEmbed::create(
184+
filename,
185+
"https://api.axolotlclient.com/v1/images/".to_owned() + &id.to_string() + "/raw",
186+
png.unwrap(),
187+
);
188+
Ok(if format == "json" {
189+
Json(embed)
190+
} else {
191+
return Err(StatusCode::NOT_IMPLEMENTED)?;
192+
})
193+
}
194+
195+
struct PngInfo {
196+
width: i32,
197+
height: i32,
198+
}
199+
200+
impl PngInfo {
201+
async fn create(reader: &Bytes) -> Option<PngInfo> {
202+
let mut bytes = reader.clone();
203+
let header = bytes.get_u64();
204+
if header != 0x89504E470D0A1A0A {
205+
return None;
206+
}
207+
let ihdr_size = bytes.get_u32();
208+
if ihdr_size != 0x0D {
209+
return None;
210+
}
211+
let ihdr_type = bytes.get_u32();
212+
if ihdr_type != 0x49484452 {
213+
return None;
214+
}
215+
Some(PngInfo {
216+
width: bytes.get_i32(),
217+
height: bytes.get_i32(),
218+
})
219+
}
220+
}

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ async fn main() -> anyhow::Result<()> {
136136
.route("/account/relations/requests", get(account::get_requests))
137137
.route("/image/:id", get(image::get).post(image::post))
138138
.route("/image/:id/raw", get(image::get_raw))
139+
.route("/image/:id/view", get(image::get_view))
140+
.route("/image/:id/oembed", get(image::get_oembed))
139141
.route("/hypixel", get(hypixel::get))
140142
//.route("/report/:message", post(channel::report_message))
141143
.route("/brew_coffee", get(brew_coffee).post(brew_coffee))

0 commit comments

Comments
 (0)