refactor (backend): port reaction-lib to backend-rs
This commit is contained in:
parent
2731003bc9
commit
0f3126196f
10 changed files with 211 additions and 94 deletions
8
packages/backend-rs/index.d.ts
vendored
8
packages/backend-rs/index.d.ts
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,3 +8,4 @@ pub mod mastodon_id;
|
|||
pub mod meta;
|
||||
pub mod nyaify;
|
||||
pub mod password;
|
||||
pub mod reaction;
|
||||
|
|
191
packages/backend-rs/src/misc/reaction.rs
Normal file
191
packages/backend-rs/src/misc/reaction.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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"] },
|
||||
|
|
Loading…
Reference in a new issue