Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom and remote emojis #405

Merged
merged 21 commits into from
Nov 11, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 154 additions & 5 deletions crates/kitsune-core/src/activitypub/fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,30 @@ use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
use diesel_async::RunQueryDsl;
use headers::{ContentType, HeaderMapExt};
use http::HeaderValue;
use iso8601_timestamp::Timestamp;
use kitsune_cache::{ArcCache, CacheBackend};
use kitsune_db::{
model::{
account::{Account, AccountConflictChangeset, NewAccount, UpdateAccountMedia},
custom_emoji::CustomEmoji,
media_attachment::{MediaAttachment, NewMediaAttachment},
post::Post,
},
schema::{accounts, posts},
schema::{accounts, custom_emojis, media_attachments, posts},
PgPool,
};
use kitsune_embed::Client as EmbedClient;
use kitsune_http_client::Client;

use kitsune_search::SearchBackend;
use kitsune_type::{
ap::{actor::Actor, Object},
ap::{actor::Actor, emoji::Emoji, Object},
jsonld::RdfNode,
};
use mime::Mime;
use scoped_futures::ScopedFutureExt;
use serde::de::DeserializeOwned;
use speedy_uuid::Uuid;
use typed_builder::TypedBuilder;
use url::Url;

Expand Down Expand Up @@ -175,7 +180,7 @@ impl Fetcher {

let mut actor: Actor = self.fetch_ap_resource(url.as_str()).await?;

let mut domain = url.host_str().unwrap();
let mut domain = url.host_str().ok_or(ApiError::MissingHost)?;
let domain_buf;
let fetch_webfinger = opts
.acct
Expand All @@ -202,7 +207,7 @@ impl Fetcher {
};
if !used_webfinger && actor.id != url.as_str() {
url = Url::parse(&actor.id)?;
domain = url.host_str().unwrap();
domain = url.host_str().ok_or(ApiError::MissingHost)?;
}

actor.clean_html();
Expand Down Expand Up @@ -299,6 +304,88 @@ impl Fetcher {
Ok(account)
}

pub async fn fetch_emoji(&self, url: &str) -> Result<CustomEmoji> {
let existing_emoji = self
.db_pool
.with_connection(|db_conn| {
async move {
custom_emojis::table
.filter(custom_emojis::remote_id.eq(url))
.select(CustomEmoji::as_select())
.first(db_conn)
.await
.optional()
}
.scoped()
})
.await?;

if let Some(emoji) = existing_emoji {
return Ok(emoji);
}

let mut url = Url::parse(url)?;
if !self.federation_filter.is_url_allowed(&url)? {
return Err(ApiError::Unauthorised.into());
}

let emoji: Emoji = self.client.get(url.as_str()).await?.jsonld().await?;

let mut domain = url.host_str().ok_or(ApiError::MissingHost)?;

if emoji.id != url.as_str() {
url = Url::parse(&emoji.id)?;
domain = url.host_str().ok_or(ApiError::MissingHost)?;
}

let content_type = emoji
.icon
.media_type
.as_deref()
.or_else(|| mime_guess::from_path(&emoji.icon.url).first_raw())
.ok_or(ApiError::UnsupportedMediaType)?;

let name_pure = emoji.name.replace(':', "");

let emoji: CustomEmoji = self
.db_pool
.with_transaction(|tx| {
async move {
let media_attachment = diesel::insert_into(media_attachments::table)
.values(NewMediaAttachment {
id: Uuid::now_v7(),
account_id: None,
content_type,
description: None,
blurhash: None,
file_path: None,
remote_url: Some(&emoji.icon.url),
})
.returning(MediaAttachment::as_returning())
.get_result::<MediaAttachment>(tx)
.await?;
let emoji = diesel::insert_into(custom_emojis::table)
.values(CustomEmoji {
id: Uuid::now_v7(),
remote_id: emoji.id,
shortcode: name_pure.to_string(),
domain: Some(domain.to_string()),
media_attachment_id: media_attachment.id,
endorsed: false,
created_at: Timestamp::now_utc(),
updated_at: Timestamp::now_utc(),
})
.returning(CustomEmoji::as_returning())
.get_result::<CustomEmoji>(tx)
.await?;
Ok::<_, Error>(emoji)
}
.scope_boxed()
})
.await?;
Ok(emoji)
}

#[async_recursion]
pub(super) async fn fetch_object_inner(
&self,
Expand Down Expand Up @@ -381,7 +468,10 @@ mod test {
use iso8601_timestamp::Timestamp;
use kitsune_cache::NoopCache;
use kitsune_config::FederationFilterConfiguration;
use kitsune_db::{model::account::Account, schema::accounts};
use kitsune_db::{
model::{account::Account, media_attachment::MediaAttachment},
schema::{accounts, media_attachments},
};
use kitsune_http_client::Client;
use kitsune_search::NoopSearchService;
use kitsune_test::{build_ap_response, database_test};
Expand Down Expand Up @@ -914,6 +1004,55 @@ mod test {
.await;
}

#[tokio::test]
#[serial_test::serial]
async fn fetch_emoji() {
database_test(|db_pool| async move {
let client = Client::builder().service(service_fn(handle));

let fetcher = Fetcher::builder()
.client(client.clone())
.db_pool(db_pool.clone())
.embed_client(None)
.federation_filter(
FederationFilterService::new(&FederationFilterConfiguration::Deny {
domains: Vec::new(),
})
.unwrap(),
)
.search_backend(NoopSearchService)
.webfinger(Webfinger::with_client(client, Arc::new(NoopCache.into())))
.post_cache(Arc::new(NoopCache.into()))
.user_cache(Arc::new(NoopCache.into()))
.build();

let emoji = fetcher
.fetch_emoji("https://corteximplant.com/emojis/7952")
.await
.expect("Fetch emoji");
assert_eq!(emoji.shortcode, "Blobhaj");
assert_eq!(emoji.domain, Some(String::from("corteximplant.com")));

let media_attachment = db_pool
.with_connection(|db_conn| {
media_attachments::table
.find(emoji.media_attachment_id)
.select(MediaAttachment::as_select())
.get_result::<MediaAttachment>(db_conn)
.scoped()
})
.await
.expect("Get media attachment");

assert_eq!(
media_attachment.remote_url,
Some(String::from(
"https://corteximplant.com/system/custom_emojis/images/000/007/952/original/33b7f12bd094b815.png"
)));
})
.await;
}

async fn handle(req: Request<Body>) -> Result<Response<Body>, Infallible> {
match req.uri().path_and_query().unwrap().as_str() {
"/users/0x0" => {
Expand All @@ -932,6 +1071,16 @@ mod test {
);
Ok::<_, Infallible>(build_ap_response(body))
}
"/emojis/7952" => {
let body =
include_str!("../../../../test-fixtures/corteximplant.com_emoji_7952.json");
Ok::<_, Infallible>(build_ap_response(body))
}
"/emojis/8933" => {
let body =
include_str!("../../../../test-fixtures/corteximplant.com_emoji_8933.json");
Ok::<_, Infallible>(build_ap_response(body))
}
"/.well-known/webfinger?resource=acct:[email protected]" => {
let body = include_str!("../../../../test-fixtures/0x0_jrd.json");
Ok::<_, Infallible>(Response::new(Body::from(body)))
Expand Down
55 changes: 53 additions & 2 deletions crates/kitsune-core/src/activitypub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ use crate::{
};
use diesel::{ExpressionMethods, SelectableHelper};
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use futures_util::future::join_all;
use http::Uri;
use iso8601_timestamp::Timestamp;
use kitsune_db::{
model::{
account::Account,
custom_emoji::PostCustomEmoji,
media_attachment::{NewMediaAttachment, NewPostMediaAttachment},
mention::NewMention,
post::{FullPostChangeset, NewPost, Post, PostConflictChangeset, Visibility},
},
schema::{media_attachments, posts, posts_media_attachments, posts_mentions},
schema::{
media_attachments, posts, posts_custom_emojis, posts_media_attachments, posts_mentions,
},
PgPool,
};
use kitsune_embed::Client as EmbedClient;
Expand Down Expand Up @@ -63,6 +67,48 @@ async fn handle_mentions(
Ok(())
}

async fn handle_custom_emojis(
db_conn: &mut AsyncPgConnection,
post_id: Uuid,
fetcher: &Fetcher,
tags: &[Tag],
) -> Result<()> {
let emoji_iter = tags.iter().filter(|tag| tag.r#type == TagType::Emoji);

let emoji_count = emoji_iter.clone().count();
if emoji_count == 0 {
return Ok(());
}

let futures = emoji_iter.clone().filter_map(|emoji| {
emoji
.id
.as_ref()
.map(|remote_id| fetcher.fetch_emoji(remote_id))
});

let emojis = join_all(futures)
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()?
.iter()
.zip(emoji_iter)
zeerooth marked this conversation as resolved.
Show resolved Hide resolved
.map(|(resolved_emoji, emoji_tag)| PostCustomEmoji {
post_id,
custom_emoji_id: resolved_emoji.id,
emoji_text: emoji_tag.name.to_string(),
})
.collect::<Vec<_>>();

diesel::insert_into(posts_custom_emojis::table)
.values(emojis)
.on_conflict_do_nothing()
.execute(db_conn)
.await?;

Ok(())
}

/// Process a bunch of ActivityPub attachments
///
/// # Returns
Expand Down Expand Up @@ -91,7 +137,7 @@ pub async fn process_attachments(

Some(NewMediaAttachment {
id: attachment_id,
account_id: author.id,
account_id: Some(author.id),
content_type,
description: attachment.name.as_deref(),
blurhash: attachment.blurhash.as_deref(),
Expand Down Expand Up @@ -129,6 +175,7 @@ struct PreprocessedObject<'a> {
content_lang: Language,
db_pool: &'a PgPool,
object: Box<Object>,
fetcher: &'a Fetcher,
search_backend: &'a Search,
}

Expand Down Expand Up @@ -200,6 +247,7 @@ async fn preprocess_object(
content_lang,
db_pool,
object,
fetcher,
search_backend,
})
}
Expand All @@ -214,6 +262,7 @@ pub async fn process_new_object(process_data: ProcessNewObject<'_>) -> Result<Po
content_lang,
db_pool,
object,
fetcher,
search_backend,
} = preprocess_object(process_data).await?;

Expand Down Expand Up @@ -262,6 +311,7 @@ pub async fn process_new_object(process_data: ProcessNewObject<'_>) -> Result<Po
.await?;

handle_mentions(tx, &user, new_post.id, &object.tag).await?;
handle_custom_emojis(tx, new_post.id, fetcher, &object.tag).await?;

Ok::<_, Error>(new_post)
}
Expand All @@ -286,6 +336,7 @@ pub async fn update_object(process_data: ProcessNewObject<'_>) -> Result<Post> {
content_lang,
db_pool,
object,
fetcher: _,
search_backend,
} = preprocess_object(process_data).await?;

Expand Down
1 change: 1 addition & 0 deletions crates/kitsune-core/src/consts.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use const_format::concatcp;

pub const API_MAX_LIMIT: usize = 40;
pub const MAX_EMOJI_SHORTCODE_LENGTH: usize = 64;
pub const MAX_MEDIA_DESCRIPTION_LENGTH: usize = 5000;
pub const USER_AGENT: &str = concatcp!(env!("CARGO_PKG_NAME"), "/", VERSION);
pub const VERSION: &str = concatcp!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA"));
3 changes: 3 additions & 0 deletions crates/kitsune-core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub enum ApiError {
#[error("Invalid captcha")]
InvalidCaptcha,

#[error("Missing host")]
MissingHost,

#[error("Not found")]
NotFound,

Expand Down
Loading
Loading