11use axum:: {
22 body:: Bytes ,
3- extract:: { Path , State } ,
3+ extract:: { Path , Query , State } ,
4+ response:: Html ,
45 Json ,
56} ;
67use base64:: { engine:: general_purpose:: STANDARD_NO_PAD , Engine } ;
8+ use bytes:: Buf ;
79use chrono:: { DateTime , Utc } ;
810use reqwest:: StatusCode ;
9- use serde:: Serialize ;
11+ use serde:: { Deserialize , Serialize } ;
1012use sqlx:: { query, PgPool } ;
1113use 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+ }
0 commit comments