mirror of https://github.com/kitsune-soc/kitsune
Custom and remote emojis (#405)
* initial custom emoji schema * some more progress idk * emoji process * better emoji processing * it just works * implement mastodon's /api/v1/custom_emojis * proper url and shortcode in /api/v1/custom_emojis * add emoji list to statues * add some tests and stand against clippy * handle some unsound unwraps * make emoji a resolvable AP type * fix tests * add a missing test fixture * address PR review --------- Co-authored-by: aumetra <aumetra@cryptolab.net>
This commit is contained in:
parent
2baa300297
commit
b63370118d
|
@ -14,25 +14,30 @@ use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use headers::{ContentType, HeaderMapExt};
|
use headers::{ContentType, HeaderMapExt};
|
||||||
use http::HeaderValue;
|
use http::HeaderValue;
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
use kitsune_cache::{ArcCache, CacheBackend};
|
use kitsune_cache::{ArcCache, CacheBackend};
|
||||||
use kitsune_db::{
|
use kitsune_db::{
|
||||||
model::{
|
model::{
|
||||||
account::{Account, AccountConflictChangeset, NewAccount, UpdateAccountMedia},
|
account::{Account, AccountConflictChangeset, NewAccount, UpdateAccountMedia},
|
||||||
|
custom_emoji::CustomEmoji,
|
||||||
|
media_attachment::{MediaAttachment, NewMediaAttachment},
|
||||||
post::Post,
|
post::Post,
|
||||||
},
|
},
|
||||||
schema::{accounts, posts},
|
schema::{accounts, custom_emojis, media_attachments, posts},
|
||||||
PgPool,
|
PgPool,
|
||||||
};
|
};
|
||||||
use kitsune_embed::Client as EmbedClient;
|
use kitsune_embed::Client as EmbedClient;
|
||||||
use kitsune_http_client::Client;
|
use kitsune_http_client::Client;
|
||||||
|
|
||||||
use kitsune_search::SearchBackend;
|
use kitsune_search::SearchBackend;
|
||||||
use kitsune_type::{
|
use kitsune_type::{
|
||||||
ap::{actor::Actor, Object},
|
ap::{actor::Actor, emoji::Emoji, Object},
|
||||||
jsonld::RdfNode,
|
jsonld::RdfNode,
|
||||||
};
|
};
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use scoped_futures::ScopedFutureExt;
|
use scoped_futures::ScopedFutureExt;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
use speedy_uuid::Uuid;
|
||||||
use typed_builder::TypedBuilder;
|
use typed_builder::TypedBuilder;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -175,7 +180,7 @@ impl Fetcher {
|
||||||
|
|
||||||
let mut actor: Actor = self.fetch_ap_resource(url.as_str()).await?;
|
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 domain_buf;
|
||||||
let fetch_webfinger = opts
|
let fetch_webfinger = opts
|
||||||
.acct
|
.acct
|
||||||
|
@ -203,7 +208,7 @@ impl Fetcher {
|
||||||
};
|
};
|
||||||
if !used_webfinger && actor.id != url.as_str() {
|
if !used_webfinger && actor.id != url.as_str() {
|
||||||
url = Url::parse(&actor.id)?;
|
url = Url::parse(&actor.id)?;
|
||||||
domain = url.host_str().unwrap();
|
domain = url.host_str().ok_or(ApiError::MissingHost)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
actor.clean_html();
|
actor.clean_html();
|
||||||
|
@ -300,6 +305,88 @@ impl Fetcher {
|
||||||
Ok(account)
|
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]
|
#[async_recursion]
|
||||||
pub(super) async fn fetch_object_inner(
|
pub(super) async fn fetch_object_inner(
|
||||||
&self,
|
&self,
|
||||||
|
@ -382,7 +469,10 @@ mod test {
|
||||||
use iso8601_timestamp::Timestamp;
|
use iso8601_timestamp::Timestamp;
|
||||||
use kitsune_cache::NoopCache;
|
use kitsune_cache::NoopCache;
|
||||||
use kitsune_config::instance::FederationFilterConfiguration;
|
use kitsune_config::instance::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_http_client::Client;
|
||||||
use kitsune_search::NoopSearchService;
|
use kitsune_search::NoopSearchService;
|
||||||
use kitsune_test::{build_ap_response, database_test};
|
use kitsune_test::{build_ap_response, database_test};
|
||||||
|
@ -915,6 +1005,55 @@ mod test {
|
||||||
.await;
|
.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> {
|
async fn handle(req: Request<Body>) -> Result<Response<Body>, Infallible> {
|
||||||
match req.uri().path_and_query().unwrap().as_str() {
|
match req.uri().path_and_query().unwrap().as_str() {
|
||||||
"/users/0x0" => {
|
"/users/0x0" => {
|
||||||
|
@ -933,6 +1072,16 @@ mod test {
|
||||||
);
|
);
|
||||||
Ok::<_, Infallible>(build_ap_response(body))
|
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:0x0@corteximplant.com" => {
|
"/.well-known/webfinger?resource=acct:0x0@corteximplant.com" => {
|
||||||
let body = include_str!("../../../../test-fixtures/0x0_jrd.json");
|
let body = include_str!("../../../../test-fixtures/0x0_jrd.json");
|
||||||
Ok::<_, Infallible>(Response::new(Body::from(body)))
|
Ok::<_, Infallible>(Response::new(Body::from(body)))
|
||||||
|
|
|
@ -5,17 +5,20 @@ use crate::{
|
||||||
};
|
};
|
||||||
use diesel::{ExpressionMethods, SelectableHelper};
|
use diesel::{ExpressionMethods, SelectableHelper};
|
||||||
use diesel_async::{AsyncPgConnection, RunQueryDsl};
|
use diesel_async::{AsyncPgConnection, RunQueryDsl};
|
||||||
use futures_util::FutureExt;
|
use futures_util::{future::try_join_all, FutureExt, TryFutureExt};
|
||||||
use http::Uri;
|
use http::Uri;
|
||||||
use iso8601_timestamp::Timestamp;
|
use iso8601_timestamp::Timestamp;
|
||||||
use kitsune_db::{
|
use kitsune_db::{
|
||||||
model::{
|
model::{
|
||||||
account::Account,
|
account::Account,
|
||||||
|
custom_emoji::PostCustomEmoji,
|
||||||
media_attachment::{NewMediaAttachment, NewPostMediaAttachment},
|
media_attachment::{NewMediaAttachment, NewPostMediaAttachment},
|
||||||
mention::NewMention,
|
mention::NewMention,
|
||||||
post::{FullPostChangeset, NewPost, Post, PostConflictChangeset, Visibility},
|
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,
|
PgPool,
|
||||||
};
|
};
|
||||||
use kitsune_embed::Client as EmbedClient;
|
use kitsune_embed::Client as EmbedClient;
|
||||||
|
@ -64,6 +67,43 @@ async fn handle_mentions(
|
||||||
Ok(())
|
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| {
|
||||||
|
let remote_id = emoji.id.as_ref()?;
|
||||||
|
Some(fetcher.fetch_emoji(remote_id).map_ok(move |f| (f, emoji)))
|
||||||
|
});
|
||||||
|
|
||||||
|
let emojis = try_join_all(futures)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|(resolved_emoji, emoji_tag)| PostCustomEmoji {
|
||||||
|
post_id,
|
||||||
|
custom_emoji_id: resolved_emoji.id,
|
||||||
|
emoji_text: emoji_tag.name.to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<PostCustomEmoji>>();
|
||||||
|
|
||||||
|
diesel::insert_into(posts_custom_emojis::table)
|
||||||
|
.values(emojis)
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.execute(db_conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Process a bunch of ActivityPub attachments
|
/// Process a bunch of ActivityPub attachments
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
|
@ -92,7 +132,7 @@ pub async fn process_attachments(
|
||||||
|
|
||||||
Some(NewMediaAttachment {
|
Some(NewMediaAttachment {
|
||||||
id: attachment_id,
|
id: attachment_id,
|
||||||
account_id: author.id,
|
account_id: Some(author.id),
|
||||||
content_type,
|
content_type,
|
||||||
description: attachment.name.as_deref(),
|
description: attachment.name.as_deref(),
|
||||||
blurhash: attachment.blurhash.as_deref(),
|
blurhash: attachment.blurhash.as_deref(),
|
||||||
|
@ -130,6 +170,7 @@ struct PreprocessedObject<'a> {
|
||||||
content_lang: Language,
|
content_lang: Language,
|
||||||
db_pool: &'a PgPool,
|
db_pool: &'a PgPool,
|
||||||
object: Box<Object>,
|
object: Box<Object>,
|
||||||
|
fetcher: &'a Fetcher,
|
||||||
search_backend: &'a AnySearchBackend,
|
search_backend: &'a AnySearchBackend,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,6 +242,7 @@ async fn preprocess_object(
|
||||||
content_lang,
|
content_lang,
|
||||||
db_pool,
|
db_pool,
|
||||||
object,
|
object,
|
||||||
|
fetcher,
|
||||||
search_backend,
|
search_backend,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -215,6 +257,7 @@ pub async fn process_new_object(process_data: ProcessNewObject<'_>) -> Result<Po
|
||||||
content_lang,
|
content_lang,
|
||||||
db_pool,
|
db_pool,
|
||||||
object,
|
object,
|
||||||
|
fetcher,
|
||||||
search_backend,
|
search_backend,
|
||||||
} = preprocess_object(process_data).boxed().await?;
|
} = preprocess_object(process_data).boxed().await?;
|
||||||
|
|
||||||
|
@ -263,6 +306,7 @@ pub async fn process_new_object(process_data: ProcessNewObject<'_>) -> Result<Po
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
handle_mentions(tx, &user, new_post.id, &object.tag).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)
|
Ok::<_, Error>(new_post)
|
||||||
}
|
}
|
||||||
|
@ -287,6 +331,7 @@ pub async fn update_object(process_data: ProcessNewObject<'_>) -> Result<Post> {
|
||||||
content_lang,
|
content_lang,
|
||||||
db_pool,
|
db_pool,
|
||||||
object,
|
object,
|
||||||
|
fetcher: _,
|
||||||
search_backend,
|
search_backend,
|
||||||
} = preprocess_object(process_data).await?;
|
} = preprocess_object(process_data).await?;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use const_format::concatcp;
|
use const_format::concatcp;
|
||||||
|
|
||||||
pub const API_MAX_LIMIT: usize = 40;
|
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 MAX_MEDIA_DESCRIPTION_LENGTH: usize = 5000;
|
||||||
pub const USER_AGENT: &str = concatcp!(env!("CARGO_PKG_NAME"), "/", VERSION);
|
pub const USER_AGENT: &str = concatcp!(env!("CARGO_PKG_NAME"), "/", VERSION);
|
||||||
pub const VERSION: &str = concatcp!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA"));
|
pub const VERSION: &str = concatcp!(env!("CARGO_PKG_VERSION"), "-", env!("VERGEN_GIT_SHA"));
|
||||||
|
|
|
@ -20,6 +20,9 @@ pub enum ApiError {
|
||||||
#[error("Invalid captcha")]
|
#[error("Invalid captcha")]
|
||||||
InvalidCaptcha,
|
InvalidCaptcha,
|
||||||
|
|
||||||
|
#[error("Missing host")]
|
||||||
|
MissingHost,
|
||||||
|
|
||||||
#[error("Not found")]
|
#[error("Not found")]
|
||||||
NotFound,
|
NotFound,
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,9 @@ use kitsune_search::{AnySearchBackend, NoopSearchService, SqlSearchService};
|
||||||
use kitsune_storage::{fs::Storage as FsStorage, s3::Storage as S3Storage, AnyStorageBackend};
|
use kitsune_storage::{fs::Storage as FsStorage, s3::Storage as S3Storage, AnyStorageBackend};
|
||||||
use rusty_s3::{Bucket as S3Bucket, Credentials as S3Credentials};
|
use rusty_s3::{Bucket as S3Bucket, Credentials as S3Credentials};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use service::custom_emoji::CustomEmojiService;
|
||||||
use service::search::SearchService;
|
use service::search::SearchService;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
|
@ -272,6 +274,12 @@ pub async fn prepare_state(
|
||||||
let captcha_backend = config.captcha.as_ref().map(prepare_captcha);
|
let captcha_backend = config.captcha.as_ref().map(prepare_captcha);
|
||||||
let captcha_service = CaptchaService::builder().backend(captcha_backend).build();
|
let captcha_service = CaptchaService::builder().backend(captcha_backend).build();
|
||||||
|
|
||||||
|
let custom_emoji_service = CustomEmojiService::builder()
|
||||||
|
.attachment_service(attachment_service.clone())
|
||||||
|
.db_pool(db_pool.clone())
|
||||||
|
.url_service(url_service.clone())
|
||||||
|
.build();
|
||||||
|
|
||||||
let instance_service = InstanceService::builder()
|
let instance_service = InstanceService::builder()
|
||||||
.db_pool(db_pool.clone())
|
.db_pool(db_pool.clone())
|
||||||
.name(config.instance.name.as_str())
|
.name(config.instance.name.as_str())
|
||||||
|
@ -292,6 +300,7 @@ pub async fn prepare_state(
|
||||||
|
|
||||||
let post_resolver = PostResolver::builder()
|
let post_resolver = PostResolver::builder()
|
||||||
.account(account_service.clone())
|
.account(account_service.clone())
|
||||||
|
.custom_emoji(custom_emoji_service.clone())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let post_service = PostService::builder()
|
let post_service = PostService::builder()
|
||||||
|
@ -344,6 +353,7 @@ pub async fn prepare_state(
|
||||||
service: Service {
|
service: Service {
|
||||||
account: account_service,
|
account: account_service,
|
||||||
captcha: captcha_service,
|
captcha: captcha_service,
|
||||||
|
custom_emoji: custom_emoji_service,
|
||||||
federation_filter: federation_filter_service,
|
federation_filter: federation_filter_service,
|
||||||
instance: instance_service,
|
instance: instance_service,
|
||||||
job: job_service,
|
job: job_service,
|
||||||
|
|
|
@ -4,21 +4,23 @@ use crate::{
|
||||||
try_join,
|
try_join,
|
||||||
util::BaseToCc,
|
util::BaseToCc,
|
||||||
};
|
};
|
||||||
use diesel::{BelongingToDsl, QueryDsl, SelectableHelper};
|
use diesel::{BelongingToDsl, ExpressionMethods, QueryDsl, SelectableHelper};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use futures_util::{future::OptionFuture, FutureExt, TryFutureExt, TryStreamExt};
|
use futures_util::{future::OptionFuture, FutureExt, TryFutureExt, TryStreamExt};
|
||||||
use kitsune_db::{
|
use kitsune_db::{
|
||||||
model::{
|
model::{
|
||||||
account::Account,
|
account::Account,
|
||||||
|
custom_emoji::{CustomEmoji, PostCustomEmoji},
|
||||||
media_attachment::{MediaAttachment as DbMediaAttachment, PostMediaAttachment},
|
media_attachment::{MediaAttachment as DbMediaAttachment, PostMediaAttachment},
|
||||||
mention::Mention,
|
mention::Mention,
|
||||||
post::Post,
|
post::Post,
|
||||||
},
|
},
|
||||||
schema::{accounts, media_attachments, posts},
|
schema::{accounts, custom_emojis, media_attachments, posts, posts_custom_emojis},
|
||||||
};
|
};
|
||||||
use kitsune_type::ap::{
|
use kitsune_type::ap::{
|
||||||
actor::{Actor, PublicKey},
|
actor::{Actor, PublicKey},
|
||||||
ap_context,
|
ap_context,
|
||||||
|
emoji::Emoji,
|
||||||
object::{MediaAttachment, MediaAttachmentType},
|
object::{MediaAttachment, MediaAttachmentType},
|
||||||
AttributedToField, Object, ObjectType, Tag, TagType,
|
AttributedToField, Object, ObjectType, Tag, TagType,
|
||||||
};
|
};
|
||||||
|
@ -56,6 +58,42 @@ impl IntoObject for DbMediaAttachment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_post_tags(
|
||||||
|
mentions: Vec<(Mention, Account)>,
|
||||||
|
to: &mut Vec<String>,
|
||||||
|
emojis: Vec<(CustomEmoji, PostCustomEmoji, DbMediaAttachment)>,
|
||||||
|
) -> Vec<Tag> {
|
||||||
|
let mut tag = Vec::new();
|
||||||
|
for (mention, mentioned) in mentions {
|
||||||
|
to.push(mentioned.url.clone());
|
||||||
|
tag.push(Tag {
|
||||||
|
id: None,
|
||||||
|
r#type: TagType::Mention,
|
||||||
|
name: mention.mention_text,
|
||||||
|
href: Some(mentioned.url),
|
||||||
|
icon: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (custom_emoji, post_emoji, attachment) in emojis {
|
||||||
|
if let Some(attachment_url) = attachment.remote_url {
|
||||||
|
tag.push(Tag {
|
||||||
|
id: Some(custom_emoji.remote_id),
|
||||||
|
r#type: TagType::Emoji,
|
||||||
|
name: post_emoji.emoji_text,
|
||||||
|
href: None,
|
||||||
|
icon: Some(MediaAttachment {
|
||||||
|
r#type: MediaAttachmentType::Image,
|
||||||
|
name: None,
|
||||||
|
media_type: Some(attachment.content_type),
|
||||||
|
blurhash: None,
|
||||||
|
url: attachment_url,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoObject for Post {
|
impl IntoObject for Post {
|
||||||
type Output = Object;
|
type Output = Object;
|
||||||
|
|
||||||
|
@ -67,7 +105,7 @@ impl IntoObject for Post {
|
||||||
return Err(ApiError::NotFound.into());
|
return Err(ApiError::NotFound.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let (account, in_reply_to, mentions, attachment_stream) = state
|
let (account, in_reply_to, mentions, emojis, attachment_stream) = state
|
||||||
.db_pool
|
.db_pool
|
||||||
.with_connection(|db_conn| {
|
.with_connection(|db_conn| {
|
||||||
async {
|
async {
|
||||||
|
@ -90,6 +128,17 @@ impl IntoObject for Post {
|
||||||
.select((Mention::as_select(), Account::as_select()))
|
.select((Mention::as_select(), Account::as_select()))
|
||||||
.load::<(Mention, Account)>(db_conn);
|
.load::<(Mention, Account)>(db_conn);
|
||||||
|
|
||||||
|
let custom_emojis_fut = custom_emojis::table
|
||||||
|
.inner_join(posts_custom_emojis::table)
|
||||||
|
.inner_join(media_attachments::table)
|
||||||
|
.filter(posts_custom_emojis::post_id.eq(self.id))
|
||||||
|
.select((
|
||||||
|
CustomEmoji::as_select(),
|
||||||
|
PostCustomEmoji::as_select(),
|
||||||
|
DbMediaAttachment::as_select(),
|
||||||
|
))
|
||||||
|
.load::<(CustomEmoji, PostCustomEmoji, DbMediaAttachment)>(db_conn);
|
||||||
|
|
||||||
let attachment_stream_fut = PostMediaAttachment::belonging_to(&self)
|
let attachment_stream_fut = PostMediaAttachment::belonging_to(&self)
|
||||||
.inner_join(media_attachments::table)
|
.inner_join(media_attachments::table)
|
||||||
.select(DbMediaAttachment::as_select())
|
.select(DbMediaAttachment::as_select())
|
||||||
|
@ -99,6 +148,7 @@ impl IntoObject for Post {
|
||||||
account_fut,
|
account_fut,
|
||||||
in_reply_to_fut,
|
in_reply_to_fut,
|
||||||
mentions_fut,
|
mentions_fut,
|
||||||
|
custom_emojis_fut,
|
||||||
attachment_stream_fut
|
attachment_stream_fut
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -122,17 +172,9 @@ impl IntoObject for Post {
|
||||||
.try_collect()
|
.try_collect()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut tag = Vec::new();
|
|
||||||
let (mut to, cc) = self.visibility.base_to_cc(state, &account);
|
let (mut to, cc) = self.visibility.base_to_cc(state, &account);
|
||||||
for (mention, mentioned) in mentions {
|
let tag = build_post_tags(mentions, &mut to, emojis);
|
||||||
to.push(mentioned.url.clone());
|
|
||||||
tag.push(Tag {
|
|
||||||
r#type: TagType::Mention,
|
|
||||||
name: mention.mention_text,
|
|
||||||
href: Some(mentioned.url),
|
|
||||||
icon: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let account_url = state.service.url.user_url(account.id);
|
let account_url = state.service.url.user_url(account.id);
|
||||||
|
|
||||||
Ok(Object {
|
Ok(Object {
|
||||||
|
@ -219,3 +261,36 @@ impl IntoObject for Account {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoObject for CustomEmoji {
|
||||||
|
type Output = Emoji;
|
||||||
|
|
||||||
|
async fn into_object(self, state: &State) -> Result<Self::Output> {
|
||||||
|
// Officially we don't have any info about remote emojis as we're not the origin
|
||||||
|
// Let's pretend we're not home and do not answer
|
||||||
|
let name = match self.domain {
|
||||||
|
None => Ok(format!(":{}:", self.shortcode)),
|
||||||
|
Some(_) => Err(ApiError::NotFound),
|
||||||
|
}?;
|
||||||
|
let icon = state
|
||||||
|
.db_pool
|
||||||
|
.with_connection(|db_conn| {
|
||||||
|
media_attachments::table
|
||||||
|
.find(self.media_attachment_id)
|
||||||
|
.get_result::<DbMediaAttachment>(db_conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
.and_then(|media_attachment| media_attachment.into_object(state))
|
||||||
|
.scoped()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Emoji {
|
||||||
|
context: ap_context(),
|
||||||
|
id: self.remote_id,
|
||||||
|
r#type: String::from("Emoji"),
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
updated: self.updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,9 +9,11 @@ use diesel::{
|
||||||
};
|
};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use futures_util::{future::OptionFuture, FutureExt, TryFutureExt, TryStreamExt};
|
use futures_util::{future::OptionFuture, FutureExt, TryFutureExt, TryStreamExt};
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
use kitsune_db::{
|
use kitsune_db::{
|
||||||
model::{
|
model::{
|
||||||
account::Account as DbAccount,
|
account::Account as DbAccount,
|
||||||
|
custom_emoji::{CustomEmoji as DbCustomEmoji, PostCustomEmoji as DbPostCustomEmoji},
|
||||||
favourite::Favourite as DbFavourite,
|
favourite::Favourite as DbFavourite,
|
||||||
follower::Follow,
|
follower::Follow,
|
||||||
link_preview::LinkPreview,
|
link_preview::LinkPreview,
|
||||||
|
@ -23,7 +25,8 @@ use kitsune_db::{
|
||||||
post::{Post as DbPost, PostSource},
|
post::{Post as DbPost, PostSource},
|
||||||
},
|
},
|
||||||
schema::{
|
schema::{
|
||||||
accounts, accounts_follows, media_attachments, notifications, posts, posts_favourites,
|
accounts, accounts_follows, custom_emojis, media_attachments, notifications, posts,
|
||||||
|
posts_favourites,
|
||||||
},
|
},
|
||||||
PgPool,
|
PgPool,
|
||||||
};
|
};
|
||||||
|
@ -35,7 +38,7 @@ use kitsune_type::mastodon::{
|
||||||
preview_card::PreviewType,
|
preview_card::PreviewType,
|
||||||
relationship::Relationship,
|
relationship::Relationship,
|
||||||
status::{Mention, StatusSource},
|
status::{Mention, StatusSource},
|
||||||
Account, MediaAttachment, Notification, PreviewCard, Status,
|
Account, CustomEmoji, MediaAttachment, Notification, PreviewCard, Status,
|
||||||
};
|
};
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use scoped_futures::ScopedFutureExt;
|
use scoped_futures::ScopedFutureExt;
|
||||||
|
@ -332,56 +335,69 @@ impl IntoMastodon for DbPost {
|
||||||
state: MapperState<'_>,
|
state: MapperState<'_>,
|
||||||
) -> impl Future<Output = Result<Self::Output>> + Send {
|
) -> impl Future<Output = Result<Self::Output>> + Send {
|
||||||
async move {
|
async move {
|
||||||
let (account, reblog_count, favourites_count, media_attachments, mentions_stream) =
|
let (
|
||||||
state
|
account,
|
||||||
.db_pool
|
reblog_count,
|
||||||
.with_connection(|db_conn| {
|
favourites_count,
|
||||||
async {
|
media_attachments,
|
||||||
let account_fut = accounts::table
|
mentions_stream,
|
||||||
.find(self.account_id)
|
custom_emojis_stream,
|
||||||
.select(DbAccount::as_select())
|
) = state
|
||||||
.get_result::<DbAccount>(db_conn)
|
.db_pool
|
||||||
.map_err(Error::from)
|
.with_connection(|db_conn| {
|
||||||
.and_then(|db_account| db_account.into_mastodon(state));
|
async {
|
||||||
|
let account_fut = accounts::table
|
||||||
|
.find(self.account_id)
|
||||||
|
.select(DbAccount::as_select())
|
||||||
|
.get_result::<DbAccount>(db_conn)
|
||||||
|
.map_err(Error::from)
|
||||||
|
.and_then(|db_account| db_account.into_mastodon(state));
|
||||||
|
|
||||||
let reblog_count_fut = posts::table
|
let reblog_count_fut = posts::table
|
||||||
.filter(posts::reposted_post_id.eq(self.id))
|
.filter(posts::reposted_post_id.eq(self.id))
|
||||||
.count()
|
.count()
|
||||||
.get_result::<i64>(db_conn)
|
.get_result::<i64>(db_conn)
|
||||||
.map_err(Error::from);
|
.map_err(Error::from);
|
||||||
|
|
||||||
let favourites_count_fut = DbFavourite::belonging_to(&self)
|
let favourites_count_fut = DbFavourite::belonging_to(&self)
|
||||||
.count()
|
.count()
|
||||||
.get_result::<i64>(db_conn)
|
.get_result::<i64>(db_conn)
|
||||||
.map_err(Error::from);
|
.map_err(Error::from);
|
||||||
|
|
||||||
let media_attachments_fut = DbPostMediaAttachment::belonging_to(&self)
|
let media_attachments_fut = DbPostMediaAttachment::belonging_to(&self)
|
||||||
.inner_join(media_attachments::table)
|
.inner_join(media_attachments::table)
|
||||||
.select(DbMediaAttachment::as_select())
|
.select(DbMediaAttachment::as_select())
|
||||||
.load_stream::<DbMediaAttachment>(db_conn)
|
.load_stream::<DbMediaAttachment>(db_conn)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
.and_then(|attachment_stream| {
|
.and_then(|attachment_stream| {
|
||||||
attachment_stream
|
attachment_stream
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
.and_then(|attachment| attachment.into_mastodon(state))
|
.and_then(|attachment| attachment.into_mastodon(state))
|
||||||
.try_collect()
|
.try_collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
let mentions_stream_fut = DbMention::belonging_to(&self)
|
let mentions_stream_fut = DbMention::belonging_to(&self)
|
||||||
.load_stream::<DbMention>(db_conn)
|
.load_stream::<DbMention>(db_conn)
|
||||||
.map_err(Error::from);
|
.map_err(Error::from);
|
||||||
|
|
||||||
try_join!(
|
let custom_emojis_stream_fut = DbPostCustomEmoji::belonging_to(&self)
|
||||||
account_fut,
|
.inner_join(custom_emojis::table.inner_join(media_attachments::table))
|
||||||
reblog_count_fut,
|
.select((DbCustomEmoji::as_select(), DbMediaAttachment::as_select()))
|
||||||
favourites_count_fut,
|
.load_stream::<(DbCustomEmoji, DbMediaAttachment)>(db_conn)
|
||||||
media_attachments_fut,
|
.map_err(Error::from);
|
||||||
mentions_stream_fut,
|
|
||||||
)
|
try_join!(
|
||||||
}
|
account_fut,
|
||||||
.scoped()
|
reblog_count_fut,
|
||||||
})
|
favourites_count_fut,
|
||||||
.await?;
|
media_attachments_fut,
|
||||||
|
mentions_stream_fut,
|
||||||
|
custom_emojis_stream_fut
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.scoped()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
let link_preview = OptionFuture::from(
|
let link_preview = OptionFuture::from(
|
||||||
self.link_preview_url
|
self.link_preview_url
|
||||||
|
@ -402,6 +418,12 @@ impl IntoMastodon for DbPost {
|
||||||
.try_collect()
|
.try_collect()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let emojis = custom_emojis_stream
|
||||||
|
.map_err(Error::from)
|
||||||
|
.and_then(|(emoji, attachment)| (emoji, attachment, None).into_mastodon(state))
|
||||||
|
.try_collect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let reblog = state
|
let reblog = state
|
||||||
.db_pool
|
.db_pool
|
||||||
.with_connection(|db_conn| {
|
.with_connection(|db_conn| {
|
||||||
|
@ -447,6 +469,7 @@ impl IntoMastodon for DbPost {
|
||||||
account,
|
account,
|
||||||
media_attachments,
|
media_attachments,
|
||||||
mentions,
|
mentions,
|
||||||
|
emojis,
|
||||||
reblog,
|
reblog,
|
||||||
favourited: false,
|
favourited: false,
|
||||||
reblogged: false,
|
reblogged: false,
|
||||||
|
@ -588,3 +611,37 @@ impl IntoMastodon for DbNotification {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IntoMastodon for (DbCustomEmoji, DbMediaAttachment, Option<Timestamp>) {
|
||||||
|
type Output = CustomEmoji;
|
||||||
|
|
||||||
|
fn id(&self) -> Option<Uuid> {
|
||||||
|
Some(self.0.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn into_mastodon(self, state: MapperState<'_>) -> Result<Self::Output> {
|
||||||
|
let (emoji, attachment, last_used) = self;
|
||||||
|
let shortcode = if let Some(ref domain) = emoji.domain {
|
||||||
|
format!(":{}@{}:", emoji.shortcode, domain)
|
||||||
|
} else {
|
||||||
|
format!(":{}:", emoji.shortcode)
|
||||||
|
};
|
||||||
|
let url = state.url_service.media_url(attachment.id);
|
||||||
|
let category = if last_used.is_some() {
|
||||||
|
Some(String::from("recently used"))
|
||||||
|
} else if emoji.endorsed {
|
||||||
|
Some(String::from("endorsed"))
|
||||||
|
} else if emoji.domain.is_none() {
|
||||||
|
Some(String::from("local"))
|
||||||
|
} else {
|
||||||
|
Some(emoji.domain.unwrap())
|
||||||
|
};
|
||||||
|
Ok(CustomEmoji {
|
||||||
|
shortcode,
|
||||||
|
url: url.clone(),
|
||||||
|
static_url: url,
|
||||||
|
visible_in_picker: true,
|
||||||
|
category,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, Result},
|
error::{Error, Result},
|
||||||
service::account::{AccountService, GetUser},
|
service::{
|
||||||
|
account::{AccountService, GetUser},
|
||||||
|
custom_emoji::{CustomEmojiService, GetEmoji},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use post_process::{BoxError, Element, Html, Render};
|
use post_process::{BoxError, Element, Html, Render};
|
||||||
use speedy_uuid::Uuid;
|
use speedy_uuid::Uuid;
|
||||||
|
@ -10,6 +13,13 @@ use typed_builder::TypedBuilder;
|
||||||
#[derive(Clone, TypedBuilder)]
|
#[derive(Clone, TypedBuilder)]
|
||||||
pub struct PostResolver {
|
pub struct PostResolver {
|
||||||
account: AccountService,
|
account: AccountService,
|
||||||
|
custom_emoji: CustomEmojiService,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ResolvedPost {
|
||||||
|
pub mentioned_accounts: Vec<(Uuid, String)>,
|
||||||
|
pub custom_emojis: Vec<(Uuid, String)>,
|
||||||
|
pub content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostResolver {
|
impl PostResolver {
|
||||||
|
@ -17,6 +27,7 @@ impl PostResolver {
|
||||||
&self,
|
&self,
|
||||||
element: Element<'a>,
|
element: Element<'a>,
|
||||||
mentioned_accounts: mpsc::Sender<(Uuid, String)>,
|
mentioned_accounts: mpsc::Sender<(Uuid, String)>,
|
||||||
|
custom_emojis: mpsc::Sender<(Uuid, String)>,
|
||||||
) -> Result<Element<'a>, BoxError> {
|
) -> Result<Element<'a>, BoxError> {
|
||||||
let element = match element {
|
let element = match element {
|
||||||
Element::Mention(mention) => {
|
Element::Mention(mention) => {
|
||||||
|
@ -47,6 +58,19 @@ impl PostResolver {
|
||||||
attributes: vec![(Cow::Borrowed("href"), link.content.clone())],
|
attributes: vec![(Cow::Borrowed("href"), link.content.clone())],
|
||||||
content: Box::new(Element::Link(link)),
|
content: Box::new(Element::Link(link)),
|
||||||
}),
|
}),
|
||||||
|
Element::Emote(emote) => {
|
||||||
|
let get_emoji = GetEmoji::builder()
|
||||||
|
.shortcode(&emote.shortcode)
|
||||||
|
.domain(emote.domain.as_deref())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
if let Some(emoji) = self.custom_emoji.get(get_emoji).await? {
|
||||||
|
let mut emoji_text = String::new();
|
||||||
|
Element::Emote(emote.clone()).render(&mut emoji_text);
|
||||||
|
let _ = custom_emojis.send((emoji.id, emoji_text));
|
||||||
|
}
|
||||||
|
Element::Emote(emote)
|
||||||
|
}
|
||||||
elem => elem,
|
elem => elem,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -60,16 +84,25 @@ impl PostResolver {
|
||||||
/// - List of mentioned accounts, represented as `(Account ID, Mention text)`
|
/// - List of mentioned accounts, represented as `(Account ID, Mention text)`
|
||||||
/// - Content with the mentions replaced by links
|
/// - Content with the mentions replaced by links
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub async fn resolve(&self, content: &str) -> Result<(Vec<(Uuid, String)>, String)> {
|
pub async fn resolve(&self, content: &str) -> Result<ResolvedPost> {
|
||||||
let (mentioned_account_ids_acc, mentioned_account_ids) = mpsc::channel();
|
let (mentioned_account_ids_acc, mentioned_account_ids) = mpsc::channel();
|
||||||
|
let (custom_emoji_ids_sen, custom_emoji_ids_rec) = mpsc::channel();
|
||||||
|
|
||||||
let content = post_process::transform(content, |elem| {
|
let content = post_process::transform(content, |elem| {
|
||||||
self.transform(elem, mentioned_account_ids_acc.clone())
|
self.transform(
|
||||||
|
elem,
|
||||||
|
mentioned_account_ids_acc.clone(),
|
||||||
|
custom_emoji_ids_sen.clone(),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(Error::PostProcessing)?;
|
.map_err(Error::PostProcessing)?;
|
||||||
|
|
||||||
Ok((mentioned_account_ids.try_iter().collect(), content))
|
Ok(ResolvedPost {
|
||||||
|
mentioned_accounts: mentioned_account_ids.try_iter().collect(),
|
||||||
|
custom_emojis: custom_emoji_ids_rec.try_iter().collect(),
|
||||||
|
content,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,8 +114,10 @@ mod test {
|
||||||
job::KitsuneContextRepo,
|
job::KitsuneContextRepo,
|
||||||
service::{
|
service::{
|
||||||
account::AccountService, attachment::AttachmentService,
|
account::AccountService, attachment::AttachmentService,
|
||||||
federation_filter::FederationFilterService, job::JobService, url::UrlService,
|
custom_emoji::CustomEmojiService, federation_filter::FederationFilterService,
|
||||||
|
job::JobService, url::UrlService,
|
||||||
},
|
},
|
||||||
|
try_join,
|
||||||
webfinger::Webfinger,
|
webfinger::Webfinger,
|
||||||
};
|
};
|
||||||
use athena::JobQueue;
|
use athena::JobQueue;
|
||||||
|
@ -90,24 +125,31 @@ mod test {
|
||||||
use diesel::{QueryDsl, SelectableHelper};
|
use diesel::{QueryDsl, SelectableHelper};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
use kitsune_cache::NoopCache;
|
use kitsune_cache::NoopCache;
|
||||||
use kitsune_config::instance::FederationFilterConfiguration;
|
use kitsune_config::instance::FederationFilterConfiguration;
|
||||||
use kitsune_db::{model::account::Account, schema::accounts};
|
use kitsune_db::{
|
||||||
|
model::{
|
||||||
|
account::Account, custom_emoji::CustomEmoji, media_attachment::NewMediaAttachment,
|
||||||
|
},
|
||||||
|
schema::{accounts, custom_emojis, media_attachments},
|
||||||
|
};
|
||||||
use kitsune_http_client::Client;
|
use kitsune_http_client::Client;
|
||||||
use kitsune_search::NoopSearchService;
|
use kitsune_search::NoopSearchService;
|
||||||
use kitsune_storage::fs::Storage as FsStorage;
|
use kitsune_storage::fs::Storage as FsStorage;
|
||||||
use kitsune_test::{build_ap_response, database_test, redis_test};
|
use kitsune_test::{build_ap_response, database_test, redis_test};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use scoped_futures::ScopedFutureExt;
|
use scoped_futures::ScopedFutureExt;
|
||||||
|
use speedy_uuid::Uuid;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tower::service_fn;
|
use tower::service_fn;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial_test::serial]
|
#[serial_test::serial]
|
||||||
async fn parse_mentions() {
|
async fn parse_post() {
|
||||||
redis_test(|redis_pool| async move {
|
redis_test(|redis_pool| async move {
|
||||||
database_test(|db_pool| async move {
|
database_test(|db_pool| async move {
|
||||||
let post = "Hello @0x0@corteximplant.com! How are you doing?";
|
let post = "Hello @0x0@corteximplant.com! How are you doing? :blobhaj_happy: :blobhaj_sad@example.com:";
|
||||||
|
|
||||||
let client = service_fn(|req: Request<_>| async move {
|
let client = service_fn(|req: Request<_>| async move {
|
||||||
match req.uri().path_and_query().unwrap().as_str() {
|
match req.uri().path_and_query().unwrap().as_str() {
|
||||||
|
@ -158,13 +200,13 @@ mod test {
|
||||||
|
|
||||||
let attachment_service = AttachmentService::builder()
|
let attachment_service = AttachmentService::builder()
|
||||||
.db_pool(db_pool.clone())
|
.db_pool(db_pool.clone())
|
||||||
.media_proxy_enabled(false)
|
.media_proxy_enabled(true)
|
||||||
.storage_backend(FsStorage::new("uploads".into()))
|
.storage_backend(FsStorage::new("uploads".into()))
|
||||||
.url_service(url_service.clone())
|
.url_service(url_service.clone())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let account_service = AccountService::builder()
|
let account_service = AccountService::builder()
|
||||||
.attachment_service(attachment_service)
|
.attachment_service(attachment_service.clone())
|
||||||
.db_pool(db_pool.clone())
|
.db_pool(db_pool.clone())
|
||||||
.fetcher(fetcher)
|
.fetcher(fetcher)
|
||||||
.job_service(job_service)
|
.job_service(job_service)
|
||||||
|
@ -172,19 +214,93 @@ mod test {
|
||||||
.webfinger(webfinger)
|
.webfinger(webfinger)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let mention_resolver = PostResolver::builder()
|
let custom_emoji_service = CustomEmojiService::builder()
|
||||||
.account(account_service)
|
.attachment_service(attachment_service.clone())
|
||||||
|
.db_pool(db_pool.clone())
|
||||||
|
.url_service(url_service.clone())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let (mentioned_account_ids, content) = mention_resolver
|
let emoji_ids = (Uuid::now_v7(), Uuid::now_v7());
|
||||||
|
let media_attachment_ids = (Uuid::now_v7(), Uuid::now_v7());
|
||||||
|
db_pool
|
||||||
|
.with_connection(|db_conn| {
|
||||||
|
async {
|
||||||
|
let media_fut = diesel::insert_into(media_attachments::table)
|
||||||
|
.values(NewMediaAttachment {
|
||||||
|
id: media_attachment_ids.0,
|
||||||
|
content_type: "image/jpeg",
|
||||||
|
account_id: None,
|
||||||
|
description: None,
|
||||||
|
blurhash: None,
|
||||||
|
file_path: None,
|
||||||
|
remote_url: None,
|
||||||
|
})
|
||||||
|
.execute(db_conn);
|
||||||
|
let emoji_fut = diesel::insert_into(custom_emojis::table)
|
||||||
|
.values(CustomEmoji {
|
||||||
|
id: emoji_ids.0,
|
||||||
|
shortcode: String::from("blobhaj_happy"),
|
||||||
|
domain: None,
|
||||||
|
remote_id: String::from("https://local.domain/emoji/blobhaj_happy"),
|
||||||
|
media_attachment_id: media_attachment_ids.0,
|
||||||
|
endorsed: false,
|
||||||
|
created_at: Timestamp::now_utc(),
|
||||||
|
updated_at: Timestamp::now_utc()
|
||||||
|
})
|
||||||
|
.execute(db_conn);
|
||||||
|
try_join!(media_fut, emoji_fut)
|
||||||
|
}.scoped()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert the local emoji");
|
||||||
|
|
||||||
|
db_pool
|
||||||
|
.with_connection(|db_conn| {
|
||||||
|
async {
|
||||||
|
let media_fut = diesel::insert_into(media_attachments::table)
|
||||||
|
.values(NewMediaAttachment {
|
||||||
|
id: media_attachment_ids.1,
|
||||||
|
content_type: "image/jpeg",
|
||||||
|
account_id: None,
|
||||||
|
description: None,
|
||||||
|
blurhash: None,
|
||||||
|
file_path: None,
|
||||||
|
remote_url: Some("https://media.example.com/emojis/blobhaj.jpeg"),
|
||||||
|
})
|
||||||
|
.execute(db_conn);
|
||||||
|
let emoji_fut = diesel::insert_into(custom_emojis::table)
|
||||||
|
.values(CustomEmoji {
|
||||||
|
id: emoji_ids.1,
|
||||||
|
shortcode: String::from("blobhaj_sad"),
|
||||||
|
domain: Some(String::from("example.com")),
|
||||||
|
remote_id: String::from("https://example.com/emojis/1"),
|
||||||
|
media_attachment_id: media_attachment_ids.1,
|
||||||
|
endorsed: false,
|
||||||
|
created_at: Timestamp::now_utc(),
|
||||||
|
updated_at: Timestamp::now_utc(),
|
||||||
|
})
|
||||||
|
.execute(db_conn);
|
||||||
|
try_join!(media_fut, emoji_fut)
|
||||||
|
}.scoped()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Failed to insert the remote emoji");
|
||||||
|
|
||||||
|
let post_resolver = PostResolver::builder()
|
||||||
|
.account(account_service)
|
||||||
|
.custom_emoji(custom_emoji_service)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let resolved = post_resolver
|
||||||
.resolve(post)
|
.resolve(post)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to resolve mentions");
|
.expect("Failed to resolve the post");
|
||||||
|
|
||||||
assert_eq!(content, "Hello <a class=\"mention\" href=\"https://corteximplant.com/users/0x0\">@0x0@corteximplant.com</a>! How are you doing?");
|
assert_eq!(resolved.content, "Hello <a class=\"mention\" href=\"https://corteximplant.com/users/0x0\">@0x0@corteximplant.com</a>! How are you doing? :blobhaj_happy: :blobhaj_sad__example_com:");
|
||||||
assert_eq!(mentioned_account_ids.len(), 1);
|
assert_eq!(resolved.mentioned_accounts.len(), 1);
|
||||||
|
assert_eq!(resolved.custom_emojis.len(), 2);
|
||||||
|
|
||||||
let (account_id, _mention_text) = &mentioned_account_ids[0];
|
let (account_id, _mention_text) = &resolved.mentioned_accounts[0];
|
||||||
let mentioned_account = db_pool
|
let mentioned_account = db_pool
|
||||||
.with_connection(|db_conn| {
|
.with_connection(|db_conn| {
|
||||||
accounts::table
|
accounts::table
|
||||||
|
@ -202,6 +318,9 @@ mod test {
|
||||||
mentioned_account.url,
|
mentioned_account.url,
|
||||||
"https://corteximplant.com/users/0x0"
|
"https://corteximplant.com/users/0x0"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(resolved.custom_emojis[0], (emoji_ids.1, String::from(":blobhaj_sad__example_com:")));
|
||||||
|
assert_eq!(resolved.custom_emojis[1], (emoji_ids.0, String::from(":blobhaj_happy:")));
|
||||||
}).await;
|
}).await;
|
||||||
}).await;
|
}).await;
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,9 @@ pub struct Update {
|
||||||
#[derive(Builder, Validate)]
|
#[derive(Builder, Validate)]
|
||||||
#[builder(pattern = "owned")]
|
#[builder(pattern = "owned")]
|
||||||
pub struct Upload<S> {
|
pub struct Upload<S> {
|
||||||
|
#[builder(default, setter(strip_option))]
|
||||||
#[garde(skip)]
|
#[garde(skip)]
|
||||||
account_id: Uuid,
|
account_id: Option<Uuid>,
|
||||||
#[garde(custom(is_allowed_filetype))]
|
#[garde(custom(is_allowed_filetype))]
|
||||||
content_type: String,
|
content_type: String,
|
||||||
#[builder(default, setter(strip_option))]
|
#[builder(default, setter(strip_option))]
|
||||||
|
@ -115,10 +116,10 @@ impl AttachmentService {
|
||||||
pub async fn get_url(&self, id: Uuid) -> Result<String> {
|
pub async fn get_url(&self, id: Uuid) -> Result<String> {
|
||||||
let media_attachment = self.get_by_id(id).await?;
|
let media_attachment = self.get_by_id(id).await?;
|
||||||
if self.media_proxy_enabled || media_attachment.file_path.is_some() {
|
if self.media_proxy_enabled || media_attachment.file_path.is_some() {
|
||||||
return Ok(self.url_service.media_url(id));
|
return Ok(self.url_service.media_url(media_attachment.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(media_attachment.remote_url.unwrap())
|
Ok(media_attachment.remote_url.as_ref().unwrap().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a stream that yields the file's contents
|
/// Return a stream that yields the file's contents
|
||||||
|
@ -224,8 +225,8 @@ impl AttachmentService {
|
||||||
diesel::insert_into(media_attachments::table)
|
diesel::insert_into(media_attachments::table)
|
||||||
.values(NewMediaAttachment {
|
.values(NewMediaAttachment {
|
||||||
id: Uuid::now_v7(),
|
id: Uuid::now_v7(),
|
||||||
account_id: upload.account_id,
|
|
||||||
content_type: upload.content_type.as_str(),
|
content_type: upload.content_type.as_str(),
|
||||||
|
account_id: upload.account_id,
|
||||||
description: upload.description.as_deref(),
|
description: upload.description.as_deref(),
|
||||||
blurhash: None,
|
blurhash: None,
|
||||||
file_path: Some(upload.path.as_str()),
|
file_path: Some(upload.path.as_str()),
|
||||||
|
@ -324,7 +325,7 @@ mod test {
|
||||||
|
|
||||||
let attachment = MediaAttachment {
|
let attachment = MediaAttachment {
|
||||||
id: Uuid::now_v7(),
|
id: Uuid::now_v7(),
|
||||||
account_id,
|
account_id: Some(account_id),
|
||||||
content_type: String::from("image/jpeg"),
|
content_type: String::from("image/jpeg"),
|
||||||
description: None,
|
description: None,
|
||||||
blurhash: None,
|
blurhash: None,
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
use crate::{
|
||||||
|
consts::MAX_EMOJI_SHORTCODE_LENGTH,
|
||||||
|
error::{BoxError, Error, Result},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bytes::Bytes;
|
||||||
|
use diesel::{
|
||||||
|
BoolExpressionMethods, ExpressionMethods, JoinOnDsl, NullableExpressionMethods,
|
||||||
|
OptionalExtension, QueryDsl, SelectableHelper,
|
||||||
|
};
|
||||||
|
use diesel_async::RunQueryDsl;
|
||||||
|
use futures_util::{Stream, TryStreamExt};
|
||||||
|
use garde::Validate;
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
|
use kitsune_db::{
|
||||||
|
model::{custom_emoji::CustomEmoji, media_attachment::MediaAttachment},
|
||||||
|
schema::{custom_emojis, media_attachments, posts, posts_custom_emojis},
|
||||||
|
PgPool,
|
||||||
|
};
|
||||||
|
use scoped_futures::ScopedFutureExt;
|
||||||
|
use speedy_uuid::Uuid;
|
||||||
|
use typed_builder::TypedBuilder;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
attachment::{AttachmentService, Upload},
|
||||||
|
url::UrlService,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALLOWED_FILETYPES: &[mime::Name<'_>] = &[mime::IMAGE];
|
||||||
|
|
||||||
|
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||||
|
fn is_allowed_filetype(value: &str, _ctx: &()) -> garde::Result {
|
||||||
|
let content_type: mime::Mime = value
|
||||||
|
.parse()
|
||||||
|
.map_err(|err: mime::FromStrError| garde::Error::new(err.to_string()))?;
|
||||||
|
|
||||||
|
if !ALLOWED_FILETYPES.contains(&content_type.type_()) {
|
||||||
|
return Err(garde::Error::new("Invalid file type"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypedBuilder)]
|
||||||
|
pub struct GetEmoji<'a> {
|
||||||
|
shortcode: &'a str,
|
||||||
|
#[builder(default)]
|
||||||
|
domain: Option<&'a str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypedBuilder)]
|
||||||
|
pub struct GetEmojiList {
|
||||||
|
#[builder(default)]
|
||||||
|
fetching_account_id: Option<Uuid>,
|
||||||
|
#[builder(default = 5000)]
|
||||||
|
limit: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(TypedBuilder, Validate)]
|
||||||
|
pub struct EmojiUpload<S> {
|
||||||
|
#[garde(custom(is_allowed_filetype))]
|
||||||
|
content_type: String,
|
||||||
|
#[garde(length(max = MAX_EMOJI_SHORTCODE_LENGTH))]
|
||||||
|
#[garde(pattern("^([a-zA-Z0-9]_?)*[a-zA-Z0-9]$"))]
|
||||||
|
shortcode: String,
|
||||||
|
#[garde(skip)]
|
||||||
|
stream: S,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, TypedBuilder)]
|
||||||
|
pub struct CustomEmojiService {
|
||||||
|
attachment_service: AttachmentService,
|
||||||
|
db_pool: PgPool,
|
||||||
|
url_service: UrlService,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomEmojiService {
|
||||||
|
pub async fn get(&self, get_emoji: GetEmoji<'_>) -> Result<Option<CustomEmoji>> {
|
||||||
|
let mut query = custom_emojis::table
|
||||||
|
.filter(custom_emojis::shortcode.eq(get_emoji.shortcode))
|
||||||
|
.inner_join(media_attachments::table)
|
||||||
|
.select(CustomEmoji::as_select())
|
||||||
|
.into_boxed();
|
||||||
|
|
||||||
|
if let Some(domain) = get_emoji.domain {
|
||||||
|
query = query.filter(custom_emojis::domain.eq(domain));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.db_pool
|
||||||
|
.with_connection(|db_conn| {
|
||||||
|
async move { query.first(db_conn).await.optional() }.scoped()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_id(&self, id: Uuid) -> Result<CustomEmoji> {
|
||||||
|
let query = custom_emojis::table
|
||||||
|
.find(id)
|
||||||
|
.select(CustomEmoji::as_select());
|
||||||
|
|
||||||
|
self.db_pool
|
||||||
|
.with_connection(|db_conn| async move { query.get_result(db_conn).await }.scoped())
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_list(
|
||||||
|
&self,
|
||||||
|
get_emoji_list: GetEmojiList,
|
||||||
|
) -> Result<impl Stream<Item = Result<(CustomEmoji, MediaAttachment, Option<Timestamp>)>> + '_>
|
||||||
|
{
|
||||||
|
let query = custom_emojis::table
|
||||||
|
.left_join(
|
||||||
|
posts_custom_emojis::table.inner_join(
|
||||||
|
posts::table.on(posts::account_id
|
||||||
|
.nullable()
|
||||||
|
.eq(get_emoji_list.fetching_account_id)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.inner_join(media_attachments::table)
|
||||||
|
.filter(
|
||||||
|
posts::account_id.is_null().or(posts::account_id
|
||||||
|
.nullable()
|
||||||
|
.eq(get_emoji_list.fetching_account_id)),
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
custom_emojis::endorsed
|
||||||
|
.eq(true)
|
||||||
|
.or(custom_emojis::domain.is_null())
|
||||||
|
.or(posts::created_at.is_not_null()),
|
||||||
|
)
|
||||||
|
.distinct_on(custom_emojis::id)
|
||||||
|
.select((
|
||||||
|
CustomEmoji::as_select(),
|
||||||
|
MediaAttachment::as_select(),
|
||||||
|
posts::created_at.nullable(),
|
||||||
|
))
|
||||||
|
.limit(get_emoji_list.limit);
|
||||||
|
|
||||||
|
self.db_pool
|
||||||
|
.with_connection(|db_conn| {
|
||||||
|
async move { Ok::<_, Error>(query.load_stream(db_conn).await?.map_err(Error::from)) }
|
||||||
|
.scoped()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_emoji<S>(&self, emoji_upload: EmojiUpload<S>) -> Result<CustomEmoji>
|
||||||
|
where
|
||||||
|
S: Stream<Item = Result<Bytes, BoxError>> + Send + 'static,
|
||||||
|
{
|
||||||
|
emoji_upload.validate(&())?;
|
||||||
|
|
||||||
|
let attachment_upload = Upload::builder()
|
||||||
|
.content_type(emoji_upload.content_type)
|
||||||
|
.stream(emoji_upload.stream)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let attachment = self.attachment_service.upload(attachment_upload).await?;
|
||||||
|
|
||||||
|
let id = Uuid::now_v7();
|
||||||
|
let remote_id = self.url_service.custom_emoji_url(id);
|
||||||
|
|
||||||
|
let custom_emoji = self
|
||||||
|
.db_pool
|
||||||
|
.with_connection(|db_conn| {
|
||||||
|
diesel::insert_into(custom_emojis::table)
|
||||||
|
.values(CustomEmoji {
|
||||||
|
id,
|
||||||
|
remote_id,
|
||||||
|
shortcode: emoji_upload.shortcode,
|
||||||
|
domain: None,
|
||||||
|
media_attachment_id: attachment.id,
|
||||||
|
endorsed: false,
|
||||||
|
created_at: Timestamp::now_utc(),
|
||||||
|
updated_at: Timestamp::now_utc(),
|
||||||
|
})
|
||||||
|
.get_result(db_conn)
|
||||||
|
.scoped()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(custom_emoji)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use crate::consts::API_MAX_LIMIT;
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod attachment;
|
pub mod attachment;
|
||||||
pub mod captcha;
|
pub mod captcha;
|
||||||
|
pub mod custom_emoji;
|
||||||
pub mod federation_filter;
|
pub mod federation_filter;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
pub mod job;
|
pub mod job;
|
||||||
|
|
|
@ -31,6 +31,7 @@ use iso8601_timestamp::Timestamp;
|
||||||
use kitsune_db::{
|
use kitsune_db::{
|
||||||
model::{
|
model::{
|
||||||
account::Account,
|
account::Account,
|
||||||
|
custom_emoji::PostCustomEmoji,
|
||||||
favourite::{Favourite, NewFavourite},
|
favourite::{Favourite, NewFavourite},
|
||||||
media_attachment::NewPostMediaAttachment,
|
media_attachment::NewPostMediaAttachment,
|
||||||
mention::NewMention,
|
mention::NewMention,
|
||||||
|
@ -40,8 +41,9 @@ use kitsune_db::{
|
||||||
},
|
},
|
||||||
post_permission_check::{PermissionCheck, PostPermissionCheckExt},
|
post_permission_check::{PermissionCheck, PostPermissionCheckExt},
|
||||||
schema::{
|
schema::{
|
||||||
accounts, accounts_preferences, media_attachments, notifications, posts, posts_favourites,
|
accounts, accounts_preferences, media_attachments, notifications, posts,
|
||||||
posts_media_attachments, posts_mentions, users_roles,
|
posts_custom_emojis, posts_favourites, posts_media_attachments, posts_mentions,
|
||||||
|
users_roles,
|
||||||
},
|
},
|
||||||
PgPool,
|
PgPool,
|
||||||
};
|
};
|
||||||
|
@ -402,11 +404,39 @@ impl PostService {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn process_custom_emojis(
|
||||||
|
conn: &mut AsyncPgConnection,
|
||||||
|
post_id: Uuid,
|
||||||
|
custom_emojis: Vec<(Uuid, String)>,
|
||||||
|
) -> Result<()> {
|
||||||
|
if custom_emojis.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
diesel::insert_into(posts_custom_emojis::table)
|
||||||
|
.values(
|
||||||
|
custom_emojis
|
||||||
|
.iter()
|
||||||
|
.map(|(emoji_id, emoji_text)| PostCustomEmoji {
|
||||||
|
post_id,
|
||||||
|
custom_emoji_id: *emoji_id,
|
||||||
|
emoji_text: emoji_text.to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<PostCustomEmoji>>(),
|
||||||
|
)
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new post and deliver it to the followers
|
/// Create a new post and deliver it to the followers
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// This should never ever panic. If it does, create a bug report.
|
/// This should never ever panic. If it does, create a bug report.
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
pub async fn create(&self, create_post: CreatePost) -> Result<Post> {
|
pub async fn create(&self, create_post: CreatePost) -> Result<Post> {
|
||||||
create_post.validate(&PostValidationContext {
|
create_post.validate(&PostValidationContext {
|
||||||
character_limit: self.instance_service.character_limit(),
|
character_limit: self.instance_service.character_limit(),
|
||||||
|
@ -432,7 +462,7 @@ impl PostService {
|
||||||
|lang| Language::from_639_1(&lang).unwrap_or_else(|| detect_language(&content)),
|
|lang| Language::from_639_1(&lang).unwrap_or_else(|| detect_language(&content)),
|
||||||
);
|
);
|
||||||
|
|
||||||
let (mentioned_account_ids, content) = self.post_resolver.resolve(&content).await?;
|
let resolved = self.post_resolver.resolve(&content).await?;
|
||||||
let link_preview_url = if let Some(ref embed_client) = self.embed_client {
|
let link_preview_url = if let Some(ref embed_client) = self.embed_client {
|
||||||
embed_client
|
embed_client
|
||||||
.fetch_embed_for_fragment(&content)
|
.fetch_embed_for_fragment(&content)
|
||||||
|
@ -468,7 +498,7 @@ impl PostService {
|
||||||
in_reply_to_id,
|
in_reply_to_id,
|
||||||
reposted_post_id: None,
|
reposted_post_id: None,
|
||||||
subject: subject.as_deref(),
|
subject: subject.as_deref(),
|
||||||
content: content.as_str(),
|
content: resolved.content.as_str(),
|
||||||
content_source: content_source.as_str(),
|
content_source: content_source.as_str(),
|
||||||
content_lang: content_lang.into(),
|
content_lang: content_lang.into(),
|
||||||
link_preview_url: link_preview_url.as_deref(),
|
link_preview_url: link_preview_url.as_deref(),
|
||||||
|
@ -482,8 +512,14 @@ impl PostService {
|
||||||
.get_result(tx)
|
.get_result(tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Self::process_mentions(tx, post.account_id, post.id, mentioned_account_ids)
|
Self::process_mentions(
|
||||||
.await?;
|
tx,
|
||||||
|
post.account_id,
|
||||||
|
post.id,
|
||||||
|
resolved.mentioned_accounts,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Self::process_custom_emojis(tx, post.id, resolved.custom_emojis).await?;
|
||||||
Self::process_media_attachments(tx, post.id, &create_post.media_ids).await?;
|
Self::process_media_attachments(tx, post.id, &create_post.media_ids).await?;
|
||||||
NotificationService::notify_on_new_post(tx, post.account_id, post.id).await?;
|
NotificationService::notify_on_new_post(tx, post.account_id, post.id).await?;
|
||||||
|
|
||||||
|
@ -591,12 +627,16 @@ impl PostService {
|
||||||
.map(|c| kitsune_language::detect_language(DetectionBackend::default(), c)),
|
.map(|c| kitsune_language::detect_language(DetectionBackend::default(), c)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mentioned_account_ids, content) = match content.as_ref() {
|
let (mentioned_account_ids, custom_emojis, content) = match content.as_ref() {
|
||||||
Some(content) => {
|
Some(content) => {
|
||||||
let resolved = self.post_resolver.resolve(content).await?;
|
let resolved = self.post_resolver.resolve(content).await?;
|
||||||
(resolved.0, Some(resolved.1))
|
(
|
||||||
|
resolved.mentioned_accounts,
|
||||||
|
resolved.custom_emojis,
|
||||||
|
Some(resolved.content),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
None => (Vec::new(), None),
|
None => (Vec::new(), Vec::new(), None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let link_preview_url = if let (Some(embed_client), Some(content)) =
|
let link_preview_url = if let (Some(embed_client), Some(content)) =
|
||||||
|
@ -631,6 +671,7 @@ impl PostService {
|
||||||
|
|
||||||
Self::process_mentions(tx, post.account_id, post.id, mentioned_account_ids)
|
Self::process_mentions(tx, post.account_id, post.id, mentioned_account_ids)
|
||||||
.await?;
|
.await?;
|
||||||
|
Self::process_custom_emojis(tx, post.id, custom_emojis).await?;
|
||||||
Self::process_media_attachments(tx, post.id, &update_post.media_ids).await?;
|
Self::process_media_attachments(tx, post.id, &update_post.media_ids).await?;
|
||||||
NotificationService::notify_on_update_post(tx, post.account_id, post.id)
|
NotificationService::notify_on_update_post(tx, post.account_id, post.id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -32,6 +32,11 @@ impl UrlService {
|
||||||
format!("{}/confirm-account/{token}", self.base_url())
|
format!("{}/confirm-account/{token}", self.base_url())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn custom_emoji_url(&self, custom_emoji_id: Uuid) -> String {
|
||||||
|
format!("{}/emojis/{}", self.base_url(), custom_emoji_id)
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn default_avatar_url(&self) -> String {
|
pub fn default_avatar_url(&self) -> String {
|
||||||
format!("{}/public/assets/default-avatar.png", self.base_url())
|
format!("{}/public/assets/default-avatar.png", self.base_url())
|
||||||
|
|
|
@ -3,9 +3,10 @@ use crate::{
|
||||||
event::PostEventEmitter,
|
event::PostEventEmitter,
|
||||||
service::{
|
service::{
|
||||||
account::AccountService, attachment::AttachmentService, captcha::CaptchaService,
|
account::AccountService, attachment::AttachmentService, captcha::CaptchaService,
|
||||||
federation_filter::FederationFilterService, instance::InstanceService, job::JobService,
|
custom_emoji::CustomEmojiService, federation_filter::FederationFilterService,
|
||||||
mailing::MailingService, notification::NotificationService, post::PostService,
|
instance::InstanceService, job::JobService, mailing::MailingService,
|
||||||
search::SearchService, timeline::TimelineService, url::UrlService, user::UserService,
|
notification::NotificationService, post::PostService, search::SearchService,
|
||||||
|
timeline::TimelineService, url::UrlService, user::UserService,
|
||||||
},
|
},
|
||||||
webfinger::Webfinger,
|
webfinger::Webfinger,
|
||||||
};
|
};
|
||||||
|
@ -30,6 +31,7 @@ pub struct Service {
|
||||||
pub account: AccountService,
|
pub account: AccountService,
|
||||||
pub attachment: AttachmentService,
|
pub attachment: AttachmentService,
|
||||||
pub captcha: CaptchaService,
|
pub captcha: CaptchaService,
|
||||||
|
pub custom_emoji: CustomEmojiService,
|
||||||
pub federation_filter: FederationFilterService,
|
pub federation_filter: FederationFilterService,
|
||||||
pub job: JobService,
|
pub job: JobService,
|
||||||
pub mailing: MailingService,
|
pub mailing: MailingService,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
CREATE TABLE media_attachments (
|
CREATE TABLE media_attachments (
|
||||||
id UUID PRIMARY KEY,
|
id UUID PRIMARY KEY,
|
||||||
account_id UUID NOT NULL,
|
account_id UUID,
|
||||||
content_type TEXT NOT NULL,
|
content_type TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
blurhash TEXT,
|
blurhash TEXT,
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
DROP INDEX "idx-custom_emojis-remote_id";
|
||||||
|
DROP INDEX "idx-custom_emojis-shortcode";
|
||||||
|
DROP INDEX "idx-custom_emojis-domain";
|
||||||
|
|
||||||
|
DROP TABLE posts_custom_emojis;
|
||||||
|
DROP TABLE custom_emojis;
|
|
@ -0,0 +1,32 @@
|
||||||
|
CREATE TABLE custom_emojis (
|
||||||
|
id UUID PRIMARY KEY,
|
||||||
|
shortcode TEXT NOT NULL,
|
||||||
|
domain TEXT,
|
||||||
|
remote_id TEXT NOT NULL UNIQUE,
|
||||||
|
media_attachment_id UUID NOT NULL,
|
||||||
|
endorsed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- UNIQUE constraints
|
||||||
|
UNIQUE (shortcode, domain),
|
||||||
|
|
||||||
|
-- Foreign key constraints
|
||||||
|
FOREIGN KEY (media_attachment_id) REFERENCES media_attachments(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE posts_custom_emojis (
|
||||||
|
post_id UUID NOT NULL,
|
||||||
|
custom_emoji_id UUID NOT NULL,
|
||||||
|
emoji_text TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (post_id, custom_emoji_id),
|
||||||
|
|
||||||
|
-- Foreign key constraints
|
||||||
|
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
FOREIGN KEY (custom_emoji_id) REFERENCES custom_emojis(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "idx-custom_emojis-remote_id" ON custom_emojis (remote_id);
|
||||||
|
CREATE INDEX "idx-custom_emojis-shortcode" ON custom_emojis (shortcode);
|
||||||
|
CREATE INDEX "idx-custom_emojis-domain" ON custom_emojis (domain);
|
|
@ -0,0 +1,42 @@
|
||||||
|
use super::post::Post;
|
||||||
|
use crate::schema::posts_custom_emojis;
|
||||||
|
use diesel::{AsChangeset, Associations, Identifiable, Insertable, Queryable, Selectable};
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use speedy_uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::schema::custom_emojis;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, Identifiable, Insertable, Selectable, Queryable)]
|
||||||
|
#[diesel(table_name = custom_emojis)]
|
||||||
|
pub struct CustomEmoji {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub shortcode: String,
|
||||||
|
pub domain: Option<String>,
|
||||||
|
pub remote_id: String,
|
||||||
|
pub media_attachment_id: Uuid,
|
||||||
|
pub endorsed: bool,
|
||||||
|
pub created_at: Timestamp,
|
||||||
|
pub updated_at: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AsChangeset)]
|
||||||
|
#[diesel(table_name = custom_emojis)]
|
||||||
|
pub struct CustomEmojiConflictChangeset {
|
||||||
|
pub media_attachment_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Associations, Clone, Deserialize, Identifiable, Insertable, Queryable, Selectable, Serialize,
|
||||||
|
)]
|
||||||
|
#[diesel(
|
||||||
|
belongs_to(CustomEmoji),
|
||||||
|
belongs_to(Post),
|
||||||
|
primary_key(custom_emoji_id, post_id),
|
||||||
|
table_name = posts_custom_emojis,
|
||||||
|
)]
|
||||||
|
pub struct PostCustomEmoji {
|
||||||
|
pub post_id: Uuid,
|
||||||
|
pub custom_emoji_id: Uuid,
|
||||||
|
pub emoji_text: String,
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ use speedy_uuid::Uuid;
|
||||||
#[diesel(belongs_to(Account), table_name = media_attachments)]
|
#[diesel(belongs_to(Account), table_name = media_attachments)]
|
||||||
pub struct MediaAttachment {
|
pub struct MediaAttachment {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub account_id: Uuid,
|
pub account_id: Option<Uuid>,
|
||||||
pub content_type: String,
|
pub content_type: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub blurhash: Option<String>,
|
pub blurhash: Option<String>,
|
||||||
|
@ -29,7 +29,7 @@ pub struct UpdateMediaAttachment<'a> {
|
||||||
#[diesel(table_name = media_attachments)]
|
#[diesel(table_name = media_attachments)]
|
||||||
pub struct NewMediaAttachment<'a> {
|
pub struct NewMediaAttachment<'a> {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub account_id: Uuid,
|
pub account_id: Option<Uuid>,
|
||||||
pub content_type: &'a str,
|
pub content_type: &'a str,
|
||||||
pub description: Option<&'a str>,
|
pub description: Option<&'a str>,
|
||||||
pub blurhash: Option<&'a str>,
|
pub blurhash: Option<&'a str>,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod account;
|
pub mod account;
|
||||||
|
pub mod custom_emoji;
|
||||||
pub mod favourite;
|
pub mod favourite;
|
||||||
pub mod follower;
|
pub mod follower;
|
||||||
pub mod job_context;
|
pub mod job_context;
|
||||||
|
|
|
@ -267,6 +267,65 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use diesel_full_text_search::Tsvector;
|
||||||
|
|
||||||
|
/// Representation of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
custom_emojis (id) {
|
||||||
|
/// The `id` column of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Uuid`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
id -> Uuid,
|
||||||
|
/// The `shortcode` column of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Text`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
shortcode -> Text,
|
||||||
|
/// The `domain` column of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Nullable<Text>`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
domain -> Nullable<Text>,
|
||||||
|
/// The `remote_id` column of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Text`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
remote_id -> Text,
|
||||||
|
/// The `media_attachment_id` column of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Uuid`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
media_attachment_id -> Uuid,
|
||||||
|
/// The `endorsed` column of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Bool`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
endorsed -> Bool,
|
||||||
|
/// The `created_at` column of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Timestamptz`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
created_at -> Timestamptz,
|
||||||
|
/// The `updated_at` column of the `custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Timestamptz`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
updated_at -> Timestamptz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
use diesel::sql_types::*;
|
use diesel::sql_types::*;
|
||||||
use diesel_full_text_search::Tsvector;
|
use diesel_full_text_search::Tsvector;
|
||||||
|
@ -359,10 +418,10 @@ diesel::table! {
|
||||||
id -> Uuid,
|
id -> Uuid,
|
||||||
/// The `account_id` column of the `media_attachments` table.
|
/// The `account_id` column of the `media_attachments` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Uuid`.
|
/// Its SQL type is `Nullable<Uuid>`.
|
||||||
///
|
///
|
||||||
/// (Automatically generated by Diesel.)
|
/// (Automatically generated by Diesel.)
|
||||||
account_id -> Uuid,
|
account_id -> Nullable<Uuid>,
|
||||||
/// The `content_type` column of the `media_attachments` table.
|
/// The `content_type` column of the `media_attachments` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Text`.
|
/// Its SQL type is `Text`.
|
||||||
|
@ -751,6 +810,35 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
use diesel::sql_types::*;
|
||||||
|
use diesel_full_text_search::Tsvector;
|
||||||
|
|
||||||
|
/// Representation of the `posts_custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
posts_custom_emojis (post_id, custom_emoji_id) {
|
||||||
|
/// The `post_id` column of the `posts_custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Uuid`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
post_id -> Uuid,
|
||||||
|
/// The `custom_emoji_id` column of the `posts_custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Uuid`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
custom_emoji_id -> Uuid,
|
||||||
|
/// The `emoji_text` column of the `posts_custom_emojis` table.
|
||||||
|
///
|
||||||
|
/// Its SQL type is `Text`.
|
||||||
|
///
|
||||||
|
/// (Automatically generated by Diesel.)
|
||||||
|
emoji_text -> Text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
use diesel::sql_types::*;
|
use diesel::sql_types::*;
|
||||||
use diesel_full_text_search::Tsvector;
|
use diesel_full_text_search::Tsvector;
|
||||||
|
@ -963,6 +1051,7 @@ diesel::table! {
|
||||||
}
|
}
|
||||||
|
|
||||||
diesel::joinable!(accounts_preferences -> accounts (account_id));
|
diesel::joinable!(accounts_preferences -> accounts (account_id));
|
||||||
|
diesel::joinable!(custom_emojis -> media_attachments (media_attachment_id));
|
||||||
diesel::joinable!(notifications -> posts (post_id));
|
diesel::joinable!(notifications -> posts (post_id));
|
||||||
diesel::joinable!(oauth2_access_tokens -> oauth2_applications (application_id));
|
diesel::joinable!(oauth2_access_tokens -> oauth2_applications (application_id));
|
||||||
diesel::joinable!(oauth2_access_tokens -> users (user_id));
|
diesel::joinable!(oauth2_access_tokens -> users (user_id));
|
||||||
|
@ -972,6 +1061,8 @@ diesel::joinable!(oauth2_refresh_tokens -> oauth2_access_tokens (access_token));
|
||||||
diesel::joinable!(oauth2_refresh_tokens -> oauth2_applications (application_id));
|
diesel::joinable!(oauth2_refresh_tokens -> oauth2_applications (application_id));
|
||||||
diesel::joinable!(posts -> accounts (account_id));
|
diesel::joinable!(posts -> accounts (account_id));
|
||||||
diesel::joinable!(posts -> link_previews (link_preview_url));
|
diesel::joinable!(posts -> link_previews (link_preview_url));
|
||||||
|
diesel::joinable!(posts_custom_emojis -> custom_emojis (custom_emoji_id));
|
||||||
|
diesel::joinable!(posts_custom_emojis -> posts (post_id));
|
||||||
diesel::joinable!(posts_favourites -> accounts (account_id));
|
diesel::joinable!(posts_favourites -> accounts (account_id));
|
||||||
diesel::joinable!(posts_favourites -> posts (post_id));
|
diesel::joinable!(posts_favourites -> posts (post_id));
|
||||||
diesel::joinable!(posts_media_attachments -> media_attachments (media_attachment_id));
|
diesel::joinable!(posts_media_attachments -> media_attachments (media_attachment_id));
|
||||||
|
@ -985,6 +1076,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
accounts,
|
accounts,
|
||||||
accounts_follows,
|
accounts_follows,
|
||||||
accounts_preferences,
|
accounts_preferences,
|
||||||
|
custom_emojis,
|
||||||
job_context,
|
job_context,
|
||||||
link_previews,
|
link_previews,
|
||||||
media_attachments,
|
media_attachments,
|
||||||
|
@ -994,6 +1086,7 @@ diesel::allow_tables_to_appear_in_same_query!(
|
||||||
oauth2_authorization_codes,
|
oauth2_authorization_codes,
|
||||||
oauth2_refresh_tokens,
|
oauth2_refresh_tokens,
|
||||||
posts,
|
posts,
|
||||||
|
posts_custom_emojis,
|
||||||
posts_favourites,
|
posts_favourites,
|
||||||
posts_media_attachments,
|
posts_media_attachments,
|
||||||
posts_mentions,
|
posts_mentions,
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
use super::object::MediaAttachment;
|
||||||
|
use crate::jsonld::RdfNode;
|
||||||
|
use iso8601_timestamp::Timestamp;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use simd_json::OwnedValue;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Emoji {
|
||||||
|
#[serde(default, rename = "@context")]
|
||||||
|
pub context: OwnedValue,
|
||||||
|
pub id: String,
|
||||||
|
pub r#type: String,
|
||||||
|
pub name: String,
|
||||||
|
pub icon: MediaAttachment,
|
||||||
|
#[serde(default = "Timestamp::now_utc")]
|
||||||
|
pub updated: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RdfNode for Emoji {
|
||||||
|
fn id(&self) -> Option<&str> {
|
||||||
|
Some(&self.id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ pub const PUBLIC_IDENTIFIER: &str = "https://www.w3.org/ns/activitystreams#Publi
|
||||||
|
|
||||||
pub mod actor;
|
pub mod actor;
|
||||||
pub mod collection;
|
pub mod collection;
|
||||||
|
pub mod emoji;
|
||||||
pub mod helper;
|
pub mod helper;
|
||||||
pub mod object;
|
pub mod object;
|
||||||
|
|
||||||
|
@ -210,6 +211,7 @@ pub enum TagType {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct Tag {
|
pub struct Tag {
|
||||||
|
pub id: Option<String>,
|
||||||
pub r#type: TagType,
|
pub r#type: TagType,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub href: Option<String>,
|
pub href: Option<String>,
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub struct CustomEmoji {
|
||||||
|
pub shortcode: String,
|
||||||
|
pub url: String,
|
||||||
|
pub static_url: String,
|
||||||
|
pub visible_in_picker: bool,
|
||||||
|
pub category: Option<String>,
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use speedy_uuid::Uuid;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
pub mod account;
|
pub mod account;
|
||||||
|
pub mod custom_emoji;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
pub mod media_attachment;
|
pub mod media_attachment;
|
||||||
pub mod notification;
|
pub mod notification;
|
||||||
|
@ -12,6 +13,7 @@ pub mod search;
|
||||||
pub mod status;
|
pub mod status;
|
||||||
|
|
||||||
pub use self::account::Account;
|
pub use self::account::Account;
|
||||||
|
pub use self::custom_emoji::CustomEmoji;
|
||||||
pub use self::instance::Instance;
|
pub use self::instance::Instance;
|
||||||
pub use self::media_attachment::MediaAttachment;
|
pub use self::media_attachment::MediaAttachment;
|
||||||
pub use self::notification::Notification;
|
pub use self::notification::Notification;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{Account, MediaAttachment, PreviewCard};
|
use super::{Account, CustomEmoji, MediaAttachment, PreviewCard};
|
||||||
use iso8601_timestamp::Timestamp;
|
use iso8601_timestamp::Timestamp;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use speedy_uuid::Uuid;
|
use speedy_uuid::Uuid;
|
||||||
|
@ -49,6 +49,7 @@ pub struct Status {
|
||||||
pub account: Account,
|
pub account: Account,
|
||||||
pub media_attachments: Vec<MediaAttachment>,
|
pub media_attachments: Vec<MediaAttachment>,
|
||||||
pub mentions: Vec<Mention>,
|
pub mentions: Vec<Mention>,
|
||||||
|
pub emojis: Vec<CustomEmoji>,
|
||||||
pub reblog: Option<Box<Status>>,
|
pub reblog: Option<Box<Status>>,
|
||||||
pub favourited: bool,
|
pub favourited: bool,
|
||||||
pub reblogged: bool,
|
pub reblogged: bool,
|
||||||
|
|
|
@ -44,7 +44,7 @@ impl From<DbMediaAttachment> for MediaAttachment {
|
||||||
fn from(value: DbMediaAttachment) -> Self {
|
fn from(value: DbMediaAttachment) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: value.id,
|
id: value.id,
|
||||||
account_id: value.account_id,
|
account_id: value.account_id.unwrap(),
|
||||||
content_type: value.content_type,
|
content_type: value.content_type,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
blurhash: value.blurhash,
|
blurhash: value.blurhash,
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
use crate::{error::Result, http::responder::ActivityPubJson, state::Zustand};
|
||||||
|
use axum::{debug_handler, extract::Path, extract::State, routing, Router};
|
||||||
|
use kitsune_core::{mapping::IntoObject, service::custom_emoji::CustomEmojiService};
|
||||||
|
use kitsune_type::ap::emoji::Emoji;
|
||||||
|
use speedy_uuid::Uuid;
|
||||||
|
|
||||||
|
#[debug_handler(state = Zustand)]
|
||||||
|
async fn get(
|
||||||
|
State(state): State<Zustand>,
|
||||||
|
State(emoji_service): State<CustomEmojiService>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> Result<ActivityPubJson<Emoji>> {
|
||||||
|
let custom_emoji = emoji_service.get_by_id(id).await?;
|
||||||
|
Ok(ActivityPubJson(
|
||||||
|
custom_emoji.into_object(&state.core).await?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router<Zustand> {
|
||||||
|
Router::new().route("/:id", routing::get(get))
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
use crate::{error::Result, http::extractor::MastodonAuthExtractor, state::Zustand};
|
||||||
|
use axum::{debug_handler, extract::State, routing, Json, Router};
|
||||||
|
use futures_util::TryStreamExt;
|
||||||
|
use kitsune_core::{
|
||||||
|
mapping::MastodonMapper,
|
||||||
|
service::custom_emoji::{CustomEmojiService, GetEmojiList},
|
||||||
|
};
|
||||||
|
use kitsune_type::mastodon::CustomEmoji;
|
||||||
|
|
||||||
|
#[debug_handler(state = crate::state::Zustand)]
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/api/v1/custom_emojis",
|
||||||
|
security(
|
||||||
|
("oauth_token" = [])
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "List of custom emojis available on the server", body = Vec<CustomEmoji>)
|
||||||
|
),
|
||||||
|
)]
|
||||||
|
pub async fn get(
|
||||||
|
State(custom_emoji_service): State<CustomEmojiService>,
|
||||||
|
State(mastodon_mapper): State<MastodonMapper>,
|
||||||
|
user_data: Option<MastodonAuthExtractor>,
|
||||||
|
) -> Result<Json<Vec<CustomEmoji>>> {
|
||||||
|
let get_emoji_list = GetEmojiList::builder()
|
||||||
|
.fetching_account_id(user_data.map(|x| x.0.account.id))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let custom_emojis: Vec<CustomEmoji> = custom_emoji_service
|
||||||
|
.get_list(get_emoji_list)
|
||||||
|
.await?
|
||||||
|
.and_then(|acc| mastodon_mapper.map(acc))
|
||||||
|
.try_collect()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Json(custom_emojis))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router<Zustand> {
|
||||||
|
Router::new().route("/", routing::get(get))
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ use axum::Router;
|
||||||
|
|
||||||
pub mod accounts;
|
pub mod accounts;
|
||||||
pub mod apps;
|
pub mod apps;
|
||||||
|
pub mod custom_emojis;
|
||||||
pub mod follow_requests;
|
pub mod follow_requests;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
|
@ -14,6 +15,7 @@ pub fn routes() -> Router<Zustand> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.nest("/apps", apps::routes())
|
.nest("/apps", apps::routes())
|
||||||
.nest("/accounts", accounts::routes())
|
.nest("/accounts", accounts::routes())
|
||||||
|
.nest("/custom_emojis", custom_emojis::routes())
|
||||||
.nest("/follow_requests", follow_requests::routes())
|
.nest("/follow_requests", follow_requests::routes())
|
||||||
.nest("/instance", instance::routes())
|
.nest("/instance", instance::routes())
|
||||||
.nest("/media", media::routes())
|
.nest("/media", media::routes())
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod confirm_account;
|
pub mod confirm_account;
|
||||||
|
pub mod custom_emojis;
|
||||||
#[cfg(feature = "mastodon-api")]
|
#[cfg(feature = "mastodon-api")]
|
||||||
pub mod mastodon;
|
pub mod mastodon;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use self::{
|
use self::{
|
||||||
handler::{confirm_account, media, nodeinfo, oauth, posts, users, well_known},
|
handler::{confirm_account, custom_emojis, media, nodeinfo, oauth, posts, users, well_known},
|
||||||
openapi::api_docs,
|
openapi::api_docs,
|
||||||
};
|
};
|
||||||
use crate::state::Zustand;
|
use crate::state::Zustand;
|
||||||
|
@ -44,6 +44,7 @@ pub fn create_router(
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
let mut router = Router::new()
|
let mut router = Router::new()
|
||||||
.nest("/confirm-account", confirm_account::routes())
|
.nest("/confirm-account", confirm_account::routes())
|
||||||
|
.nest("/emojis", custom_emojis::routes())
|
||||||
.nest("/media", media::routes())
|
.nest("/media", media::routes())
|
||||||
.nest("/nodeinfo", nodeinfo::routes())
|
.nest("/nodeinfo", nodeinfo::routes())
|
||||||
.nest(
|
.nest(
|
||||||
|
|
|
@ -4,7 +4,7 @@ use kitsune_core::{
|
||||||
activitypub::Fetcher,
|
activitypub::Fetcher,
|
||||||
event::PostEventEmitter,
|
event::PostEventEmitter,
|
||||||
service::{
|
service::{
|
||||||
account::AccountService, attachment::AttachmentService,
|
account::AccountService, attachment::AttachmentService, custom_emoji::CustomEmojiService,
|
||||||
federation_filter::FederationFilterService, instance::InstanceService, job::JobService,
|
federation_filter::FederationFilterService, instance::InstanceService, job::JobService,
|
||||||
notification::NotificationService, post::PostService, search::SearchService,
|
notification::NotificationService, post::PostService, search::SearchService,
|
||||||
timeline::TimelineService, url::UrlService, user::UserService,
|
timeline::TimelineService, url::UrlService, user::UserService,
|
||||||
|
@ -55,6 +55,7 @@ impl_from_ref! {
|
||||||
[
|
[
|
||||||
AccountService => |input: &Zustand| input.core.service.account.clone(),
|
AccountService => |input: &Zustand| input.core.service.account.clone(),
|
||||||
AttachmentService => |input: &Zustand| input.core.service.attachment.clone(),
|
AttachmentService => |input: &Zustand| input.core.service.attachment.clone(),
|
||||||
|
CustomEmojiService => |input: &Zustand| input.core.service.custom_emoji.clone(),
|
||||||
FederationFilterService => |input: &Zustand| input.core.service.federation_filter.clone(),
|
FederationFilterService => |input: &Zustand| input.core.service.federation_filter.clone(),
|
||||||
JobService => |input: &Zustand| input.core.service.job.clone(),
|
JobService => |input: &Zustand| input.core.service.job.clone(),
|
||||||
NotificationService => |input: &Zustand| input.core.service.notification.clone(),
|
NotificationService => |input: &Zustand| input.core.service.notification.clone(),
|
||||||
|
|
|
@ -41,6 +41,19 @@ fn enforce_prefix<'a>(lexer: &Lexer<'a, PostElement<'a>>) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn emoji_split<'a>(lexer: &Lexer<'a, PostElement<'a>>) -> (&'a str, Option<&'a str>) {
|
||||||
|
let slice = lexer.slice().trim_matches(':');
|
||||||
|
|
||||||
|
let emoji_data = if let Some((shortcode, domain)) = slice.split_once('@') {
|
||||||
|
(shortcode, Some(domain))
|
||||||
|
} else {
|
||||||
|
(slice, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
emoji_data
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn mention_split<'a>(lexer: &Lexer<'a, PostElement<'a>>) -> Option<(&'a str, Option<&'a str>)> {
|
fn mention_split<'a>(lexer: &Lexer<'a, PostElement<'a>>) -> Option<(&'a str, Option<&'a str>)> {
|
||||||
if !enforce_prefix(lexer) || !enforce_postfix(lexer) {
|
if !enforce_prefix(lexer) || !enforce_postfix(lexer) {
|
||||||
|
@ -61,11 +74,8 @@ fn mention_split<'a>(lexer: &Lexer<'a, PostElement<'a>>) -> Option<(&'a str, Opt
|
||||||
|
|
||||||
#[derive(Debug, Logos, PartialEq)]
|
#[derive(Debug, Logos, PartialEq)]
|
||||||
pub enum PostElement<'a> {
|
pub enum PostElement<'a> {
|
||||||
#[regex(
|
#[regex(r":[\w\d_-]+(@[\w\-_]+\.[\.\w]+)?:", emoji_split)]
|
||||||
r":[\w\d-]+:",
|
Emote((&'a str, Option<&'a str>)),
|
||||||
|lexer| lexer.slice().trim_matches(':'),
|
|
||||||
)]
|
|
||||||
Emote(&'a str),
|
|
||||||
|
|
||||||
#[regex(
|
#[regex(
|
||||||
r"#[\w_-]+",
|
r"#[\w_-]+",
|
||||||
|
@ -150,8 +160,9 @@ impl<'a> Element<'a> {
|
||||||
) -> impl Iterator<Item = (Element<'a>, Span)> {
|
) -> impl Iterator<Item = (Element<'a>, Span)> {
|
||||||
pairs.map(|(item, span)| {
|
pairs.map(|(item, span)| {
|
||||||
let element = match item {
|
let element = match item {
|
||||||
PostElement::Emote(name) => Self::Emote(Emote {
|
PostElement::Emote((name, domain)) => Self::Emote(Emote {
|
||||||
content: Cow::Borrowed(name),
|
shortcode: Cow::Borrowed(name),
|
||||||
|
domain: domain.map(Cow::Borrowed),
|
||||||
}),
|
}),
|
||||||
PostElement::Hashtag(content) => Self::Hashtag(Hashtag {
|
PostElement::Hashtag(content) => Self::Hashtag(Hashtag {
|
||||||
content: Cow::Borrowed(content),
|
content: Cow::Borrowed(content),
|
||||||
|
@ -187,12 +198,22 @@ impl Render for Element<'_> {
|
||||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
pub struct Emote<'a> {
|
pub struct Emote<'a> {
|
||||||
/// Name of an emote
|
/// Name of an emote
|
||||||
pub content: Cow<'a, str>,
|
pub shortcode: Cow<'a, str>,
|
||||||
|
/// Domain
|
||||||
|
pub domain: Option<Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Emote<'_> {
|
impl Render for Emote<'_> {
|
||||||
fn render(&self, out: &mut impl fmt::Write) {
|
fn render(&self, out: &mut impl fmt::Write) {
|
||||||
let _ = write!(out, ":{}:", self.content);
|
let _ = match &self.domain {
|
||||||
|
Some(domain) => write!(
|
||||||
|
out,
|
||||||
|
":{}__{}:",
|
||||||
|
self.shortcode,
|
||||||
|
domain.replace(['.', '-'], "_")
|
||||||
|
),
|
||||||
|
None => write!(out, ":{}:", self.shortcode),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,9 +250,7 @@ impl Render for Html<'_> {
|
||||||
let _ = write!(out, " {name}=\"{value}\"");
|
let _ = write!(out, " {name}=\"{value}\"");
|
||||||
}
|
}
|
||||||
let _ = out.write_char('>');
|
let _ = out.write_char('>');
|
||||||
|
|
||||||
self.content.render(out);
|
self.content.render(out);
|
||||||
|
|
||||||
let _ = write!(out, "</{}>", self.tag);
|
let _ = write!(out, "</{}>", self.tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,20 @@ input_file: lib/post-process/tests/input/emote/full_post_1
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
Emote(
|
Emote(
|
||||||
"blobfoxcoffee",
|
(
|
||||||
|
"blobfoxcoffee",
|
||||||
|
None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
6..21,
|
6..21,
|
||||||
":blobfoxcoffee:",
|
":blobfoxcoffee:",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Emote(
|
Emote(
|
||||||
"blobcatpeek",
|
(
|
||||||
|
"blobcatpeek",
|
||||||
|
None,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
40..53,
|
40..53,
|
||||||
":blobcatpeek:",
|
":blobcatpeek:",
|
||||||
|
|
|
@ -12,7 +12,7 @@ fn link_transformation() {
|
||||||
tag: Cow::Borrowed("a"),
|
tag: Cow::Borrowed("a"),
|
||||||
attributes: vec![(
|
attributes: vec![(
|
||||||
Cow::Borrowed("href"),
|
Cow::Borrowed("href"),
|
||||||
Cow::Owned(format!("https://example.com/emote/{}", emote.content)),
|
Cow::Owned(format!("https://example.com/emote/{}", emote.shortcode)),
|
||||||
)],
|
)],
|
||||||
content: Box::new(Element::Emote(emote)),
|
content: Box::new(Element::Emote(emote)),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
{"@context":["https://www.w3.org/ns/activitystreams",{"toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"}}],"id":"https://corteximplant.com/emojis/7952","type":"Emoji","name":":Blobhaj:","updated":"2022-11-10T18:48:57Z","icon":{"type":"Image","mediaType":"image/png","url":"https://corteximplant.com/system/custom_emojis/images/000/007/952/original/33b7f12bd094b815.png"}}
|
|
@ -0,0 +1 @@
|
||||||
|
{"@context":["https://www.w3.org/ns/activitystreams",{"toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","focalPoint":{"@container":"@list","@id":"toot:focalPoint"}}],"id":"https://corteximplant.com/emojis/8933","type":"Emoji","name":":blobcat:","updated":"2022-11-10T19:10:49Z","icon":{"type":"Image","mediaType":"image/png","url":"https://corteximplant.com/system/custom_emojis/images/000/008/933/original/ee14c9edeec55f08.png"}}
|
Loading…
Reference in New Issue