Add support for emoji shortcodes
This commit is contained in:
parent
e8500b982b
commit
75579eae4f
12 changed files with 207 additions and 22 deletions
|
@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
- Save sizes of media attachments and other files to database.
|
||||
- Added `import-emoji` command.
|
||||
- Added support for emoji shortcodes.
|
||||
|
||||
### Security
|
||||
|
||||
|
|
|
@ -9,17 +9,21 @@ use crate::activitypub::{
|
|||
local_actor_id,
|
||||
local_actor_followers,
|
||||
local_actor_subscribers,
|
||||
local_emoji_id,
|
||||
local_object_id,
|
||||
},
|
||||
types::{Attachment, LinkTag, SimpleTag},
|
||||
vocabulary::{CREATE, DOCUMENT, HASHTAG, LINK, MENTION, NOTE},
|
||||
types::{Attachment, EmojiTag, EmojiTagImage, LinkTag, SimpleTag},
|
||||
vocabulary::*,
|
||||
};
|
||||
use crate::config::Instance;
|
||||
use crate::database::{DatabaseClient, DatabaseError};
|
||||
use crate::models::posts::queries::get_post_author;
|
||||
use crate::models::posts::types::{Post, Visibility};
|
||||
use crate::models::relationships::queries::{get_followers, get_subscribers};
|
||||
use crate::models::users::types::User;
|
||||
use crate::models::{
|
||||
emojis::types::DbEmoji,
|
||||
posts::queries::get_post_author,
|
||||
posts::types::{Post, Visibility},
|
||||
relationships::queries::{get_followers, get_subscribers},
|
||||
users::types::User,
|
||||
};
|
||||
use crate::utils::files::get_file_url;
|
||||
use crate::web_client::urls::get_tag_page_url;
|
||||
|
||||
|
@ -29,6 +33,7 @@ use crate::web_client::urls::get_tag_page_url;
|
|||
enum Tag {
|
||||
SimpleTag(SimpleTag),
|
||||
LinkTag(LinkTag),
|
||||
EmojiTag(EmojiTag),
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -64,6 +69,20 @@ pub struct Note {
|
|||
quote_url: Option<String>,
|
||||
}
|
||||
|
||||
pub fn build_emoji_tag(instance_url: &str, emoji: &DbEmoji) -> EmojiTag {
|
||||
EmojiTag {
|
||||
tag_type: EMOJI.to_string(),
|
||||
icon: EmojiTagImage {
|
||||
object_type: IMAGE.to_string(),
|
||||
url: get_file_url(instance_url, &emoji.image.file_name),
|
||||
media_type: Some(emoji.image.media_type.clone()),
|
||||
},
|
||||
id: local_emoji_id(instance_url, &emoji.emoji_name),
|
||||
name: format!(":{}:", emoji.emoji_name),
|
||||
updated: emoji.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_note(
|
||||
instance_hostname: &str,
|
||||
instance_url: &str,
|
||||
|
@ -125,6 +144,7 @@ pub fn build_note(
|
|||
};
|
||||
tags.push(Tag::SimpleTag(tag));
|
||||
};
|
||||
|
||||
assert_eq!(post.links.len(), post.linked.len());
|
||||
for linked in &post.linked {
|
||||
// Build FEP-e232 object link
|
||||
|
@ -140,9 +160,16 @@ pub fn build_note(
|
|||
};
|
||||
let maybe_quote_url = post.linked.get(0)
|
||||
.map(|linked| linked.object_id(instance_url));
|
||||
|
||||
for emoji in &post.emojis {
|
||||
let tag = build_emoji_tag(instance_url, emoji);
|
||||
tags.push(Tag::EmojiTag(tag));
|
||||
};
|
||||
|
||||
let in_reply_to_object_id = match post.in_reply_to_id {
|
||||
Some(in_reply_to_id) => {
|
||||
let in_reply_to = post.in_reply_to.as_ref().unwrap();
|
||||
let in_reply_to = post.in_reply_to.as_ref()
|
||||
.expect("in_reply_to should be populated");
|
||||
assert_eq!(in_reply_to.id, in_reply_to_id);
|
||||
let in_reply_to_actor_id = in_reply_to.author.actor_id(instance_url);
|
||||
if !primary_audience.contains(&in_reply_to_actor_id) {
|
||||
|
@ -283,6 +310,17 @@ mod tests {
|
|||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_emoji_tag() {
|
||||
let emoji = DbEmoji {
|
||||
emoji_name: "test".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let emoji_tag = build_emoji_tag(INSTANCE_URL, &emoji);
|
||||
assert_eq!(emoji_tag.id, "https://example.com/objects/emojis/test");
|
||||
assert_eq!(emoji_tag.name, ":test:");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_note() {
|
||||
let author = DbActorProfile {
|
||||
|
|
|
@ -61,6 +61,10 @@ pub fn local_object_id(instance_url: &str, internal_object_id: &Uuid) -> String
|
|||
format!("{}/objects/{}", instance_url, internal_object_id)
|
||||
}
|
||||
|
||||
pub fn local_emoji_id(instance_url: &str, emoji_name: &str) -> String {
|
||||
format!("{}/objects/emojis/{}", instance_url, emoji_name)
|
||||
}
|
||||
|
||||
pub fn parse_local_actor_id(
|
||||
instance_url: &str,
|
||||
actor_id: &str,
|
||||
|
|
|
@ -54,7 +54,7 @@ pub struct LinkTag {
|
|||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmojiTagImage {
|
||||
#[serde(rename = "type")]
|
||||
|
@ -63,7 +63,7 @@ pub struct EmojiTagImage {
|
|||
pub media_type: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EmojiTag {
|
||||
#[serde(rename = "type")]
|
||||
|
|
|
@ -12,12 +12,19 @@ use uuid::Uuid;
|
|||
use crate::config::Config;
|
||||
use crate::database::{get_database_client, DbPool};
|
||||
use crate::errors::HttpError;
|
||||
use crate::models::posts::helpers::{add_related_posts, can_view_post};
|
||||
use crate::models::posts::queries::{get_post_by_id, get_posts_by_author};
|
||||
use crate::models::users::queries::get_user_by_name;
|
||||
use crate::models::{
|
||||
emojis::queries::get_local_emoji_by_name,
|
||||
posts::helpers::{add_related_posts, can_view_post},
|
||||
posts::queries::{get_post_by_id, get_posts_by_author},
|
||||
users::queries::get_user_by_name,
|
||||
};
|
||||
use crate::web_client::urls::{get_post_page_url, get_profile_page_url};
|
||||
use super::actors::types::{get_local_actor, get_instance_actor};
|
||||
use super::builders::create_note::{build_note, build_create_note};
|
||||
use super::builders::create_note::{
|
||||
build_emoji_tag,
|
||||
build_note,
|
||||
build_create_note,
|
||||
};
|
||||
use super::collections::{
|
||||
COLLECTION_PAGE_SIZE,
|
||||
OrderedCollection,
|
||||
|
@ -336,6 +343,27 @@ pub async fn object_view(
|
|||
Ok(response)
|
||||
}
|
||||
|
||||
#[get("/objects/emojis/{emoji_name}")]
|
||||
pub async fn emoji_view(
|
||||
config: web::Data<Config>,
|
||||
db_pool: web::Data<DbPool>,
|
||||
emoji_name: web::Path<String>,
|
||||
) -> Result<HttpResponse, HttpError> {
|
||||
let db_client = &**get_database_client(&db_pool).await?;
|
||||
let emoji = get_local_emoji_by_name(
|
||||
db_client,
|
||||
&emoji_name,
|
||||
).await?;
|
||||
let object = build_emoji_tag(
|
||||
&config.instance().url(),
|
||||
&emoji,
|
||||
);
|
||||
let response = HttpResponse::Ok()
|
||||
.content_type(AP_MEDIA_TYPE)
|
||||
.json(object);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::http::{
|
||||
|
|
|
@ -158,6 +158,7 @@ async fn main() -> std::io::Result<()> {
|
|||
.service(activitypub::actor_scope())
|
||||
.service(activitypub::instance_actor_scope())
|
||||
.service(activitypub::object_view)
|
||||
.service(activitypub::emoji_view)
|
||||
.service(atom::get_atom_feed)
|
||||
.service(nodeinfo::get_nodeinfo)
|
||||
.service(nodeinfo::get_nodeinfo_2_0)
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::config::Instance;
|
|||
use crate::database::{DatabaseClient, DatabaseError};
|
||||
use crate::models::{
|
||||
posts::{
|
||||
emojis::find_emojis,
|
||||
hashtags::{find_hashtags, replace_hashtags},
|
||||
helpers::{add_related_posts, add_user_actions},
|
||||
links::{replace_object_links, find_linked_posts},
|
||||
|
@ -20,6 +21,7 @@ pub struct PostContent {
|
|||
pub tags: Vec<String>,
|
||||
pub links: Vec<Uuid>,
|
||||
pub linked: Vec<Post>,
|
||||
pub emojis: Vec<Uuid>,
|
||||
}
|
||||
|
||||
pub async fn parse_microsyntaxes(
|
||||
|
@ -59,7 +61,13 @@ pub async fn parse_microsyntaxes(
|
|||
);
|
||||
let links = link_map.values().map(|post| post.id).collect();
|
||||
let linked = link_map.into_values().collect();
|
||||
Ok(PostContent { content, mentions, tags, links, linked })
|
||||
// Emojis
|
||||
let emoji_map = find_emojis(
|
||||
db_client,
|
||||
&content,
|
||||
).await?;
|
||||
let emojis = emoji_map.values().map(|emoji| emoji.id).collect();
|
||||
Ok(PostContent { content, mentions, tags, links, linked, emojis })
|
||||
}
|
||||
|
||||
/// Load related objects and build status for API response
|
||||
|
|
|
@ -31,7 +31,11 @@ use crate::models::posts::queries::{
|
|||
delete_post,
|
||||
};
|
||||
use crate::models::posts::types::{PostCreateData, Visibility};
|
||||
use crate::models::posts::validators::{clean_content, ATTACHMENTS_MAX_NUM};
|
||||
use crate::models::posts::validators::{
|
||||
clean_content,
|
||||
ATTACHMENTS_MAX_NUM,
|
||||
EMOJIS_MAX_NUM,
|
||||
};
|
||||
use crate::models::reactions::queries::{
|
||||
create_reaction,
|
||||
delete_reaction,
|
||||
|
@ -83,7 +87,7 @@ async fn create_status(
|
|||
_ => return Err(ValidationError("unsupported content type").into()),
|
||||
};
|
||||
// Parse content
|
||||
let PostContent { mut content, mut mentions, tags, links, linked } =
|
||||
let PostContent { mut content, mut mentions, tags, links, linked, emojis } =
|
||||
parse_microsyntaxes(
|
||||
db_client,
|
||||
&instance,
|
||||
|
@ -111,6 +115,12 @@ async fn create_status(
|
|||
if links.len() > 0 && visibility != Visibility::Public {
|
||||
return Err(ValidationError("can't add links to non-public posts").into());
|
||||
};
|
||||
|
||||
// Emoji validation
|
||||
if emojis.len() > EMOJIS_MAX_NUM {
|
||||
return Err(ValidationError("too many emojis").into());
|
||||
};
|
||||
|
||||
// Reply validation
|
||||
let maybe_in_reply_to = if let Some(in_reply_to_id) = status_data.in_reply_to_id.as_ref() {
|
||||
let in_reply_to = match get_post_by_id(db_client, in_reply_to_id).await {
|
||||
|
@ -155,7 +165,7 @@ async fn create_status(
|
|||
mentions: mentions,
|
||||
tags: tags,
|
||||
links: links,
|
||||
emojis: vec![],
|
||||
emojis: emojis,
|
||||
object_id: None,
|
||||
created_at: Utc::now(),
|
||||
};
|
||||
|
|
|
@ -76,6 +76,41 @@ pub async fn update_emoji(
|
|||
Ok(emoji)
|
||||
}
|
||||
|
||||
pub async fn get_local_emoji_by_name(
|
||||
db_client: &impl DatabaseClient,
|
||||
emoji_name: &str,
|
||||
) -> Result<DbEmoji, DatabaseError> {
|
||||
let maybe_row = db_client.query_opt(
|
||||
"
|
||||
SELECT emoji
|
||||
FROM emoji
|
||||
WHERE hostname IS NULL AND emoji_name = $1
|
||||
",
|
||||
&[&emoji_name],
|
||||
).await?;
|
||||
let row = maybe_row.ok_or(DatabaseError::NotFound("emoji"))?;
|
||||
let emoji = row.try_get("emoji")?;
|
||||
Ok(emoji)
|
||||
}
|
||||
|
||||
pub async fn get_local_emojis_by_names(
|
||||
db_client: &impl DatabaseClient,
|
||||
names: &[String],
|
||||
) -> Result<Vec<DbEmoji>, DatabaseError> {
|
||||
let rows = db_client.query(
|
||||
"
|
||||
SELECT emoji
|
||||
FROM emoji
|
||||
WHERE hostname IS NULL AND emoji_name = ANY($1)
|
||||
",
|
||||
&[&names],
|
||||
).await?;
|
||||
let emojis = rows.iter()
|
||||
.map(|row| row.try_get("emoji"))
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(emojis)
|
||||
}
|
||||
|
||||
pub async fn get_emoji_by_name_and_hostname(
|
||||
db_client: &impl DatabaseClient,
|
||||
emoji_name: &str,
|
||||
|
@ -172,11 +207,7 @@ mod tests {
|
|||
#[serial]
|
||||
async fn test_delete_emoji() {
|
||||
let db_client = &create_test_database().await;
|
||||
let image = EmojiImage {
|
||||
file_name: "test.png".to_string(),
|
||||
file_size: 10000,
|
||||
media_type: "image/png".to_string(),
|
||||
};
|
||||
let image = EmojiImage::default();
|
||||
let emoji = create_emoji(
|
||||
db_client,
|
||||
"test",
|
||||
|
|
|
@ -9,6 +9,7 @@ use super::validators::EMOJI_MAX_SIZE;
|
|||
fn default_emoji_file_size() -> usize { EMOJI_MAX_SIZE }
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[cfg_attr(test, derive(Default))]
|
||||
pub struct EmojiImage {
|
||||
pub file_name: String,
|
||||
#[serde(default = "default_emoji_file_size")]
|
||||
|
@ -20,6 +21,7 @@ json_from_sql!(EmojiImage);
|
|||
json_to_sql!(EmojiImage);
|
||||
|
||||
#[derive(Clone, FromSql)]
|
||||
#[cfg_attr(test, derive(Default))]
|
||||
#[postgres(name = "emoji")]
|
||||
pub struct DbEmoji {
|
||||
pub id: Uuid,
|
||||
|
|
61
src/models/posts/emojis.rs
Normal file
61
src/models/posts/emojis.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use crate::database::{DatabaseClient, DatabaseError};
|
||||
use crate::models::emojis::queries::get_local_emojis_by_names;
|
||||
use crate::models::emojis::types::DbEmoji;
|
||||
use super::links::is_inside_code_block;
|
||||
|
||||
// See also: EMOJI_NAME_RE in models::emojis::validators
|
||||
const SHORTCODE_SEARCH_RE: &str = r"(?m):(?P<name>[\w.]+):";
|
||||
|
||||
/// Finds emoji shortcodes in text
|
||||
fn find_shortcodes(text: &str) -> Vec<String> {
|
||||
let shortcode_re = Regex::new(SHORTCODE_SEARCH_RE)
|
||||
.expect("regex should be valid");
|
||||
let mut emoji_names = vec![];
|
||||
for caps in shortcode_re.captures_iter(text) {
|
||||
let name_match = caps.name("name").expect("should have name group");
|
||||
if is_inside_code_block(&name_match, text) {
|
||||
// Ignore shortcodes inside code blocks
|
||||
continue;
|
||||
};
|
||||
let name = caps["name"].to_string();
|
||||
if !emoji_names.contains(&name) {
|
||||
emoji_names.push(name);
|
||||
};
|
||||
};
|
||||
emoji_names
|
||||
}
|
||||
|
||||
pub async fn find_emojis(
|
||||
db_client: &impl DatabaseClient,
|
||||
text: &str,
|
||||
) -> Result<HashMap<String, DbEmoji>, DatabaseError> {
|
||||
let emoji_names = find_shortcodes(text);
|
||||
// If shortcode doesn't exist in database, it is ignored
|
||||
let emojis = get_local_emojis_by_names(db_client, &emoji_names).await?;
|
||||
let mut emoji_map: HashMap<String, DbEmoji> = HashMap::new();
|
||||
for emoji in emojis {
|
||||
emoji_map.insert(emoji.emoji_name.clone(), emoji);
|
||||
};
|
||||
Ok(emoji_map)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TEXT_WITH_EMOJIS: &str = "@user1@server1 text :emoji_name: :abc:";
|
||||
|
||||
#[test]
|
||||
fn test_find_shortcodes() {
|
||||
let emoji_names = find_shortcodes(TEXT_WITH_EMOJIS);
|
||||
|
||||
assert_eq!(emoji_names, vec![
|
||||
"emoji_name",
|
||||
"abc",
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod emojis;
|
||||
pub mod hashtags;
|
||||
pub mod helpers;
|
||||
pub mod links;
|
||||
|
|
Loading…
Reference in a new issue