use crate::database::db_conn; use crate::misc::{convert_host::to_puny, emoji::is_unicode_emoji, meta::fetch_meta}; use crate::model::entity::emoji; use once_cell::sync::Lazy; use regex::Regex; use sea_orm::prelude::*; use std::collections::HashMap; #[derive(PartialEq, Debug)] #[crate::export(object)] pub struct DecodedReaction { pub reaction: String, pub name: Option, pub host: Option, } #[crate::export] pub fn decode_reaction(reaction: &str) -> DecodedReaction { // Misskey allows you to include "+" and "-" in emoji shortcodes // MFM spec: https://github.com/misskey-dev/mfm.js/blob/6aaf68089023c6adebe44123eebbc4dcd75955e0/docs/syntax.md?plain=1#L583 // Misskey's implementation: https://github.com/misskey-dev/misskey/blob/bba3097765317cbf95d09627961b5b5dce16a972/packages/backend/src/core/ReactionService.ts#L68 static RE: Lazy = Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@([0-9A-Za-z_.-]+))?:$").unwrap()); if let Some(captures) = RE.captures(reaction) { let name = &captures[1]; let host = captures.get(2).map(|s| s.as_str()); DecodedReaction { reaction: format!(":{}@{}:", name, host.unwrap_or(".")), name: Some(name.to_owned()), host: host.map(|s| s.to_owned()), } } else { DecodedReaction { reaction: reaction.to_owned(), name: None, host: None, } } } #[crate::export] pub fn count_reactions(reactions: &HashMap) -> HashMap { let mut res = HashMap::::new(); for (reaction, count) in reactions.iter() { if count > &0 { let decoded = decode_reaction(reaction).reaction; let total = res.entry(decoded).or_insert(0); *total += count; } } res } #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Idna error: {0}")] IdnaError(#[from] idna::Errors), #[error("Database error: {0}")] DbError(#[from] DbErr), } #[crate::export] pub async fn to_db_reaction(reaction: Option<&str>, host: Option<&str>) -> Result { if let Some(reaction) = reaction { // FIXME: Is it okay to do this only here? // This was introduced in https://firefish.dev/firefish/firefish/-/commit/af730e75b6fc1a57ca680ce83459d7e433b130cf if reaction.contains('❤') || reaction.contains("♥️") { return Ok("❤️".to_owned()); } if is_unicode_emoji(reaction) { return Ok(reaction.to_owned()); } static RE: Lazy = Lazy::new(|| Regex::new(r"^:([0-9A-Za-z_+-]+)(?:@\.)?:$").unwrap()); if let Some(captures) = RE.captures(reaction) { let name = &captures[1]; let db = db_conn().await?; if let Some(host) = host { // remote emoji let ascii_host = to_puny(host)?; // TODO: Does SeaORM have the `exists` method? if emoji::Entity::find() .filter(emoji::Column::Name.eq(name)) .filter(emoji::Column::Host.eq(&ascii_host)) .one(db) .await? .is_some() { return Ok(format!(":{name}@{ascii_host}:")); } } else { // local emoji // TODO: Does SeaORM have the `exists` method? if emoji::Entity::find() .filter(emoji::Column::Name.eq(name)) .filter(emoji::Column::Host.is_null()) .one(db) .await? .is_some() { return Ok(format!(":{name}:")); } } }; }; Ok(fetch_meta(true).await?.default_reaction) } #[cfg(test)] mod unit_test { use super::{decode_reaction, DecodedReaction}; use pretty_assertions::{assert_eq, assert_ne}; #[test] fn test_decode_reaction() { let unicode_emoji_1 = DecodedReaction { reaction: "⭐".to_string(), name: None, host: None, }; let unicode_emoji_2 = DecodedReaction { reaction: "🩷".to_string(), name: None, host: None, }; assert_eq!(decode_reaction("⭐"), unicode_emoji_1); assert_eq!(decode_reaction("🩷"), unicode_emoji_2); assert_ne!(decode_reaction("⭐"), unicode_emoji_2); assert_ne!(decode_reaction("🩷"), unicode_emoji_1); let unicode_emoji_3 = DecodedReaction { reaction: "🖖🏿".to_string(), name: None, host: None, }; assert_eq!(decode_reaction("🖖🏿"), unicode_emoji_3); let local_emoji = DecodedReaction { reaction: ":meow_melt_tears@.:".to_string(), name: Some("meow_melt_tears".to_string()), host: None, }; assert_eq!(decode_reaction(":meow_melt_tears:"), local_emoji); let remote_emoji_1 = DecodedReaction { reaction: ":meow_uwu@some-domain.example.org:".to_string(), name: Some("meow_uwu".to_string()), host: Some("some-domain.example.org".to_string()), }; assert_eq!( decode_reaction(":meow_uwu@some-domain.example.org:"), remote_emoji_1 ); let remote_emoji_2 = DecodedReaction { reaction: ":C++23@xn--eckwd4c7c.example.org:".to_string(), name: Some("C++23".to_string()), host: Some("xn--eckwd4c7c.example.org".to_string()), }; assert_eq!( decode_reaction(":C++23@xn--eckwd4c7c.example.org:"), remote_emoji_2 ); let invalid_reaction_1 = DecodedReaction { reaction: ":foo".to_string(), name: None, host: None, }; assert_eq!(decode_reaction(":foo"), invalid_reaction_1); let invalid_reaction_2 = DecodedReaction { reaction: ":foo&@example.com:".to_string(), name: None, host: None, }; assert_eq!(decode_reaction(":foo&@example.com:"), invalid_reaction_2); } }