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.
|
- Save sizes of media attachments and other files to database.
|
||||||
- Added `import-emoji` command.
|
- Added `import-emoji` command.
|
||||||
|
- Added support for emoji shortcodes.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
|
|
|
@ -9,17 +9,21 @@ use crate::activitypub::{
|
||||||
local_actor_id,
|
local_actor_id,
|
||||||
local_actor_followers,
|
local_actor_followers,
|
||||||
local_actor_subscribers,
|
local_actor_subscribers,
|
||||||
|
local_emoji_id,
|
||||||
local_object_id,
|
local_object_id,
|
||||||
},
|
},
|
||||||
types::{Attachment, LinkTag, SimpleTag},
|
types::{Attachment, EmojiTag, EmojiTagImage, LinkTag, SimpleTag},
|
||||||
vocabulary::{CREATE, DOCUMENT, HASHTAG, LINK, MENTION, NOTE},
|
vocabulary::*,
|
||||||
};
|
};
|
||||||
use crate::config::Instance;
|
use crate::config::Instance;
|
||||||
use crate::database::{DatabaseClient, DatabaseError};
|
use crate::database::{DatabaseClient, DatabaseError};
|
||||||
use crate::models::posts::queries::get_post_author;
|
use crate::models::{
|
||||||
use crate::models::posts::types::{Post, Visibility};
|
emojis::types::DbEmoji,
|
||||||
use crate::models::relationships::queries::{get_followers, get_subscribers};
|
posts::queries::get_post_author,
|
||||||
use crate::models::users::types::User;
|
posts::types::{Post, Visibility},
|
||||||
|
relationships::queries::{get_followers, get_subscribers},
|
||||||
|
users::types::User,
|
||||||
|
};
|
||||||
use crate::utils::files::get_file_url;
|
use crate::utils::files::get_file_url;
|
||||||
use crate::web_client::urls::get_tag_page_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 {
|
enum Tag {
|
||||||
SimpleTag(SimpleTag),
|
SimpleTag(SimpleTag),
|
||||||
LinkTag(LinkTag),
|
LinkTag(LinkTag),
|
||||||
|
EmojiTag(EmojiTag),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
@ -64,6 +69,20 @@ pub struct Note {
|
||||||
quote_url: Option<String>,
|
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(
|
pub fn build_note(
|
||||||
instance_hostname: &str,
|
instance_hostname: &str,
|
||||||
instance_url: &str,
|
instance_url: &str,
|
||||||
|
@ -125,6 +144,7 @@ pub fn build_note(
|
||||||
};
|
};
|
||||||
tags.push(Tag::SimpleTag(tag));
|
tags.push(Tag::SimpleTag(tag));
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(post.links.len(), post.linked.len());
|
assert_eq!(post.links.len(), post.linked.len());
|
||||||
for linked in &post.linked {
|
for linked in &post.linked {
|
||||||
// Build FEP-e232 object link
|
// Build FEP-e232 object link
|
||||||
|
@ -140,9 +160,16 @@ pub fn build_note(
|
||||||
};
|
};
|
||||||
let maybe_quote_url = post.linked.get(0)
|
let maybe_quote_url = post.linked.get(0)
|
||||||
.map(|linked| linked.object_id(instance_url));
|
.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 {
|
let in_reply_to_object_id = match post.in_reply_to_id {
|
||||||
Some(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);
|
assert_eq!(in_reply_to.id, in_reply_to_id);
|
||||||
let in_reply_to_actor_id = in_reply_to.author.actor_id(instance_url);
|
let in_reply_to_actor_id = in_reply_to.author.actor_id(instance_url);
|
||||||
if !primary_audience.contains(&in_reply_to_actor_id) {
|
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]
|
#[test]
|
||||||
fn test_build_note() {
|
fn test_build_note() {
|
||||||
let author = DbActorProfile {
|
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)
|
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(
|
pub fn parse_local_actor_id(
|
||||||
instance_url: &str,
|
instance_url: &str,
|
||||||
actor_id: &str,
|
actor_id: &str,
|
||||||
|
|
|
@ -54,7 +54,7 @@ pub struct LinkTag {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EmojiTagImage {
|
pub struct EmojiTagImage {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
@ -63,7 +63,7 @@ pub struct EmojiTagImage {
|
||||||
pub media_type: Option<String>,
|
pub media_type: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct EmojiTag {
|
pub struct EmojiTag {
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
|
|
|
@ -12,12 +12,19 @@ use uuid::Uuid;
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::database::{get_database_client, DbPool};
|
use crate::database::{get_database_client, DbPool};
|
||||||
use crate::errors::HttpError;
|
use crate::errors::HttpError;
|
||||||
use crate::models::posts::helpers::{add_related_posts, can_view_post};
|
use crate::models::{
|
||||||
use crate::models::posts::queries::{get_post_by_id, get_posts_by_author};
|
emojis::queries::get_local_emoji_by_name,
|
||||||
use crate::models::users::queries::get_user_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 crate::web_client::urls::{get_post_page_url, get_profile_page_url};
|
||||||
use super::actors::types::{get_local_actor, get_instance_actor};
|
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::{
|
use super::collections::{
|
||||||
COLLECTION_PAGE_SIZE,
|
COLLECTION_PAGE_SIZE,
|
||||||
OrderedCollection,
|
OrderedCollection,
|
||||||
|
@ -336,6 +343,27 @@ pub async fn object_view(
|
||||||
Ok(response)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use actix_web::http::{
|
use actix_web::http::{
|
||||||
|
|
|
@ -158,6 +158,7 @@ async fn main() -> std::io::Result<()> {
|
||||||
.service(activitypub::actor_scope())
|
.service(activitypub::actor_scope())
|
||||||
.service(activitypub::instance_actor_scope())
|
.service(activitypub::instance_actor_scope())
|
||||||
.service(activitypub::object_view)
|
.service(activitypub::object_view)
|
||||||
|
.service(activitypub::emoji_view)
|
||||||
.service(atom::get_atom_feed)
|
.service(atom::get_atom_feed)
|
||||||
.service(nodeinfo::get_nodeinfo)
|
.service(nodeinfo::get_nodeinfo)
|
||||||
.service(nodeinfo::get_nodeinfo_2_0)
|
.service(nodeinfo::get_nodeinfo_2_0)
|
||||||
|
|
|
@ -4,6 +4,7 @@ use crate::config::Instance;
|
||||||
use crate::database::{DatabaseClient, DatabaseError};
|
use crate::database::{DatabaseClient, DatabaseError};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
posts::{
|
posts::{
|
||||||
|
emojis::find_emojis,
|
||||||
hashtags::{find_hashtags, replace_hashtags},
|
hashtags::{find_hashtags, replace_hashtags},
|
||||||
helpers::{add_related_posts, add_user_actions},
|
helpers::{add_related_posts, add_user_actions},
|
||||||
links::{replace_object_links, find_linked_posts},
|
links::{replace_object_links, find_linked_posts},
|
||||||
|
@ -20,6 +21,7 @@ pub struct PostContent {
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
pub links: Vec<Uuid>,
|
pub links: Vec<Uuid>,
|
||||||
pub linked: Vec<Post>,
|
pub linked: Vec<Post>,
|
||||||
|
pub emojis: Vec<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn parse_microsyntaxes(
|
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 links = link_map.values().map(|post| post.id).collect();
|
||||||
let linked = link_map.into_values().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
|
/// Load related objects and build status for API response
|
||||||
|
|
|
@ -31,7 +31,11 @@ use crate::models::posts::queries::{
|
||||||
delete_post,
|
delete_post,
|
||||||
};
|
};
|
||||||
use crate::models::posts::types::{PostCreateData, Visibility};
|
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::{
|
use crate::models::reactions::queries::{
|
||||||
create_reaction,
|
create_reaction,
|
||||||
delete_reaction,
|
delete_reaction,
|
||||||
|
@ -83,7 +87,7 @@ async fn create_status(
|
||||||
_ => return Err(ValidationError("unsupported content type").into()),
|
_ => return Err(ValidationError("unsupported content type").into()),
|
||||||
};
|
};
|
||||||
// Parse content
|
// Parse content
|
||||||
let PostContent { mut content, mut mentions, tags, links, linked } =
|
let PostContent { mut content, mut mentions, tags, links, linked, emojis } =
|
||||||
parse_microsyntaxes(
|
parse_microsyntaxes(
|
||||||
db_client,
|
db_client,
|
||||||
&instance,
|
&instance,
|
||||||
|
@ -111,6 +115,12 @@ async fn create_status(
|
||||||
if links.len() > 0 && visibility != Visibility::Public {
|
if links.len() > 0 && visibility != Visibility::Public {
|
||||||
return Err(ValidationError("can't add links to non-public posts").into());
|
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
|
// Reply validation
|
||||||
let maybe_in_reply_to = if let Some(in_reply_to_id) = status_data.in_reply_to_id.as_ref() {
|
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 {
|
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,
|
mentions: mentions,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
links: links,
|
links: links,
|
||||||
emojis: vec![],
|
emojis: emojis,
|
||||||
object_id: None,
|
object_id: None,
|
||||||
created_at: Utc::now(),
|
created_at: Utc::now(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -76,6 +76,41 @@ pub async fn update_emoji(
|
||||||
Ok(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(
|
pub async fn get_emoji_by_name_and_hostname(
|
||||||
db_client: &impl DatabaseClient,
|
db_client: &impl DatabaseClient,
|
||||||
emoji_name: &str,
|
emoji_name: &str,
|
||||||
|
@ -172,11 +207,7 @@ mod tests {
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_delete_emoji() {
|
async fn test_delete_emoji() {
|
||||||
let db_client = &create_test_database().await;
|
let db_client = &create_test_database().await;
|
||||||
let image = EmojiImage {
|
let image = EmojiImage::default();
|
||||||
file_name: "test.png".to_string(),
|
|
||||||
file_size: 10000,
|
|
||||||
media_type: "image/png".to_string(),
|
|
||||||
};
|
|
||||||
let emoji = create_emoji(
|
let emoji = create_emoji(
|
||||||
db_client,
|
db_client,
|
||||||
"test",
|
"test",
|
||||||
|
|
|
@ -9,6 +9,7 @@ use super::validators::EMOJI_MAX_SIZE;
|
||||||
fn default_emoji_file_size() -> usize { EMOJI_MAX_SIZE }
|
fn default_emoji_file_size() -> usize { EMOJI_MAX_SIZE }
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[cfg_attr(test, derive(Default))]
|
||||||
pub struct EmojiImage {
|
pub struct EmojiImage {
|
||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
#[serde(default = "default_emoji_file_size")]
|
#[serde(default = "default_emoji_file_size")]
|
||||||
|
@ -20,6 +21,7 @@ json_from_sql!(EmojiImage);
|
||||||
json_to_sql!(EmojiImage);
|
json_to_sql!(EmojiImage);
|
||||||
|
|
||||||
#[derive(Clone, FromSql)]
|
#[derive(Clone, FromSql)]
|
||||||
|
#[cfg_attr(test, derive(Default))]
|
||||||
#[postgres(name = "emoji")]
|
#[postgres(name = "emoji")]
|
||||||
pub struct DbEmoji {
|
pub struct DbEmoji {
|
||||||
pub id: Uuid,
|
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 hashtags;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
pub mod links;
|
pub mod links;
|
||||||
|
|
Loading…
Reference in a new issue