Skip to content

Commit

Permalink
implement oembed for images
Browse files Browse the repository at this point in the history
  • Loading branch information
moehreag committed Jan 18, 2025
1 parent ff6417c commit 6b0213d
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 2 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ reqwest = { version = "0.12.11", features = ["json"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["net", "rt-multi-thread"] }
uuid = { version = "1", features = ["serde", "v4"] }
bytes = "1.9.0"

[dependencies.axum]
version = "0.7"
Expand Down
142 changes: 140 additions & 2 deletions src/endpoints/image.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use axum::{
body::Bytes,
extract::{Path, State},
extract::{Path, Query, State},
response::Html,
Json,
};
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine};
use bytes::Buf;
use chrono::{DateTime, Utc};
use reqwest::StatusCode;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use sqlx::{query, PgPool};
use uuid::Uuid;

Expand Down Expand Up @@ -60,6 +62,10 @@ pub async fn post(
Path(filename): Path<String>,
body: Bytes,
) -> Result<String, ApiError> {
let png = PngInfo::create(&body).await;
if png.is_none() {
return Err(StatusCode::BAD_REQUEST)?;
}
let id = Id::new();
query!(
"INSERT INTO images (id, player, filename, file) VALUES ($1, $2, $3, $4)",
Expand All @@ -80,3 +86,135 @@ pub async fn evict_expired(database: &PgPool) -> Result<(), TaskError> {
.await?;
Ok(())
}

pub async fn get_view(
State(ApiState { database, .. }): State<ApiState>,
Path(id): Path<Id>,
) -> Result<Html<String>, ApiError> {
let image = query!("SELECT player, filename, file, timestamp FROM images WHERE id = $1", id as _)
.fetch_optional(&database)
.await?
.ok_or(StatusCode::NOT_FOUND)?;

let filename = String::from_utf8(image.filename).unwrap();
let base_url = "https://api.axolotlclient.com/v1/";
let image_url = base_url.to_string() + "image/" + &id.to_string() + "/";

Ok(Html(format!(
r#"
<html>
<head>
<title>{filename}</title>
<link rel="alternate" type="application/json+oembed"
href="{}oembed?format=json"
title="{filename}" />
<style>
.title {{
text-align: center;
}}
img {{
width: 100%;
max-height: 85%;
}}
</style>
</head>
<body>
<div class="title">
<h2>{filename}</h2>
</div>
<img src="{}raw">
</body>
</html>
"#,
&image_url, &image_url
)))
}

#[derive(Serialize)]
pub struct OEmbed {
version: &'static str,
#[serde(rename(serialize = "type"))]
_type: &'static str,
title: String,
url: String,
width: i32,
height: i32,
provider_name: &'static str,
provider_url: &'static str,
}

impl OEmbed {
fn create(title: String, url: String, png: PngInfo) -> OEmbed {
OEmbed {
version: "1.0",
_type: "photo",
title,
url,
width: png.width,
height: png.height,
provider_name: "AxolotlClient",
provider_url: "https://axolotlclient.com",
}
}
}

#[derive(Deserialize)]
pub struct OEmbedQuery {
format: String,
}

pub async fn get_oembed(
State(ApiState { database, .. }): State<ApiState>,
Path(id): Path<Id>,
Query(OEmbedQuery { format }): Query<OEmbedQuery>,
) -> Result<Json<OEmbed>, ApiError> {
let image = query!("SELECT filename, file FROM images WHERE id = $1", id as _)
.fetch_optional(&database)
.await?
.ok_or(StatusCode::NOT_FOUND)?;
let png = PngInfo::create(&Bytes::from(image.file)).await;

if png.is_none() {
return Err(StatusCode::BAD_REQUEST)?;
}

let filename = String::from_utf8(image.filename).unwrap();

let embed = OEmbed::create(
filename,
"https://api.axolotlclient.com/v1/images/".to_owned() + &id.to_string() + "/raw",
png.unwrap(),
);
Ok(if format == "json" {
Json(embed)
} else {
return Err(StatusCode::NOT_IMPLEMENTED)?;
})
}

struct PngInfo {
width: i32,
height: i32,
}

impl PngInfo {
async fn create(reader: &Bytes) -> Option<PngInfo> {
let mut bytes = reader.clone();
let header = bytes.get_u64();
if header != 0x89504E470D0A1A0A {
return None;
}
let ihdr_size = bytes.get_u32();
if ihdr_size != 0x0D {
return None;
}
let ihdr_type = bytes.get_u32();
if ihdr_type != 0x49484452 {
return None;
}
Some(PngInfo {
width: bytes.get_i32(),
height: bytes.get_i32(),
})
}
}
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ async fn main() -> anyhow::Result<()> {
.route("/account/relations/requests", get(account::get_requests))
.route("/image/:id", get(image::get).post(image::post))
.route("/image/:id/raw", get(image::get_raw))
.route("/image/:id/view", get(image::get_view))
.route("/image/:id/oembed", get(image::get_oembed))
.route("/hypixel", get(hypixel::get))
//.route("/report/:message", post(channel::report_message))
.route("/brew_coffee", get(brew_coffee).post(brew_coffee))
Expand Down

0 comments on commit 6b0213d

Please sign in to comment.