refactor (backend): port checkWordMute to backend-rs

Co-authored-by: sup39 <dev@sup39.dev>
This commit is contained in:
naskya 2024-04-12 15:43:17 +09:00
parent 0cfa85197d
commit 83c15b1026
No known key found for this signature in database
GPG Key ID: 712D413B3A9FED5C
15 changed files with 138 additions and 111 deletions

View File

@ -118,6 +118,15 @@ export interface Acct {
}
export function stringToAcct(acct: string): Acct
export function acctToString(acct: Acct): string
export interface NoteLike {
fileIds: Array<string>
userId: string | null
text: string | null
cw: string | null
renoteId: string | null
replyId: string | null
}
export function checkWordMute(note: NoteLike, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean>
export function nyaify(text: string, lang?: string | undefined | null): string
export interface AbuseUserReport {
id: string

View File

@ -310,11 +310,12 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}
const { readServerConfig, stringToAcct, acctToString, nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr, IdConvertType, convertId } = nativeBinding
const { readServerConfig, stringToAcct, acctToString, checkWordMute, nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr, IdConvertType, convertId } = nativeBinding
module.exports.readServerConfig = readServerConfig
module.exports.stringToAcct = stringToAcct
module.exports.acctToString = acctToString
module.exports.checkWordMute = checkWordMute
module.exports.nyaify = nyaify
module.exports.AntennaSrcEnum = AntennaSrcEnum
module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum

View File

@ -1,9 +0,0 @@
use sea_orm::error::DbErr;
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum Error {
#[error("The database connections have not been initialized yet")]
Uninitialized,
#[error("ORM error: {0}")]
OrmError(#[from] DbErr),
}

View File

@ -1,12 +1,9 @@
pub mod error;
use crate::config::server::SERVER_CONFIG;
use error::Error;
use sea_orm::{Database, DbConn};
use sea_orm::{Database, DbConn, DbErr};
static DB_CONN: once_cell::sync::OnceCell<DbConn> = once_cell::sync::OnceCell::new();
async fn init_database() -> Result<&'static DbConn, Error> {
async fn init_database() -> Result<&'static DbConn, DbErr> {
let database_uri = format!(
"postgres://{}:{}@{}:{}/{}",
SERVER_CONFIG.db.user,
@ -19,7 +16,7 @@ async fn init_database() -> Result<&'static DbConn, Error> {
Ok(DB_CONN.get_or_init(move || conn))
}
pub async fn db_conn() -> Result<&'static DbConn, Error> {
pub async fn db_conn() -> Result<&'static DbConn, DbErr> {
match DB_CONN.get() {
Some(conn) => Ok(conn),
None => init_database().await,

View File

@ -0,0 +1,107 @@
use crate::database::db_conn;
use crate::model::entity::{drive_file, note};
use once_cell::sync::Lazy;
use regex::Regex;
use sea_orm::{prelude::*, QuerySelect};
#[crate::export(object)]
pub struct NoteLike {
pub file_ids: Vec<String>,
pub user_id: Option<String>,
pub text: Option<String>,
pub cw: Option<String>,
pub renote_id: Option<String>,
pub reply_id: Option<String>,
}
async fn all_texts(note: NoteLike) -> Result<Vec<String>, DbErr> {
let db = db_conn().await?;
let mut texts: Vec<String> = vec![];
if let Some(text) = note.text {
texts.push(text);
}
if let Some(cw) = note.cw {
texts.push(cw);
}
texts.extend(
drive_file::Entity::find()
.select_only()
.column(drive_file::Column::Comment)
.filter(drive_file::Column::Id.is_in(note.file_ids))
.into_tuple::<String>()
.all(db)
.await?,
);
if let Some(renote_id) = note.renote_id {
if let Some((text, cw)) = note::Entity::find_by_id(renote_id)
.select_only()
.columns([note::Column::Text, note::Column::Cw])
.into_tuple::<(String, String)>()
.one(db)
.await?
{
texts.push(text);
texts.push(cw);
}
}
if let Some(reply_id) = note.reply_id {
if let Some((text, cw)) = note::Entity::find_by_id(reply_id)
.select_only()
.columns([note::Column::Text, note::Column::Cw])
.into_tuple::<(String, String)>()
.one(db)
.await?
{
texts.push(text);
texts.push(cw);
}
}
Ok(texts)
}
fn convert_regex(js_regex: &str) -> String {
static RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^/(.+)/(.*)$").unwrap());
RE.replace(js_regex, "(?$2)$1").to_string()
}
fn check_word_mute_impl(
texts: &[String],
muted_word_lists: &[Vec<String>],
muted_patterns: &[String],
) -> bool {
muted_word_lists.iter().any(|muted_word_list| {
texts.iter().any(|text| {
let text_lower = text.to_lowercase();
muted_word_list
.iter()
.all(|muted_word| text_lower.contains(&muted_word.to_lowercase()))
})
}) || muted_patterns.iter().any(|muted_pattern| {
Regex::new(convert_regex(muted_pattern).as_str())
.map(|re| texts.iter().any(|text| re.is_match(text)))
.unwrap_or(false)
})
}
#[crate::export]
pub async fn check_word_mute(
note: NoteLike,
muted_word_lists: Vec<Vec<String>>,
muted_patterns: Vec<String>,
) -> Result<bool, DbErr> {
if muted_word_lists.is_empty() && muted_patterns.is_empty() {
Ok(false)
} else {
Ok(check_word_mute_impl(
&all_texts(note).await?,
&muted_word_lists,
&muted_patterns,
))
}
}

View File

@ -1,2 +1,3 @@
pub mod acct;
pub mod check_word_mute;
pub mod nyaify;

View File

@ -2,10 +2,8 @@
pub enum Error {
#[error("Failed to parse string: {0}")]
ParseError(#[from] parse_display::ParseError),
#[error("Failed to get database connection: {0}")]
DbConnError(#[from] crate::database::error::Error),
#[error("Database operation error: {0}")]
DbOperationError(#[from] sea_orm::DbErr),
#[error("Database error: {0}")]
DbError(#[from] sea_orm::DbErr),
#[error("Requested entity not found")]
NotFound,
}

View File

@ -4,8 +4,7 @@ import type { User } from "@/models/entities/user.js";
import type { UserProfile } from "@/models/entities/user-profile.js";
import { Blockings, Followings, UserProfiles } from "@/models/index.js";
import { getFullApAccount } from "@/misc/convert-host.js";
import { stringToAcct } from "backend-rs";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { checkWordMute, stringToAcct } from "backend-rs";
import type { Packed } from "@/misc/schema.js";
import { Cache } from "@/misc/cache.js";
@ -124,7 +123,7 @@ export async function checkHitAntenna(
mutes.mutedWords != null &&
mutes.mutedPatterns != null &&
antenna.userId !== note.userId &&
(await getWordHardMute(note, mutes.mutedWords, mutes.mutedPatterns))
(await checkWordMute(note, mutes.mutedWords, mutes.mutedPatterns))
)
return false;

View File

@ -1,76 +0,0 @@
import RE2 from "re2";
import type { Note } from "@/models/entities/note.js";
type NoteLike = {
userId: Note["userId"];
text: Note["text"];
files?: Note["files"];
cw?: Note["cw"];
reply?: NoteLike | null;
renote?: NoteLike | null;
};
function checkWordMute(
note: NoteLike | null | undefined,
mutedWords: string[][],
mutedPatterns: string[],
): boolean {
if (note == null) return false;
let text = `${note.cw ?? ""} ${note.text ?? ""}`;
if (note.files != null)
text += ` ${note.files.map((f) => f.comment ?? "").join(" ")}`;
text = text.trim();
if (text === "") return false;
for (const mutedWord of mutedWords) {
// Clean up
const keywords = mutedWord.filter((keyword) => keyword !== "");
if (
keywords.length > 0 &&
keywords.every((keyword) =>
text.toLowerCase().includes(keyword.toLowerCase()),
)
)
return true;
}
for (const mutedPattern of mutedPatterns) {
// represents RegExp
const regexp = mutedPattern.match(/^\/(.+)\/(.*)$/);
// This should never happen due to input sanitisation.
if (!regexp) {
console.warn(`Found invalid regex in word mutes: ${mutedPattern}`);
continue;
}
try {
if (new RE2(regexp[1], regexp[2]).test(text)) return true;
} catch (err) {
// This should never happen due to input sanitisation.
}
}
return false;
}
export async function getWordHardMute(
note: NoteLike | null,
mutedWords: string[][],
mutedPatterns: string[],
): Promise<boolean> {
if (note == null || mutedWords == null || mutedPatterns == null) return false;
if (mutedWords.length > 0) {
return (
checkWordMute(note, mutedWords, mutedPatterns) ||
checkWordMute(note.reply, mutedWords, mutedPatterns) ||
checkWordMute(note.renote, mutedWords, mutedPatterns)
);
}
return false;
}

View File

@ -1,6 +1,6 @@
import Channel from "../channel.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { checkWordMute } from "backend-rs";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import { isUserRelated } from "@/misc/is-user-related.js";
import type { Packed } from "@/misc/schema.js";
@ -72,7 +72,7 @@ export default class extends Channel {
if (
this.userProfile &&
this.user?.id !== note.userId &&
(await getWordHardMute(
(await checkWordMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,

View File

@ -1,5 +1,5 @@
import Channel from "../channel.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { checkWordMute } from "backend-rs";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import type { Packed } from "@/misc/schema.js";
@ -69,7 +69,7 @@ export default class extends Channel {
if (
this.userProfile &&
this.user?.id !== note.userId &&
(await getWordHardMute(
(await checkWordMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,

View File

@ -1,6 +1,6 @@
import Channel from "../channel.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { checkWordMute } from "backend-rs";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import type { Packed } from "@/misc/schema.js";
@ -86,7 +86,7 @@ export default class extends Channel {
if (
this.userProfile &&
this.user?.id !== note.userId &&
(await getWordHardMute(
(await checkWordMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,

View File

@ -1,6 +1,6 @@
import Channel from "../channel.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { checkWordMute } from "backend-rs";
import { isUserRelated } from "@/misc/is-user-related.js";
import type { Packed } from "@/misc/schema.js";
@ -64,7 +64,7 @@ export default class extends Channel {
if (
this.userProfile &&
this.user?.id !== note.userId &&
(await getWordHardMute(
(await checkWordMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,

View File

@ -1,6 +1,6 @@
import Channel from "../channel.js";
import { fetchMeta } from "@/misc/fetch-meta.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { checkWordMute } from "backend-rs";
import { isUserRelated } from "@/misc/is-user-related.js";
import { isInstanceMuted } from "@/misc/is-instance-muted.js";
import type { Packed } from "@/misc/schema.js";
@ -84,7 +84,7 @@ export default class extends Channel {
if (
this.userProfile &&
this.user?.id !== note.userId &&
(await getWordHardMute(
(await checkWordMute(
note,
this.userProfile.mutedWords,
this.userProfile.mutedPatterns,

View File

@ -44,7 +44,7 @@ import { Poll } from "@/models/entities/poll.js";
import { createNotification } from "@/services/create-notification.js";
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
import { checkHitAntenna } from "@/misc/check-hit-antenna.js";
import { getWordHardMute } from "@/misc/check-word-mute.js";
import { checkWordMute } from "backend-rs";
import { addNoteToAntenna } from "@/services/add-note-to-antenna.js";
import { countSameRenotes } from "@/misc/count-same-renotes.js";
import { deliverToRelays, getCachedRelays } from "../relay.js";
@ -380,7 +380,7 @@ export default async (
.then((us) => {
for (const u of us) {
if (u.userId === user.id) return;
getWordHardMute(note, u.mutedWords, u.mutedPatterns).then(
checkWordMute(note, u.mutedWords, u.mutedPatterns).then(
(shouldMute: boolean) => {
if (shouldMute) {
MutedNotes.insert({