From 452e0b921cdb9d3543d1e75f4226bf29888b8ca5 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Fri, 12 Apr 2024 06:10:11 +0900
Subject: [PATCH] refactor (backend): port acct to backend-rs

---
 packages/backend-rs/index.js                  |  4 +-
 packages/backend-rs/src/misc/acct.rs          | 73 +++++++++++++++++++
 packages/backend-rs/src/misc/mod.rs           |  1 +
 packages/backend/src/misc/acct.ts             | 14 ----
 .../backend/src/misc/check-hit-antenna.ts     |  4 +-
 .../queue/processors/db/import-blocking.ts    |  4 +-
 .../queue/processors/db/import-following.ts   |  6 +-
 .../src/queue/processors/db/import-muting.ts  |  5 +-
 .../queue/processors/db/import-user-lists.ts  |  5 +-
 .../src/server/api/endpoints/i/known-as.ts    |  4 +-
 .../src/server/api/endpoints/i/move.ts        |  4 +-
 .../src/server/api/endpoints/pinned-users.ts  |  4 +-
 packages/backend/src/server/index.ts          |  4 +-
 packages/backend/src/server/web/index.ts      |  8 +-
 packages/backend/src/server/well-known.ts     |  6 +-
 .../src/services/send-email-notification.ts   |  6 +-
 16 files changed, 106 insertions(+), 46 deletions(-)
 create mode 100644 packages/backend-rs/src/misc/acct.rs
 delete mode 100644 packages/backend/src/misc/acct.ts

diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index 7b66fdb6de..d3a197e5f0 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,8 +310,10 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr, IdConvertType, convertId } = nativeBinding
+const { stringToAcct, acctToString, nyaify, AntennaSrcEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr, IdConvertType, convertId } = nativeBinding
 
+module.exports.stringToAcct = stringToAcct
+module.exports.acctToString = acctToString
 module.exports.nyaify = nyaify
 module.exports.AntennaSrcEnum = AntennaSrcEnum
 module.exports.MutedNoteReasonEnum = MutedNoteReasonEnum
diff --git a/packages/backend-rs/src/misc/acct.rs b/packages/backend-rs/src/misc/acct.rs
new file mode 100644
index 0000000000..c24ff06dd4
--- /dev/null
+++ b/packages/backend-rs/src/misc/acct.rs
@@ -0,0 +1,73 @@
+#[derive(Debug, PartialEq)]
+#[cfg_attr(feature = "napi", crate::export(object, use_nullable = true))]
+pub struct Acct {
+    pub username: String,
+    pub host: Option<String>,
+}
+
+#[cfg_attr(feature = "napi", crate::export)]
+pub fn string_to_acct(acct: &str) -> Acct {
+    let split: Vec<&str> = if let Some(stripped) = acct.strip_prefix('@') {
+        stripped
+    } else {
+        &acct
+    }
+    .split('@')
+    .collect();
+
+    Acct {
+        username: split[0].to_string(),
+        host: if split.len() == 1 {
+            None
+        } else {
+            Some(split[1].to_string())
+        },
+    }
+}
+
+#[cfg_attr(feature = "napi", crate::export)]
+pub fn acct_to_string(acct: &Acct) -> String {
+    match &acct.host {
+        Some(host) => format!("{}@{}", acct.username, host),
+        None => acct.username.clone(),
+    }
+}
+
+#[cfg(test)]
+mod unit_test {
+    use super::{acct_to_string, string_to_acct, Acct};
+
+    #[test]
+    fn test_acct_to_string() {
+        let remote_acct = Acct {
+            username: "firefish".to_string(),
+            host: Some("example.com".to_string()),
+        };
+        let local_acct = Acct {
+            username: "MisakaMikoto".to_string(),
+            host: None,
+        };
+
+        assert_eq!(acct_to_string(&remote_acct), "firefish@example.com");
+        assert_ne!(acct_to_string(&remote_acct), "mastodon@example.com");
+        assert_eq!(acct_to_string(&local_acct), "MisakaMikoto");
+        assert_ne!(acct_to_string(&local_acct), "ShiraiKuroko");
+    }
+
+    #[test]
+    fn test_string_to_acct() {
+        let remote_acct = Acct {
+            username: "firefish".to_string(),
+            host: Some("example.com".to_string()),
+        };
+        let local_acct = Acct {
+            username: "MisakaMikoto".to_string(),
+            host: None,
+        };
+
+        assert_eq!(string_to_acct("@firefish@example.com"), remote_acct);
+        assert_eq!(string_to_acct("firefish@example.com"), remote_acct);
+        assert_eq!(string_to_acct("@MisakaMikoto"), local_acct);
+        assert_eq!(string_to_acct("MisakaMikoto"), local_acct);
+    }
+}
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 6c5d7c4f2e..02cdbfb404 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -1 +1,2 @@
+pub mod acct;
 pub mod nyaify;
diff --git a/packages/backend/src/misc/acct.ts b/packages/backend/src/misc/acct.ts
deleted file mode 100644
index cb6808b4b4..0000000000
--- a/packages/backend/src/misc/acct.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-export type Acct = {
-	username: string;
-	host: string | null;
-};
-
-export function parse(acct: string): Acct {
-	if (acct.startsWith("@")) acct = acct.slice(1);
-	const split = acct.split("@", 2);
-	return { username: split[0], host: split[1] || null };
-}
-
-export function toString(acct: Acct): string {
-	return acct.host == null ? acct.username : `${acct.username}@${acct.host}`;
-}
diff --git a/packages/backend/src/misc/check-hit-antenna.ts b/packages/backend/src/misc/check-hit-antenna.ts
index b93cb459e8..a2090934b6 100644
--- a/packages/backend/src/misc/check-hit-antenna.ts
+++ b/packages/backend/src/misc/check-hit-antenna.ts
@@ -4,7 +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 * as Acct from "@/misc/acct.js";
+import { stringToAcct } from "backend-rs";
 import { getWordHardMute } from "@/misc/check-word-mute.js";
 import type { Packed } from "@/misc/schema.js";
 import { Cache } from "@/misc/cache.js";
@@ -30,7 +30,7 @@ export async function checkHitAntenna(
 
 	if (antenna.src === "users") {
 		const accts = antenna.users.map((x) => {
-			const { username, host } = Acct.parse(x);
+			const { username, host } = stringToAcct(x);
 			return getFullApAccount(username, host).toLowerCase();
 		});
 		if (
diff --git a/packages/backend/src/queue/processors/db/import-blocking.ts b/packages/backend/src/queue/processors/db/import-blocking.ts
index 159ccbfd4a..bb1920d2a8 100644
--- a/packages/backend/src/queue/processors/db/import-blocking.ts
+++ b/packages/backend/src/queue/processors/db/import-blocking.ts
@@ -1,7 +1,7 @@
 import type Bull from "bull";
 
 import { queueLogger } from "../../logger.js";
-import * as Acct from "@/misc/acct.js";
+import { stringToAcct } from "backend-rs";
 import { resolveUser } from "@/remote/resolve-user.js";
 import { downloadTextFile } from "@/misc/download-text-file.js";
 import { isSelfHost, toPuny } from "@/misc/convert-host.js";
@@ -42,7 +42,7 @@ export async function importBlocking(
 
 		try {
 			const acct = line.split(",")[0].trim();
-			const { username, host } = Acct.parse(acct);
+			const { username, host } = stringToAcct(acct);
 
 			let target = isSelfHost(host!)
 				? await Users.findOneBy({
diff --git a/packages/backend/src/queue/processors/db/import-following.ts b/packages/backend/src/queue/processors/db/import-following.ts
index d2c2430fa3..f7aa5afedd 100644
--- a/packages/backend/src/queue/processors/db/import-following.ts
+++ b/packages/backend/src/queue/processors/db/import-following.ts
@@ -1,7 +1,7 @@
 import { IsNull } from "typeorm";
 import follow from "@/services/following/create.js";
 
-import * as Acct from "@/misc/acct.js";
+import { stringToAcct } from "backend-rs";
 import { resolveUser } from "@/remote/resolve-user.js";
 import { downloadTextFile } from "@/misc/download-text-file.js";
 import { isSelfHost, toPuny } from "@/misc/convert-host.js";
@@ -40,7 +40,7 @@ export async function importFollowing(
 	if (file.type.endsWith("json")) {
 		for (const acct of JSON.parse(csv)) {
 			try {
-				const { username, host } = Acct.parse(acct);
+				const { username, host } = stringToAcct(acct);
 
 				let target = isSelfHost(host!)
 					? await Users.findOneBy({
@@ -78,7 +78,7 @@ export async function importFollowing(
 
 			try {
 				const acct = line.split(",")[0].trim();
-				const { username, host } = Acct.parse(acct);
+				const { username, host } = stringToAcct(acct);
 
 				let target = isSelfHost(host!)
 					? await Users.findOneBy({
diff --git a/packages/backend/src/queue/processors/db/import-muting.ts b/packages/backend/src/queue/processors/db/import-muting.ts
index cd42a3281d..cdd8eab88d 100644
--- a/packages/backend/src/queue/processors/db/import-muting.ts
+++ b/packages/backend/src/queue/processors/db/import-muting.ts
@@ -1,14 +1,13 @@
 import type Bull from "bull";
 
 import { queueLogger } from "../../logger.js";
-import * as Acct from "@/misc/acct.js";
 import { resolveUser } from "@/remote/resolve-user.js";
 import { downloadTextFile } from "@/misc/download-text-file.js";
 import { isSelfHost, toPuny } from "@/misc/convert-host.js";
 import { Users, DriveFiles, Mutings } from "@/models/index.js";
 import type { DbUserImportJobData } from "@/queue/types.js";
 import type { User } from "@/models/entities/user.js";
-import { genId } from "backend-rs";
+import { genId, stringToAcct } from "backend-rs";
 import { IsNull } from "typeorm";
 import { inspect } from "node:util";
 
@@ -43,7 +42,7 @@ export async function importMuting(
 
 		try {
 			const acct = line.split(",")[0].trim();
-			const { username, host } = Acct.parse(acct);
+			const { username, host } = stringToAcct(acct);
 
 			let target = isSelfHost(host!)
 				? await Users.findOneBy({
diff --git a/packages/backend/src/queue/processors/db/import-user-lists.ts b/packages/backend/src/queue/processors/db/import-user-lists.ts
index 1ea6aa84ba..9995b0d0a3 100644
--- a/packages/backend/src/queue/processors/db/import-user-lists.ts
+++ b/packages/backend/src/queue/processors/db/import-user-lists.ts
@@ -1,7 +1,6 @@
 import type Bull from "bull";
 
 import { queueLogger } from "../../logger.js";
-import * as Acct from "@/misc/acct.js";
 import { resolveUser } from "@/remote/resolve-user.js";
 import { pushUserToUserList } from "@/services/user-list/push.js";
 import { downloadTextFile } from "@/misc/download-text-file.js";
@@ -12,7 +11,7 @@ import {
 	UserLists,
 	UserListJoinings,
 } from "@/models/index.js";
-import { genId } from "backend-rs";
+import { genId, stringToAcct } from "backend-rs";
 import type { DbUserImportJobData } from "@/queue/types.js";
 import { IsNull } from "typeorm";
 import { inspect } from "node:util";
@@ -48,7 +47,7 @@ export async function importUserLists(
 
 		try {
 			const listName = line.split(",")[0].trim();
-			const { username, host } = Acct.parse(line.split(",")[1].trim());
+			const { username, host } = stringToAcct(line.split(",")[1].trim());
 
 			let list = await UserLists.findOneBy({
 				userId: user.id,
diff --git a/packages/backend/src/server/api/endpoints/i/known-as.ts b/packages/backend/src/server/api/endpoints/i/known-as.ts
index 070d6d7f00..9eaeedb39f 100644
--- a/packages/backend/src/server/api/endpoints/i/known-as.ts
+++ b/packages/backend/src/server/api/endpoints/i/known-as.ts
@@ -8,7 +8,7 @@ import { DAY } from "@/const.js";
 import { apiLogger } from "@/server/api/logger.js";
 import define from "@/server/api/define.js";
 import { ApiError } from "@/server/api/error.js";
-import { parse } from "@/misc/acct.js";
+import { stringToAcct } from "backend-rs";
 import { inspect } from "node:util";
 
 export const meta = {
@@ -72,7 +72,7 @@ export default define(meta, paramDef, async (ps, user) => {
 
 	for (const line of ps.alsoKnownAs) {
 		if (!line) throw new ApiError(meta.errors.noSuchUser);
-		const { username, host } = parse(line);
+		const { username, host } = stringToAcct(line);
 
 		const aka = await resolveUser(username, host).catch((e) => {
 			apiLogger.warn(`failed to resolve remote user:\n${inspect(e)}`);
diff --git a/packages/backend/src/server/api/endpoints/i/move.ts b/packages/backend/src/server/api/endpoints/i/move.ts
index 53379c76c3..4784d3ee20 100644
--- a/packages/backend/src/server/api/endpoints/i/move.ts
+++ b/packages/backend/src/server/api/endpoints/i/move.ts
@@ -12,7 +12,7 @@ import { getUser } from "@/server/api/common/getters.js";
 import { Followings, Users } from "@/models/index.js";
 import config from "@/config/index.js";
 import { publishMainStream } from "@/services/stream.js";
-import { parse } from "@/misc/acct.js";
+import { stringToAcct } from "backend-rs";
 import { inspect } from "node:util";
 
 export const meta = {
@@ -90,7 +90,7 @@ export default define(meta, paramDef, async (ps, user) => {
 	if (!ps.moveToAccount) throw new ApiError(meta.errors.noSuchMoveTarget);
 	if (user.movedToUri) throw new ApiError(meta.errors.alreadyMoved);
 
-	const { username, host } = parse(ps.moveToAccount);
+	const { username, host } = stringToAcct(ps.moveToAccount);
 	if (!host) throw new ApiError(meta.errors.notRemote);
 
 	const moveTo: User = await resolveUser(username, host).catch((e) => {
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index b327378700..6d6519e47b 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -1,7 +1,7 @@
 import { IsNull } from "typeorm";
 import { Users } from "@/models/index.js";
 import { fetchMeta } from "@/misc/fetch-meta.js";
-import * as Acct from "@/misc/acct.js";
+import { stringToAcct } from "backend-rs";
 import type { User } from "@/models/entities/user.js";
 import define from "@/server/api/define.js";
 
@@ -35,7 +35,7 @@ export default define(meta, paramDef, async (ps, me) => {
 
 	const users = await Promise.all(
 		meta.pinnedUsers
-			.map((acct) => Acct.parse(acct))
+			.map((acct) => stringToAcct(acct))
 			.map((acct) =>
 				Users.findOneBy({
 					usernameLower: acct.username.toLowerCase(),
diff --git a/packages/backend/src/server/index.ts b/packages/backend/src/server/index.ts
index 28d62a3ac3..2a6dfdf674 100644
--- a/packages/backend/src/server/index.ts
+++ b/packages/backend/src/server/index.ts
@@ -19,7 +19,7 @@ import { Users } from "@/models/index.js";
 import { fetchMeta } from "@/misc/fetch-meta.js";
 import { genIdenticon } from "@/misc/gen-identicon.js";
 import { createTemp } from "@/misc/create-temp.js";
-import * as Acct from "@/misc/acct.js";
+import { stringToAcct } from "backend-rs";
 import { envOption } from "@/env.js";
 import megalodon, { MegalodonInterface } from "megalodon";
 import activityPub from "./activitypub.js";
@@ -108,7 +108,7 @@ router.use(nodeinfo.routes());
 router.use(wellKnown.routes());
 
 router.get("/avatar/@:acct", async (ctx) => {
-	const { username, host } = Acct.parse(ctx.params.acct);
+	const { username, host } = stringToAcct(ctx.params.acct);
 	const user = await Users.findOne({
 		where: {
 			usernameLower: username.toLowerCase(),
diff --git a/packages/backend/src/server/web/index.ts b/packages/backend/src/server/web/index.ts
index 676b9dc9fd..bb17cd279a 100644
--- a/packages/backend/src/server/web/index.ts
+++ b/packages/backend/src/server/web/index.ts
@@ -28,7 +28,7 @@ import {
 	Emojis,
 	GalleryPosts,
 } from "@/models/index.js";
-import * as Acct from "@/misc/acct.js";
+import { stringToAcct } from "backend-rs";
 import { getNoteSummary } from "@/misc/get-note-summary.js";
 import { queues } from "@/queue/queues.js";
 import { genOpenapiSpec } from "../api/openapi/gen-spec.js";
@@ -330,7 +330,7 @@ const getFeed = async (
 	if (meta.privateMode) {
 		return;
 	}
-	const { username, host } = Acct.parse(acct);
+	const { username, host } = stringToAcct(acct);
 	const user = await Users.findOneBy({
 		usernameLower: username.toLowerCase(),
 		host: host ?? IsNull(),
@@ -461,7 +461,7 @@ const jsonFeed: Router.Middleware = async (ctx) => {
 const userPage: Router.Middleware = async (ctx, next) => {
 	const userParam = ctx.params.user;
 	const subParam = ctx.params.sub;
-	const { username, host } = Acct.parse(userParam);
+	const { username, host } = stringToAcct(userParam);
 
 	const user = await Users.findOneBy({
 		usernameLower: username.toLowerCase(),
@@ -580,7 +580,7 @@ router.get("/posts/:note", async (ctx, next) => {
 
 // Page
 router.get("/@:user/pages/:page", async (ctx, next) => {
-	const { username, host } = Acct.parse(ctx.params.user);
+	const { username, host } = stringToAcct(ctx.params.user);
 	const user = await Users.findOneBy({
 		usernameLower: username.toLowerCase(),
 		host: host ?? IsNull(),
diff --git a/packages/backend/src/server/well-known.ts b/packages/backend/src/server/well-known.ts
index 1dc0f3d0a5..5177c36908 100644
--- a/packages/backend/src/server/well-known.ts
+++ b/packages/backend/src/server/well-known.ts
@@ -1,7 +1,7 @@
 import Router from "@koa/router";
 
 import config from "@/config/index.js";
-import * as Acct from "@/misc/acct.js";
+import { type Acct, stringToAcct } from "backend-rs";
 import { links } from "./nodeinfo.js";
 import { escapeAttribute, escapeValue } from "@/prelude/xml.js";
 import { Users } from "@/models/index.js";
@@ -110,7 +110,7 @@ router.get(webFingerPath, async (ctx) => {
 		resource.startsWith(`${config.url.toLowerCase()}/users/`)
 			? fromId(resource.split("/").pop()!)
 			: fromAcct(
-					Acct.parse(
+					stringToAcct(
 						resource.startsWith(`${config.url.toLowerCase()}/@`)
 							? resource.split("/").pop()!
 							: resource.startsWith("acct:")
@@ -119,7 +119,7 @@ router.get(webFingerPath, async (ctx) => {
 					),
 				);
 
-	const fromAcct = (acct: Acct.Acct): FindOptionsWhere<User> | number =>
+	const fromAcct = (acct: Acct): FindOptionsWhere<User> | number =>
 		!acct.host || acct.host === config.host.toLowerCase()
 			? {
 					usernameLower: acct.username,
diff --git a/packages/backend/src/services/send-email-notification.ts b/packages/backend/src/services/send-email-notification.ts
index aa8fa35ffe..8eed0a4e69 100644
--- a/packages/backend/src/services/send-email-notification.ts
+++ b/packages/backend/src/services/send-email-notification.ts
@@ -2,7 +2,7 @@
 import type { User } from "@/models/entities/user.js";
 // import { sendEmail } from "./send-email.js";
 // import { I18n } from "@/misc/i18n.js";
-// import * as Acct from "@/misc/acct.js";
+// import { acctToString } from "backend-rs";
 // TODO
 //const locales = await import('../../../../locales/index.js');
 
@@ -15,7 +15,7 @@ async function follow(userId: User["id"], follower: User) {
 	const locale = locales['en-US'];
 	const i18n = new I18n(locale);
 	// TODO: render user information html
-	sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
+	sendEmail(userProfile.email, i18n.t('_email._follow.title'), `${follower.name} (@${acctToString(follower)})`, `${follower.name} (@${acctToString(follower)})`);
 	*/
 }
 
@@ -26,7 +26,7 @@ async function receiveFollowRequest(userId: User["id"], follower: User) {
 	const locale = locales['en-US'];
 	const i18n = new I18n(locale);
 	// TODO: render user information html
-	sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${Acct.toString(follower)})`, `${follower.name} (@${Acct.toString(follower)})`);
+	sendEmail(userProfile.email, i18n.t('_email._receiveFollowRequest.title'), `${follower.name} (@${acctToString(follower)})`, `${follower.name} (@${acctToString(follower)})`);
 	*/
 }