@@ -14,25 +14,30 @@ use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
14
14
use diesel_async:: RunQueryDsl ;
15
15
use headers:: { ContentType , HeaderMapExt } ;
16
16
use http:: HeaderValue ;
17
+ use iso8601_timestamp:: Timestamp ;
17
18
use kitsune_cache:: { ArcCache , CacheBackend } ;
18
19
use kitsune_db:: {
19
20
model:: {
20
21
account:: { Account , AccountConflictChangeset , NewAccount , UpdateAccountMedia } ,
22
+ custom_emoji:: CustomEmoji ,
23
+ media_attachment:: { MediaAttachment , NewMediaAttachment } ,
21
24
post:: Post ,
22
25
} ,
23
- schema:: { accounts, posts} ,
26
+ schema:: { accounts, custom_emojis , media_attachments , posts} ,
24
27
PgPool ,
25
28
} ;
26
29
use kitsune_embed:: Client as EmbedClient ;
27
30
use kitsune_http_client:: Client ;
31
+
28
32
use kitsune_search:: SearchBackend ;
29
33
use kitsune_type:: {
30
- ap:: { actor:: Actor , Object } ,
34
+ ap:: { actor:: Actor , emoji :: Emoji , Object } ,
31
35
jsonld:: RdfNode ,
32
36
} ;
33
37
use mime:: Mime ;
34
38
use scoped_futures:: ScopedFutureExt ;
35
39
use serde:: de:: DeserializeOwned ;
40
+ use speedy_uuid:: Uuid ;
36
41
use typed_builder:: TypedBuilder ;
37
42
use url:: Url ;
38
43
@@ -175,7 +180,7 @@ impl Fetcher {
175
180
176
181
let mut actor: Actor = self . fetch_ap_resource ( url. as_str ( ) ) . await ?;
177
182
178
- let mut domain = url. host_str ( ) . unwrap ( ) ;
183
+ let mut domain = url. host_str ( ) . ok_or ( ApiError :: MissingHost ) ? ;
179
184
let domain_buf;
180
185
let fetch_webfinger = opts
181
186
. acct
@@ -203,7 +208,7 @@ impl Fetcher {
203
208
} ;
204
209
if !used_webfinger && actor. id != url. as_str ( ) {
205
210
url = Url :: parse ( & actor. id ) ?;
206
- domain = url. host_str ( ) . unwrap ( ) ;
211
+ domain = url. host_str ( ) . ok_or ( ApiError :: MissingHost ) ? ;
207
212
}
208
213
209
214
actor. clean_html ( ) ;
@@ -300,6 +305,88 @@ impl Fetcher {
300
305
Ok ( account)
301
306
}
302
307
308
+ pub async fn fetch_emoji ( & self , url : & str ) -> Result < CustomEmoji > {
309
+ let existing_emoji = self
310
+ . db_pool
311
+ . with_connection ( |db_conn| {
312
+ async move {
313
+ custom_emojis:: table
314
+ . filter ( custom_emojis:: remote_id. eq ( url) )
315
+ . select ( CustomEmoji :: as_select ( ) )
316
+ . first ( db_conn)
317
+ . await
318
+ . optional ( )
319
+ }
320
+ . scoped ( )
321
+ } )
322
+ . await ?;
323
+
324
+ if let Some ( emoji) = existing_emoji {
325
+ return Ok ( emoji) ;
326
+ }
327
+
328
+ let mut url = Url :: parse ( url) ?;
329
+ if !self . federation_filter . is_url_allowed ( & url) ? {
330
+ return Err ( ApiError :: Unauthorised . into ( ) ) ;
331
+ }
332
+
333
+ let emoji: Emoji = self . client . get ( url. as_str ( ) ) . await ?. jsonld ( ) . await ?;
334
+
335
+ let mut domain = url. host_str ( ) . ok_or ( ApiError :: MissingHost ) ?;
336
+
337
+ if emoji. id != url. as_str ( ) {
338
+ url = Url :: parse ( & emoji. id ) ?;
339
+ domain = url. host_str ( ) . ok_or ( ApiError :: MissingHost ) ?;
340
+ }
341
+
342
+ let content_type = emoji
343
+ . icon
344
+ . media_type
345
+ . as_deref ( )
346
+ . or_else ( || mime_guess:: from_path ( & emoji. icon . url ) . first_raw ( ) )
347
+ . ok_or ( ApiError :: UnsupportedMediaType ) ?;
348
+
349
+ let name_pure = emoji. name . replace ( ':' , "" ) ;
350
+
351
+ let emoji: CustomEmoji = self
352
+ . db_pool
353
+ . with_transaction ( |tx| {
354
+ async move {
355
+ let media_attachment = diesel:: insert_into ( media_attachments:: table)
356
+ . values ( NewMediaAttachment {
357
+ id : Uuid :: now_v7 ( ) ,
358
+ account_id : None ,
359
+ content_type,
360
+ description : None ,
361
+ blurhash : None ,
362
+ file_path : None ,
363
+ remote_url : Some ( & emoji. icon . url ) ,
364
+ } )
365
+ . returning ( MediaAttachment :: as_returning ( ) )
366
+ . get_result :: < MediaAttachment > ( tx)
367
+ . await ?;
368
+ let emoji = diesel:: insert_into ( custom_emojis:: table)
369
+ . values ( CustomEmoji {
370
+ id : Uuid :: now_v7 ( ) ,
371
+ remote_id : emoji. id ,
372
+ shortcode : name_pure. to_string ( ) ,
373
+ domain : Some ( domain. to_string ( ) ) ,
374
+ media_attachment_id : media_attachment. id ,
375
+ endorsed : false ,
376
+ created_at : Timestamp :: now_utc ( ) ,
377
+ updated_at : Timestamp :: now_utc ( ) ,
378
+ } )
379
+ . returning ( CustomEmoji :: as_returning ( ) )
380
+ . get_result :: < CustomEmoji > ( tx)
381
+ . await ?;
382
+ Ok :: < _ , Error > ( emoji)
383
+ }
384
+ . scope_boxed ( )
385
+ } )
386
+ . await ?;
387
+ Ok ( emoji)
388
+ }
389
+
303
390
#[ async_recursion]
304
391
pub ( super ) async fn fetch_object_inner (
305
392
& self ,
@@ -382,7 +469,10 @@ mod test {
382
469
use iso8601_timestamp:: Timestamp ;
383
470
use kitsune_cache:: NoopCache ;
384
471
use kitsune_config:: instance:: FederationFilterConfiguration ;
385
- use kitsune_db:: { model:: account:: Account , schema:: accounts} ;
472
+ use kitsune_db:: {
473
+ model:: { account:: Account , media_attachment:: MediaAttachment } ,
474
+ schema:: { accounts, media_attachments} ,
475
+ } ;
386
476
use kitsune_http_client:: Client ;
387
477
use kitsune_search:: NoopSearchService ;
388
478
use kitsune_test:: { build_ap_response, database_test} ;
@@ -915,6 +1005,55 @@ mod test {
915
1005
. await ;
916
1006
}
917
1007
1008
+ #[ tokio:: test]
1009
+ #[ serial_test:: serial]
1010
+ async fn fetch_emoji ( ) {
1011
+ database_test ( |db_pool| async move {
1012
+ let client = Client :: builder ( ) . service ( service_fn ( handle) ) ;
1013
+
1014
+ let fetcher = Fetcher :: builder ( )
1015
+ . client ( client. clone ( ) )
1016
+ . db_pool ( db_pool. clone ( ) )
1017
+ . embed_client ( None )
1018
+ . federation_filter (
1019
+ FederationFilterService :: new ( & FederationFilterConfiguration :: Deny {
1020
+ domains : Vec :: new ( ) ,
1021
+ } )
1022
+ . unwrap ( ) ,
1023
+ )
1024
+ . search_backend ( NoopSearchService )
1025
+ . webfinger ( Webfinger :: with_client ( client, Arc :: new ( NoopCache . into ( ) ) ) )
1026
+ . post_cache ( Arc :: new ( NoopCache . into ( ) ) )
1027
+ . user_cache ( Arc :: new ( NoopCache . into ( ) ) )
1028
+ . build ( ) ;
1029
+
1030
+ let emoji = fetcher
1031
+ . fetch_emoji ( "https://corteximplant.com/emojis/7952" )
1032
+ . await
1033
+ . expect ( "Fetch emoji" ) ;
1034
+ assert_eq ! ( emoji. shortcode, "Blobhaj" ) ;
1035
+ assert_eq ! ( emoji. domain, Some ( String :: from( "corteximplant.com" ) ) ) ;
1036
+
1037
+ let media_attachment = db_pool
1038
+ . with_connection ( |db_conn| {
1039
+ media_attachments:: table
1040
+ . find ( emoji. media_attachment_id )
1041
+ . select ( MediaAttachment :: as_select ( ) )
1042
+ . get_result :: < MediaAttachment > ( db_conn)
1043
+ . scoped ( )
1044
+ } )
1045
+ . await
1046
+ . expect ( "Get media attachment" ) ;
1047
+
1048
+ assert_eq ! (
1049
+ media_attachment. remote_url,
1050
+ Some ( String :: from(
1051
+ "https://corteximplant.com/system/custom_emojis/images/000/007/952/original/33b7f12bd094b815.png"
1052
+ ) ) ) ;
1053
+ } )
1054
+ . await ;
1055
+ }
1056
+
918
1057
async fn handle ( req : Request < Body > ) -> Result < Response < Body > , Infallible > {
919
1058
match req. uri ( ) . path_and_query ( ) . unwrap ( ) . as_str ( ) {
920
1059
"/users/0x0" => {
@@ -933,6 +1072,16 @@ mod test {
933
1072
) ;
934
1073
Ok :: < _ , Infallible > ( build_ap_response ( body) )
935
1074
}
1075
+ "/emojis/7952" => {
1076
+ let body =
1077
+ include_str ! ( "../../../../test-fixtures/corteximplant.com_emoji_7952.json" ) ;
1078
+ Ok :: < _ , Infallible > ( build_ap_response ( body) )
1079
+ }
1080
+ "/emojis/8933" => {
1081
+ let body =
1082
+ include_str ! ( "../../../../test-fixtures/corteximplant.com_emoji_8933.json" ) ;
1083
+ Ok :: < _ , Infallible > ( build_ap_response ( body) )
1084
+ }
936
1085
"/.well-known/webfinger?resource=acct:0x0@corteximplant.com" => {
937
1086
let body = include_str ! ( "../../../../test-fixtures/0x0_jrd.json" ) ;
938
1087
Ok :: < _ , Infallible > ( Response :: new ( Body :: from ( body) ) )
0 commit comments