refactor (backend): port reaction-lib to backend-rs

This commit is contained in:
naskya 2024-04-15 10:02:44 +09:00
parent 2731003bc9
commit 0f3126196f
No known key found for this signature in database
GPG Key ID: 712D413B3A9FED5C
10 changed files with 211 additions and 94 deletions

View File

@ -156,6 +156,14 @@ export function nyaify(text: string, lang?: string | undefined | null): string
export function hashPassword(password: string): string
export function verifyPassword(password: string, hash: string): boolean
export function isOldPasswordAlgorithm(hash: string): boolean
export interface DecodedReaction {
reaction: string
name: string | null
host: string | null
}
export function decodeReaction(reaction: string): DecodedReaction
export function countReactions(reactions: Record<string, number>): Record<string, number>
export function toDbReaction(reaction?: string | undefined | null, host?: string | undefined | null): Promise<string>
export interface AbuseUserReport {
id: string
createdAt: Date

View File

@ -310,7 +310,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
const { readServerConfig, stringToAcct, acctToString, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
module.exports.readServerConfig = readServerConfig
module.exports.stringToAcct = stringToAcct
@ -333,6 +333,9 @@ module.exports.nyaify = nyaify
module.exports.hashPassword = hashPassword
module.exports.verifyPassword = verifyPassword
module.exports.isOldPasswordAlgorithm = isOldPasswordAlgorithm
module.exports.decodeReaction = decodeReaction
module.exports.countReactions = countReactions
module.exports.toDbReaction = toDbReaction
module.exports.AntennaSrcEnum = AntennaSrcEnum
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
module.exports.NoteVisibilityEnum = NoteVisibilityEnum

View File

@ -8,3 +8,4 @@ pub mod mastodon_id;
pub mod meta;
pub mod nyaify;
pub mod password;
pub mod reaction;

View File

@ -0,0 +1,191 @@
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<String>,
pub host: Option<String>,
}
#[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<Regex> =
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<String, u32>) -> HashMap<String, u32> {
let mut res = HashMap::<String, u32>::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<String, Error> {
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<Regex> =
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);
}
}

View File

@ -3,8 +3,7 @@ import { Emojis } from "@/models/index.js";
import type { Emoji } from "@/models/entities/emoji.js";
import type { Note } from "@/models/entities/note.js";
import { Cache } from "./cache.js";
import { isSelfHost, toPuny } from "backend-rs";
import { decodeReaction } from "./reaction-lib.js";
import { decodeReaction, isSelfHost, toPuny } from "backend-rs";
import config from "@/config/index.js";
import { query } from "@/prelude/url.js";
import { redisClient } from "@/db/redis.js";

View File

@ -1,83 +0,0 @@
import { fetchMeta, isUnicodeEmoji, toPuny } from "backend-rs";
import { Emojis } from "@/models/index.js";
import { IsNull } from "typeorm";
export function convertReactions(reactions: Record<string, number>) {
const result = new Map();
for (const reaction in reactions) {
if (reactions[reaction] <= 0) continue;
const decoded = decodeReaction(reaction).reaction;
result.set(decoded, (result.get(decoded) || 0) + reactions[reaction]);
}
return Object.fromEntries(result);
}
export async function toDbReaction(
reaction?: string | null,
reacterHost?: string | null,
): Promise<string> {
if (!reaction) return (await fetchMeta(true)).defaultReaction;
if (reaction.includes("❤") || reaction.includes("♥️")) return "❤️";
// Allow unicode reactions
if (isUnicodeEmoji(reaction)) {
return reaction;
}
reacterHost = reacterHost == null ? null : toPuny(reacterHost);
const custom = reaction.match(/^:([\w+-]+)(?:@\.)?:$/);
if (custom) {
const name = custom[1];
const emoji = await Emojis.findOneBy({
host: reacterHost || IsNull(),
name,
});
if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
}
return (await fetchMeta(true)).defaultReaction;
}
type DecodedReaction = {
/**
* (Unicode Emoji or ':name@hostname' or ':name@.')
*/
reaction: string;
/**
* name (name, Emojiクエリに使う)
*/
name?: string;
/**
* host (host, Emojiクエリに使う)
*/
host?: string | null;
};
export function decodeReaction(str: string): DecodedReaction {
const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/);
if (custom) {
const name = custom[1];
const host = custom[2] || null;
return {
reaction: `:${name}@${host || "."}:`, // ローカル分は@以降を省略するのではなく.にする
name,
host,
};
}
return {
reaction: str,
name: undefined,
host: undefined,
};
}

View File

@ -2,7 +2,7 @@ import { db } from "@/db/postgre.js";
import { NoteReaction } from "@/models/entities/note-reaction.js";
import { Notes, Users } from "../index.js";
import type { Packed } from "@/misc/schema.js";
import { decodeReaction } from "@/misc/reaction-lib.js";
import { decodeReaction } from "backend-rs";
import type { User } from "@/models/entities/user.js";
export const NoteReactionRepository = db.getRepository(NoteReaction).extend({

View File

@ -12,9 +12,8 @@ import {
Channels,
} from "../index.js";
import type { Packed } from "@/misc/schema.js";
import { nyaify } from "backend-rs";
import { countReactions, decodeReaction, nyaify } from "backend-rs";
import { awaitAll } from "@/prelude/await-all.js";
import { convertReactions, decodeReaction } from "@/misc/reaction-lib.js";
import type { NoteReaction } from "@/models/entities/note-reaction.js";
import {
aggregateNoteEmojis,
@ -214,7 +213,7 @@ export const NoteRepository = db.getRepository(Note).extend({
note.visibility === "specified" ? note.visibleUserIds : undefined,
renoteCount: note.renoteCount,
repliesCount: note.repliesCount,
reactions: convertReactions(note.reactions),
reactions: countReactions(note.reactions),
reactionEmojis: reactionEmoji,
emojis: noteEmoji,
tags: note.tags.length > 0 ? note.tags : undefined,

View File

@ -2,7 +2,6 @@ import { publishNoteStream } from "@/services/stream.js";
import { renderLike } from "@/remote/activitypub/renderer/like.js";
import DeliverManager from "@/remote/activitypub/deliver-manager.js";
import { renderActivity } from "@/remote/activitypub/renderer/index.js";
import { toDbReaction, decodeReaction } from "@/misc/reaction-lib.js";
import type { User, IRemoteUser } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js";
import {
@ -14,7 +13,7 @@ import {
Blockings,
} from "@/models/index.js";
import { IsNull, Not } from "typeorm";
import { genId } from "backend-rs";
import { decodeReaction, genId, toDbReaction } from "backend-rs";
import { createNotification } from "@/services/create-notification.js";
import deleteReaction from "./delete.js";
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
@ -95,7 +94,7 @@ export default async (
const emoji = await Emojis.findOne({
where: {
name: decodedReaction.name,
name: decodedReaction.name ?? undefined,
host: decodedReaction.host ?? IsNull(),
},
select: ["name", "host", "originalUrl", "publicUrl"],

View File

@ -7,7 +7,7 @@ import { IdentifiableError } from "@/misc/identifiable-error.js";
import type { User, IRemoteUser } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js";
import { NoteReactions, Users, Notes } from "@/models/index.js";
import { decodeReaction } from "@/misc/reaction-lib.js";
import { decodeReaction } from "backend-rs";
export default async (
user: { id: User["id"]; host: User["host"] },