From 80e3e0e91d030aa868fef1c61dbf1f5af4db76d9 Mon Sep 17 00:00:00 2001 From: naskya Date: Sun, 17 Mar 2024 02:59:30 +0900 Subject: [PATCH 01/18] refactor (client): compare Firefish versions lexicographically --- packages/client/src/init.ts | 7 +------ packages/client/src/pages/admin/index.vue | 4 +--- .../client/src/scripts/compare-versions.ts | 19 ------------------- packages/client/src/ui/_common_/navbar.vue | 4 +--- 4 files changed, 3 insertions(+), 31 deletions(-) delete mode 100644 packages/client/src/scripts/compare-versions.ts diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index 53d2062e15..c413fa37e4 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -40,7 +40,6 @@ import { i18n } from "@/i18n"; import { fetchInstance, instance } from "@/instance"; import { isSignedIn, me } from "@/me"; import { alert, api, confirm, popup, post, toast } from "@/os"; -import { compareFirefishVersions } from "@/scripts/compare-versions"; import { deviceKind } from "@/scripts/device-kind"; import { getAccountFromId } from "@/scripts/get-account-from-id"; import { makeHotkey } from "@/scripts/hotkey"; @@ -246,11 +245,7 @@ function checkForSplash() { try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため - if ( - lastVersion != null && - compareFirefishVersions(lastVersion, version) === 1 && - defaultStore.state.showUpdates - ) { + if (lastVersion < version && defaultStore.state.showUpdates) { // ログインしてる場合だけ if (me) { popup( diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index 53cdad840f..701a63e6c2 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -85,7 +85,6 @@ import { provideMetadataReceiver, } from "@/scripts/page-metadata"; import icon from "@/scripts/icon"; -import { compareFirefishVersions } from "@/scripts/compare-versions"; const isEmpty = (x: string | null) => x == null || x === ""; const el = ref(null); @@ -122,8 +121,7 @@ os.api("admin/abuse-user-reports", { if (defaultStore.state.showAdminUpdates) { os.api("latest-version").then((res) => { - updateAvailable.value = - compareFirefishVersions(version, res?.latest_version) === 1; + updateAvailable.value = version < res?.latest_version; }); } diff --git a/packages/client/src/scripts/compare-versions.ts b/packages/client/src/scripts/compare-versions.ts deleted file mode 100644 index 7b232db838..0000000000 --- a/packages/client/src/scripts/compare-versions.ts +++ /dev/null @@ -1,19 +0,0 @@ -const less = -1; -const same = 0; -const more = 1; - -export const compareFirefishVersions = ( - oldVersion: string, - newVersion: string, -) => { - if (oldVersion === newVersion) return same; - - const o = oldVersion.split("-"); - const n = newVersion.split("-"); - - if (o[0] < n[0]) return more; - if (o[0] === n[0] && o[1] == null && n[1] != null) return more; - if (o[0] === n[0] && o[1] != null && n[1] != null && o[1] < n[1]) return more; - - return less; -}; diff --git a/packages/client/src/ui/_common_/navbar.vue b/packages/client/src/ui/_common_/navbar.vue index e2937528de..0251a38c6a 100644 --- a/packages/client/src/ui/_common_/navbar.vue +++ b/packages/client/src/ui/_common_/navbar.vue @@ -162,7 +162,6 @@ import { i18n } from "@/i18n"; import { instance } from "@/instance"; import { version } from "@/config"; import icon from "@/scripts/icon"; -import { compareFirefishVersions } from "@/scripts/compare-versions"; const isEmpty = (x: string | null) => x == null || x === ""; @@ -212,8 +211,7 @@ if (isAdmin) { if (defaultStore.state.showAdminUpdates) { os.api("latest-version").then((res) => { - updateAvailable.value = - compareFirefishVersions(version, res?.latest_version) === 1; + updateAvailable.value = version < res?.latest_version; }); } From 80fc73efd54aa17c2145789b518ee8d3c36b73b4 Mon Sep 17 00:00:00 2001 From: naskya Date: Sun, 17 Mar 2024 13:08:54 +0900 Subject: [PATCH 02/18] locale: update search words description --- locales/en-US.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index f354d4c36e..edadfca386 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1198,7 +1198,9 @@ searchWordsDescription: "To search for posts, enter the search term. Separate wo search.\nFor example, 'morning night' will find posts that contain both 'morning' and 'night', and 'morning OR night' will find posts that contain either 'morning' or 'night' (or both).\nYou can also combine AND/OR conditions like '(morning OR - night) sleepy'.\n\nIf you want to go to a specific user page or post page, enter + night) sleepy'.\nIf you want to search for a sequence of words (e.g., a sentence), you + must put it in double quotes, not to make it an AND search: \"Today I learened\"\n\n + If you want to go to a specific user page or post page, enter the ID or URL in this field and click the 'Lookup' button. Clicking 'Search' will search for posts that literally contain the ID/URL." searchUsers: "Posted by (optional)" From afedb2e8f8e9269adc76dcefc4dbc92c11b2a37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8=E9=83=A8=E8=90=BD?= Date: Sun, 17 Mar 2024 03:13:34 +0000 Subject: [PATCH 03/18] locale: update translations (Chinese (Simplified)) Currently translated at 100.0% (1915 of 1915 strings) Translation: Firefish/locales Translate-URL: https://hosted.weblate.org/projects/firefish/locales/zh_Hans/ --- locales/zh-CN.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 30677ef021..16a32783af 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -2053,5 +2053,5 @@ searchRangeDescription: "如果您要过滤时间段,请按以下格式输入 或 20231105-0110),它将被解释为当前年份。\n\n您还可以省略开始日期或结束日期。 例如 -0102 将过滤搜索结果以仅显示今年 1 月 2 日之前发布的帖子,而 20231026- 将过滤结果以仅显示 2023 年 10 月 26 日之后发布的帖子。" messagingUnencryptedInfo: "Firefish 上的聊天没有经过端到端加密,请不要在聊天中分享您的敏感信息。" -noAltTextWarning: 有些附件没有说明。您是否忘记写说明了? -showNoAltTextWarning: 当您尝试发布没有说明的帖子附件时显示警告 +noAltTextWarning: 有些附件没有描述。您是否忘记写描述了? +showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告 From 786eb0b1a9d1dc834fa5ff3265325a816e390cba Mon Sep 17 00:00:00 2001 From: naskya Date: Sun, 17 Mar 2024 04:17:35 +0000 Subject: [PATCH 04/18] locale: update translations (Japanese) Currently translated at 100.0% (1915 of 1915 strings) Translation: Firefish/locales Translate-URL: https://hosted.weblate.org/projects/firefish/locales/ja/ --- locales/ja-JP.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index abbce70fcc..c61187a510 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1008,7 +1008,8 @@ enableTimelineStreaming: "タイムラインを自動で更新する" searchWords: "検索語句・照会するIDやURL" searchWordsDescription: "投稿を検索するには、ここに検索語句を入力してください。空白区切りでAND検索になり、ORを挟むとOR検索になります。\n 例えば「朝 夜」と入力すると「朝」と「夜」が両方含まれた投稿を検索し、「朝 OR 夜」と入力すると「朝」または「夜」(または両方)が含まれた投稿を検索します。\n - 「(朝 OR 夜) 眠い」のように、AND検索とOR検索を同時に行うこともできます。\n\n特定のユーザーや投稿のページに飛びたい場合には、この欄にID (@user@example.com) + 「(朝 OR 夜) 眠い」のように、AND検索とOR検索を同時に行うこともできます。\n空白を含む文字列をAND検索ではなくそのまま検索したい場合、\"明日 買うもの\"\ + \ のように二重引用符 (\") で囲む必要があります。\n\n特定のユーザーや投稿のページに飛びたい場合には、この欄にID (@user@example.com) や投稿のURLを入力し「照会」を押してください。「検索」を押すとそのIDやURLが文字通り含まれる投稿を検索します。" searchUsers: "投稿元(オプション)" searchUsersDescription: "投稿検索で投稿者を絞りたい場合、@user@example.com(ローカルユーザーなら @user)の形式で投稿者のIDを入力してください。ユーザーIDではなくドメイン名 From 11b613f752013a8fb4cd4e247c141088bf2eedd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8=E9=83=A8=E8=90=BD?= Date: Sun, 17 Mar 2024 04:37:20 +0000 Subject: [PATCH 05/18] locale: update translations (Chinese (Simplified)) Currently translated at 100.0% (1915 of 1915 strings) Translation: Firefish/locales Translate-URL: https://hosted.weblate.org/projects/firefish/locales/zh_Hans/ --- locales/zh-CN.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 16a32783af..399255cd57 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1936,7 +1936,7 @@ moveFrom: 从旧账号迁移至此账号 defaultReaction: 发出和收到帖子的默认表情符号反应 sendModMail: 发送管理通知 moderationNote: "管理笔记" -ipFirstAcknowledged: "该日期是这个 IP 地址首次被获取到的日期" +ipFirstAcknowledged: "首次获取此 IP 地址的日期" driveCapacityOverride: "网盘容量变更" isLocked: 该账号设置了关注请求 _filters: @@ -2047,8 +2047,9 @@ publishTimelines: 为访客发布时间线 publishTimelinesDescription: 如果启用,在用户登出时本地和全局时间线也会显示在 {url} 上。 searchWordsDescription: "要搜索帖子,请输入关键词。交集搜索关键词之间使用空格进行区分,并集搜索关键词之间使用 OR 进行区分。\n例如 '早上 晚上' 将查找包含 '早上' 和 '晚上' 的帖子,而 '早上 OR 晚上' 将查找包含 '早上' 或 '晚上' (以及同时包含两者)的帖子。\n您还可以组合交集/并集条件,例如 - '(早上 OR 晚上) 困了' 。\n\n如果您想转到特定的用户页面或帖子页面,请在此字段中输入用户 ID 或 URL,然后单击 “查询” 按钮。 单击 “搜索” - 将搜索字面包含用户 ID/URL 的帖子。" + '(早上 OR 晚上) 困了' 。\n如果您想搜索单词序列(例如一个英语句子),您必须将其放在双引号中,例如 \"Today I learened\" 以区分于交集搜索。\n + \n如果您想转到特定的用户页面或帖子页面,请在此字段中输入用户 ID 或 URL,然后单击 “查询” 按钮。 单击 “搜索” 将搜索字面包含用户 ID/URL + 的帖子。" searchRangeDescription: "如果您要过滤时间段,请按以下格式输入:20220615-20231031\n\n如果您省略年份(例如 0105-0106 或 20231105-0110),它将被解释为当前年份。\n\n您还可以省略开始日期或结束日期。 例如 -0102 将过滤搜索结果以仅显示今年 1 月 2 日之前发布的帖子,而 20231026- 将过滤结果以仅显示 2023 年 10 月 26 日之后发布的帖子。" From 0f880b55e3a58e1170c904bc11acc86e44cab5f4 Mon Sep 17 00:00:00 2001 From: naskya Date: Sun, 17 Mar 2024 15:26:27 +0900 Subject: [PATCH 06/18] chore (backend): add FIXME to a questionable database column --- packages/backend/src/models/entities/note.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index e45fb02466..5dca038ac2 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -151,6 +151,7 @@ export class Note { }) public score: number; + // FIXME: file id is not removed from this array even if the file is deleted @Index() @Column({ ...id(), @@ -183,6 +184,7 @@ export class Note { }) public mentions: User["id"][]; + // FIXME: WHAT IS THIS @Column("text", { default: "[]", }) From 2220d5c56e3cd493e693e0f1df52591ff56d5c31 Mon Sep 17 00:00:00 2001 From: naskya Date: Sun, 17 Mar 2024 20:56:54 +0900 Subject: [PATCH 07/18] refactor (backend): add note_file table to store (noteId, fileId) pairs Co-authored-by: sup39 --- docs/downgrade.sql | 4 ++ .../backend-rs/src/model/entity/drive_file.rs | 8 +++ packages/backend-rs/src/model/entity/mod.rs | 1 + packages/backend-rs/src/model/entity/note.rs | 8 +++ .../backend-rs/src/model/entity/note_file.rs | 48 +++++++++++++ .../backend-rs/src/model/entity/prelude.rs | 1 + packages/backend/src/db/postgre.ts | 2 + .../src/migration/1710304584214-note-file.ts | 41 +++++++++++ packages/backend/src/misc/schema.ts | 2 + .../backend/src/models/entities/drive-file.ts | 43 +++++++---- .../backend/src/models/entities/note-file.ts | 45 ++++++++++++ packages/backend/src/models/entities/note.ts | 71 +++++++++++++------ packages/backend/src/models/index.ts | 2 + .../src/models/repositories/note-file.ts | 4 ++ .../backend/src/models/schema/note-file.ts | 24 +++++++ packages/backend/src/services/note/create.ts | 7 ++ 16 files changed, 279 insertions(+), 32 deletions(-) create mode 100644 packages/backend-rs/src/model/entity/note_file.rs create mode 100644 packages/backend/src/migration/1710304584214-note-file.ts create mode 100644 packages/backend/src/models/entities/note-file.ts create mode 100644 packages/backend/src/models/repositories/note-file.ts create mode 100644 packages/backend/src/models/schema/note-file.ts diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 64622c3af7..192800257b 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'NoteFile1710304584214', 'RenameMetaColumns1705944717480', 'SeparateHardMuteWordsAndPatterns1706413792769', 'IndexAltTextAndCw1708872574733', @@ -16,6 +17,9 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); +-- note-file +DROP TABLE "note_file"; + -- rename-meta-columns ALTER TABLE "meta" RENAME COLUMN "tosUrl" TO "ToSUrl"; ALTER TABLE "meta" RENAME COLUMN "objectStorageUseSsl" TO "objectStorageUseSSL"; diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs index 1a7a0f35b1..a5f7e9bfca 100644 --- a/packages/backend-rs/src/model/entity/drive_file.rs +++ b/packages/backend-rs/src/model/entity/drive_file.rs @@ -64,6 +64,8 @@ pub enum Relation { DriveFolder, #[sea_orm(has_many = "super::messaging_message::Entity")] MessagingMessage, + #[sea_orm(has_many = "super::note_file::Entity")] + NoteFile, #[sea_orm(has_many = "super::page::Entity")] Page, #[sea_orm( @@ -94,6 +96,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::NoteFile.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::Page.def() diff --git a/packages/backend-rs/src/model/entity/mod.rs b/packages/backend-rs/src/model/entity/mod.rs index 38be7561b1..223b07fb4f 100644 --- a/packages/backend-rs/src/model/entity/mod.rs +++ b/packages/backend-rs/src/model/entity/mod.rs @@ -35,6 +35,7 @@ pub mod muting; pub mod note; pub mod note_edit; pub mod note_favorite; +pub mod note_file; pub mod note_reaction; pub mod note_thread_muting; pub mod note_unread; diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs index 4774cae952..c851c776e2 100644 --- a/packages/backend-rs/src/model/entity/note.rs +++ b/packages/backend-rs/src/model/entity/note.rs @@ -100,6 +100,8 @@ pub enum Relation { NoteEdit, #[sea_orm(has_many = "super::note_favorite::Entity")] NoteFavorite, + #[sea_orm(has_many = "super::note_file::Entity")] + NoteFile, #[sea_orm(has_many = "super::note_reaction::Entity")] NoteReaction, #[sea_orm(has_many = "super::note_unread::Entity")] @@ -164,6 +166,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::NoteFile.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::NoteReaction.def() diff --git a/packages/backend-rs/src/model/entity/note_file.rs b/packages/backend-rs/src/model/entity/note_file.rs new file mode 100644 index 0000000000..c75222360b --- /dev/null +++ b/packages/backend-rs/src/model/entity/note_file.rs @@ -0,0 +1,48 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "note_file")] +pub struct Model { + #[sea_orm(column_name = "serialNo", primary_key)] + pub serial_no: i64, + #[sea_orm(column_name = "noteId")] + pub note_id: String, + #[sea_orm(column_name = "fileId")] + pub file_id: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::drive_file::Entity", + from = "Column::FileId", + to = "super::drive_file::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + DriveFile, + #[sea_orm( + belongs_to = "super::note::Entity", + from = "Column::NoteId", + to = "super::note::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Note, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::DriveFile.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Note.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/packages/backend-rs/src/model/entity/prelude.rs b/packages/backend-rs/src/model/entity/prelude.rs index 10d3795362..0935ac9ff7 100644 --- a/packages/backend-rs/src/model/entity/prelude.rs +++ b/packages/backend-rs/src/model/entity/prelude.rs @@ -33,6 +33,7 @@ pub use super::muting::Entity as Muting; pub use super::note::Entity as Note; pub use super::note_edit::Entity as NoteEdit; pub use super::note_favorite::Entity as NoteFavorite; +pub use super::note_file::Entity as NoteFile; pub use super::note_reaction::Entity as NoteReaction; pub use super::note_thread_muting::Entity as NoteThreadMuting; pub use super::note_unread::Entity as NoteUnread; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index b14f028e8b..b6c3f0db8f 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -73,6 +73,7 @@ import { UserPending } from "@/models/entities/user-pending.js"; import { Webhook } from "@/models/entities/webhook.js"; import { UserIp } from "@/models/entities/user-ip.js"; import { NoteEdit } from "@/models/entities/note-edit.js"; +import { NoteFile } from "@/models/entities/note-file.js"; import { entities as charts } from "@/services/chart/entities.js"; import { dbLogger } from "./logger.js"; @@ -143,6 +144,7 @@ export const entities = [ Note, NoteEdit, NoteFavorite, + NoteFile, NoteReaction, NoteWatching, NoteThreadMuting, diff --git a/packages/backend/src/migration/1710304584214-note-file.ts b/packages/backend/src/migration/1710304584214-note-file.ts new file mode 100644 index 0000000000..29c340f2cf --- /dev/null +++ b/packages/backend/src/migration/1710304584214-note-file.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class NoteFile1710304584214 implements MigrationInterface { + name = "NoteFile1710304584214"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "note_file" ( + "serialNo" bigserial PRIMARY KEY, + "noteId" varchar(32) NOT NULL, + "fileId" varchar(32) NOT NULL + )`, + ); + await queryRunner.query(` + INSERT INTO "note_file" ("noteId", "fileId") + SELECT "t"."id", "t"."fid" FROM ( + SELECT ROW_NUMBER() OVER () AS "rn", * FROM ( + SELECT "id", UNNEST("fileIds") AS "fid" FROM "note" + ) + ) AS "t" + INNER JOIN "drive_file" ON "drive_file"."id" = "t"."fid" + ORDER BY "rn" + `); + await queryRunner.query( + `ALTER TABLE "note_file" ADD FOREIGN KEY ("noteId") REFERENCES "note" ("id") ON DELETE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "note_file" ADD FOREIGN KEY ("fileId") REFERENCES "drive_file" ("id") ON DELETE CASCADE`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_note_file_noteId" ON "note_file" ("noteId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_note_file_fileId" ON "note_file" ("fileId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "note_file"`); + } +} diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 9d698527c4..016b7159c9 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -32,6 +32,7 @@ import { packedQueueCountSchema } from "@/models/schema/queue.js"; import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js"; import { packedEmojiSchema } from "@/models/schema/emoji.js"; import { packedNoteEdit } from "@/models/schema/note-edit.js"; +import { packedNoteFileSchema } from "@/models/schema/note-file.js"; export const refs = { UserLite: packedUserLiteSchema, @@ -47,6 +48,7 @@ export const refs = { App: packedAppSchema, MessagingMessage: packedMessagingMessageSchema, Note: packedNoteSchema, + NoteFile: packedNoteFileSchema, NoteEdit: packedNoteEdit, NoteReaction: packedNoteReactionSchema, NoteFavorite: packedNoteFavoriteSchema, diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 5b31a7e336..3c4510b533 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -4,12 +4,17 @@ import { Index, JoinColumn, Column, + ManyToMany, ManyToOne, + OneToMany, + type Relation, } from "typeorm"; import { id } from "../id.js"; +import { Note } from "./note.js"; import { User } from "./user.js"; import { DriveFolder } from "./drive-folder.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; +import { NoteFile } from "./note-file.js"; @Entity() @Index(["userId", "folderId", "id"]) @@ -31,12 +36,6 @@ export class DriveFile { }) public userId: User["id"] | null; - @ManyToOne((type) => User, { - onDelete: "SET NULL", - }) - @JoinColumn() - public user: User | null; - @Index() @Column("varchar", { length: 512, @@ -171,12 +170,6 @@ export class DriveFile { }) public folderId: DriveFolder["id"] | null; - @ManyToOne((type) => DriveFolder, { - onDelete: "SET NULL", - }) - @JoinColumn() - public folder: DriveFolder | null; - @Index() @Column("boolean", { default: false, @@ -205,4 +198,30 @@ export class DriveFile { nullable: true, }) public requestIp: string | null; + + //#region Relations + @OneToMany( + () => NoteFile, + (noteFile: NoteFile) => noteFile.file, + ) + public noteFiles: Relation; + + @ManyToMany( + () => Note, + (note: Note) => note.files, + ) + public notes: Relation; + + @ManyToOne(() => User, { + onDelete: "SET NULL", + }) + @JoinColumn() + public user: User | null; + + @ManyToOne(() => DriveFolder, { + onDelete: "SET NULL", + }) + @JoinColumn() + public folder: DriveFolder | null; + //#endregion Relations } diff --git a/packages/backend/src/models/entities/note-file.ts b/packages/backend/src/models/entities/note-file.ts new file mode 100644 index 0000000000..7e7013e03a --- /dev/null +++ b/packages/backend/src/models/entities/note-file.ts @@ -0,0 +1,45 @@ +import { + Entity, + Index, + Column, + ManyToOne, + PrimaryGeneratedColumn, + type Relation, +} from "typeorm"; +import { Note } from "./note.js"; +import { DriveFile } from "./drive-file.js"; +import { id } from "../id.js"; + +@Entity() +export class NoteFile { + @PrimaryGeneratedColumn("increment") + public serialNo: number; + + @Index("IDX_note_file_noteId", { unique: false }) + @Column({ + ...id(), + nullable: false, + }) + public noteId: Note["id"]; + + @Index("IDX_note_file_fileId", { unique: false }) + @Column({ + ...id(), + nullable: false, + }) + public fileId: DriveFile["id"]; + + //#region Relations + @ManyToOne( + () => Note, + (note: Note) => note.noteFiles, + ) + public note: Relation; + + @ManyToOne( + () => DriveFile, + (file: DriveFile) => file.noteFiles, + ) + public file: Relation; + //#endregion Relations +} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 5dca038ac2..a31dd7dd46 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -2,15 +2,20 @@ import { Entity, Index, JoinColumn, + JoinTable, Column, PrimaryColumn, + ManyToMany, ManyToOne, + OneToMany, + type Relation, } from "typeorm"; import { User } from "./user.js"; -import type { DriveFile } from "./drive-file.js"; +import { DriveFile } from "./drive-file.js"; import { id } from "../id.js"; import { noteVisibilities } from "../../types.js"; import { Channel } from "./channel.js"; +import { NoteFile } from "./note-file.js"; @Entity() @Index("IDX_NOTE_TAGS", { synchronize: false }) @@ -34,12 +39,6 @@ export class Note { }) public replyId: Note["id"] | null; - @ManyToOne((type) => Note, { - onDelete: "CASCADE", - }) - @JoinColumn() - public reply: Note | null; - @Index() @Column({ ...id(), @@ -48,12 +47,6 @@ export class Note { }) public renoteId: Note["id"] | null; - @ManyToOne((type) => Note, { - onDelete: "CASCADE", - }) - @JoinColumn() - public renote: Note | null; - @Index() @Column("varchar", { length: 256, @@ -93,12 +86,6 @@ export class Note { }) public userId: User["id"]; - @ManyToOne((type) => User, { - onDelete: "CASCADE", - }) - @JoinColumn() - public user: User | null; - @Column("boolean", { default: false, }) @@ -152,6 +139,7 @@ export class Note { public score: number; // FIXME: file id is not removed from this array even if the file is deleted + // TODO: drop this column and use note_files @Index() @Column({ ...id(), @@ -218,12 +206,55 @@ export class Note { }) public channelId: Channel["id"] | null; - @ManyToOne((type) => Channel, { + //#region Relations + @OneToMany( + () => NoteFile, + (noteFile: NoteFile) => noteFile.note, + ) + public noteFiles: Relation; + + @ManyToMany( + () => DriveFile, + (file: DriveFile) => file.notes, + ) + @JoinTable({ + name: "note_file", + joinColumn: { + name: "noteId", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "fileId", + referencedColumnName: "id", + }, + }) + public files: Relation; + + @ManyToOne(() => Note, { + onDelete: "CASCADE", + }) + @JoinColumn() + public reply: Note | null; + + @ManyToOne(() => Note, { + onDelete: "CASCADE", + }) + @JoinColumn() + public renote: Note | null; + + @ManyToOne(() => Channel, { onDelete: "CASCADE", }) @JoinColumn() public channel: Channel | null; + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + @JoinColumn() + public user: User | null; + //#endregion Relations + //#region Denormalized fields @Index() @Column("varchar", { diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 8ae12a63df..5d4ff52198 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -66,12 +66,14 @@ import { InstanceRepository } from "./repositories/instance.js"; import { Webhook } from "./entities/webhook.js"; import { UserIp } from "./entities/user-ip.js"; import { NoteEdit } from "./entities/note-edit.js"; +import { NoteFileRepository } from "./repositories/note-file.js"; export const Announcements = db.getRepository(Announcement); export const AnnouncementReads = db.getRepository(AnnouncementRead); export const Apps = AppRepository; export const Notes = NoteRepository; export const NoteEdits = db.getRepository(NoteEdit); +export const NoteFiles = NoteFileRepository; export const NoteFavorites = NoteFavoriteRepository; export const NoteWatchings = db.getRepository(NoteWatching); export const NoteThreadMutings = db.getRepository(NoteThreadMuting); diff --git a/packages/backend/src/models/repositories/note-file.ts b/packages/backend/src/models/repositories/note-file.ts new file mode 100644 index 0000000000..f755fb5fea --- /dev/null +++ b/packages/backend/src/models/repositories/note-file.ts @@ -0,0 +1,4 @@ +import { db } from "@/db/postgre.js"; +import { NoteFile } from "@/models/entities/note-file.js"; + +export const NoteFileRepository = db.getRepository(NoteFile).extend({}); diff --git a/packages/backend/src/models/schema/note-file.ts b/packages/backend/src/models/schema/note-file.ts new file mode 100644 index 0000000000..c9b8a7e181 --- /dev/null +++ b/packages/backend/src/models/schema/note-file.ts @@ -0,0 +1,24 @@ +export const packedNoteFileSchema = { + type: "object", + properties: { + serialNo: { + type: "number", + optional: false, + nullable: false, + }, + noteId: { + type: "string", + optional: false, + nullable: false, + format: "id", + example: "xxxxxxxxxx", + }, + fileId: { + type: "string", + optional: false, + nullable: false, + format: "id", + example: "xxxxxxxxxx", + }, + }, +} as const; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index fc7f1265b3..fc9913e985 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -31,6 +31,7 @@ import { Channels, ChannelFollowings, NoteThreadMutings, + NoteFiles, } from "@/models/index.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import type { App } from "@/models/entities/app.js"; @@ -343,6 +344,12 @@ export default async ( const note = await insertNote(user, data, tags, emojis, mentionedUsers); + await NoteFiles.insert( + note.fileIds.map((fileId) => ({ noteId: note.id, fileId })), + ).catch((e) => { + logger.error(inspect(e)); + }); + res(note); // Register host From cca63b728699374953bec59c8908cdb8604e72b7 Mon Sep 17 00:00:00 2001 From: sup39 Date: Sun, 17 Mar 2024 22:37:58 +0900 Subject: [PATCH 08/18] perf (backend): improved post search with CW/alt text Co-authored-by: naskya --- .../src/server/api/endpoints/notes/search.ts | 182 ++++++++++-------- 1 file changed, 107 insertions(+), 75 deletions(-) diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index 0bc70d37f9..b159a91944 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,4 +1,3 @@ -import { Brackets } from "typeorm"; import { Notes } from "@/models/index.js"; import { Note } from "@/models/entities/note.js"; import define from "@/server/api/define.js"; @@ -7,6 +6,7 @@ import { generateVisibilityQuery } from "@/server/api/common/generate-visibility import { generateMutedUserQuery } from "@/server/api/common/generate-muted-user-query.js"; import { generateBlockedUserQuery } from "@/server/api/common/generate-block-query.js"; import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import type { SelectQueryBuilder } from "typeorm"; export const meta = { tags: ["notes"], @@ -69,91 +69,123 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { - const query = makePaginationQuery( - Notes.createQueryBuilder("note"), - ps.sinceId, - ps.untilId, - ps.sinceDate ?? undefined, - ps.untilDate ?? undefined, - ); + async function search( + modifier?: (query: SelectQueryBuilder) => void, + ): Promise { + const query = makePaginationQuery( + Notes.createQueryBuilder("note"), + ps.sinceId, + ps.untilId, + ps.sinceDate ?? undefined, + ps.untilDate ?? undefined, + ); + modifier?.(query); - if (ps.userId != null) { - query.andWhere("note.userId = :userId", { userId: ps.userId }); + if (ps.userId != null) { + query.andWhere("note.userId = :userId", { userId: ps.userId }); + } + + if (ps.channelId != null) { + query.andWhere("note.channelId = :channelId", { + channelId: ps.channelId, + }); + } + + query.innerJoinAndSelect("note.user", "user"); + + // "from: me": search all (public, home, followers, specified) my posts + // otherwise: search public indexable posts only + if (ps.userId == null || ps.userId !== me?.id) { + query + .andWhere("note.visibility = 'public'") + .andWhere("user.isIndexable = TRUE"); + } + + if (ps.userId != null) { + query.andWhere("note.userId = :userId", { userId: ps.userId }); + } + + if (ps.host === null) { + query.andWhere("note.userHost IS NULL"); + } + if (ps.host != null) { + query.andWhere("note.userHost = :userHost", { userHost: ps.host }); + } + + if (ps.withFiles === true) { + query.andWhere("note.fileIds != '{}'"); + } + + query + .leftJoinAndSelect("user.avatar", "avatar") + .leftJoinAndSelect("user.banner", "banner") + .leftJoinAndSelect("note.reply", "reply") + .leftJoinAndSelect("note.renote", "renote") + .leftJoinAndSelect("reply.user", "replyUser") + .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") + .leftJoinAndSelect("replyUser.banner", "replyUserBanner") + .leftJoinAndSelect("renote.user", "renoteUser") + .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") + .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); + + generateVisibilityQuery(query, me); + if (me) generateMutedUserQuery(query, me); + if (me) generateBlockedUserQuery(query, me); + + return await query.take(ps.limit).getMany(); } - if (ps.channelId != null) { - query.andWhere("note.channelId = :channelId", { - channelId: ps.channelId, - }); - } + let notes: Note[]; if (ps.query != null) { const q = sqlLikeEscape(ps.query); if (ps.searchCwAndAlt) { - query.andWhere( - new Brackets((qb) => { - qb.where("note.text &@~ :q", { q }) - .orWhere("note.cw &@~ :q", { q }) - .orWhere( - `EXISTS ( - SELECT FROM "drive_file" - WHERE - comment &@~ :q - AND - drive_file."id" = ANY(note."fileIds") - )`, - { q }, - ); - }), - ); + // Whether we should return latest notes first + const isDescendingOrder = + (ps.sinceId == null || ps.untilId != null) && + (ps.sinceId != null || + ps.untilId != null || + ps.sinceDate == null || + ps.untilDate != null); + + const compare = isDescendingOrder + ? (lhs: Note, rhs: Note) => + Math.sign(rhs.createdAt.getTime() - lhs.createdAt.getTime()) + : (lhs: Note, rhs: Note) => + Math.sign(lhs.createdAt.getTime() - rhs.createdAt.getTime()); + + notes = [ + ...new Map( + ( + await Promise.all([ + search((query) => { + query.andWhere("note.text &@~ :q", { q }); + }), + search((query) => { + query.andWhere("note.cw &@~ :q", { q }); + }), + search((query) => { + query + .andWhere("drive_file.comment &@~ :q", { q }) + .innerJoin("note.files", "drive_file"); + }), + ]) + ) + .flatMap((e) => e) + .map((note) => [note.id, note]), + ).values(), + ] + .sort(compare) + .slice(0, ps.limit); } else { - query.andWhere("note.text &@~ :q", { q }); + notes = await search((query) => { + query.andWhere("note.text &@~ :q", { q }); + }); } + } else { + notes = await search(); } - query.innerJoinAndSelect("note.user", "user"); - - // "from: me": search all (public, home, followers, specified) my posts - // otherwise: search public indexable posts only - if (ps.userId == null || ps.userId !== me?.id) { - query - .andWhere("note.visibility = 'public'") - .andWhere("user.isIndexable = TRUE"); - } - - if (ps.userId != null) { - query.andWhere("note.userId = :userId", { userId: ps.userId }); - } - - if (ps.host === null) { - query.andWhere("note.userHost IS NULL"); - } - if (ps.host != null) { - query.andWhere("note.userHost = :userHost", { userHost: ps.host }); - } - - if (ps.withFiles === true) { - query.andWhere("note.fileIds != '{}'"); - } - - query - .leftJoinAndSelect("user.avatar", "avatar") - .leftJoinAndSelect("user.banner", "banner") - .leftJoinAndSelect("note.reply", "reply") - .leftJoinAndSelect("note.renote", "renote") - .leftJoinAndSelect("reply.user", "replyUser") - .leftJoinAndSelect("replyUser.avatar", "replyUserAvatar") - .leftJoinAndSelect("replyUser.banner", "replyUserBanner") - .leftJoinAndSelect("renote.user", "renoteUser") - .leftJoinAndSelect("renoteUser.avatar", "renoteUserAvatar") - .leftJoinAndSelect("renoteUser.banner", "renoteUserBanner"); - - generateVisibilityQuery(query, me); - if (me) generateMutedUserQuery(query, me); - if (me) generateBlockedUserQuery(query, me); - - const notes: Note[] = await query.take(ps.limit).getMany(); - return await Notes.packMany(notes, me); }); From a72999c872ed272b366178fab0141e9efa58cc82 Mon Sep 17 00:00:00 2001 From: naskya Date: Sun, 17 Mar 2024 23:04:57 +0900 Subject: [PATCH 09/18] locale: fix typo I'm tired --- locales/en-US.yml | 2 +- locales/zh-CN.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index edadfca386..77564ec7bd 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1199,7 +1199,7 @@ searchWordsDescription: "To search for posts, enter the search term. Separate wo and 'night', and 'morning OR night' will find posts that contain either 'morning' or 'night' (or both).\nYou can also combine AND/OR conditions like '(morning OR night) sleepy'.\nIf you want to search for a sequence of words (e.g., a sentence), you - must put it in double quotes, not to make it an AND search: \"Today I learened\"\n\n + must put it in double quotes, not to make it an AND search: \"Today I learned\"\n\n If you want to go to a specific user page or post page, enter the ID or URL in this field and click the 'Lookup' button. Clicking 'Search' will search for posts that literally contain the ID/URL." diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 399255cd57..2e220950d8 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -2047,7 +2047,7 @@ publishTimelines: 为访客发布时间线 publishTimelinesDescription: 如果启用,在用户登出时本地和全局时间线也会显示在 {url} 上。 searchWordsDescription: "要搜索帖子,请输入关键词。交集搜索关键词之间使用空格进行区分,并集搜索关键词之间使用 OR 进行区分。\n例如 '早上 晚上' 将查找包含 '早上' 和 '晚上' 的帖子,而 '早上 OR 晚上' 将查找包含 '早上' 或 '晚上' (以及同时包含两者)的帖子。\n您还可以组合交集/并集条件,例如 - '(早上 OR 晚上) 困了' 。\n如果您想搜索单词序列(例如一个英语句子),您必须将其放在双引号中,例如 \"Today I learened\" 以区分于交集搜索。\n + '(早上 OR 晚上) 困了' 。\n如果您想搜索单词序列(例如一个英语句子),您必须将其放在双引号中,例如 \"Today I learned\" 以区分于交集搜索。\n \n如果您想转到特定的用户页面或帖子页面,请在此字段中输入用户 ID 或 URL,然后单击 “查询” 按钮。 单击 “搜索” 将搜索字面包含用户 ID/URL 的帖子。" searchRangeDescription: "如果您要过滤时间段,请按以下格式输入:20220615-20231031\n\n如果您省略年份(例如 0105-0106 From 9bce737f67f03ae62f63a621176dd29cf60e9914 Mon Sep 17 00:00:00 2001 From: sup39 Date: Sun, 17 Mar 2024 23:13:54 +0900 Subject: [PATCH 10/18] fix (backend): add alias to a migration subquery Co-authored-by: naskya --- packages/backend/src/migration/1710304584214-note-file.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/migration/1710304584214-note-file.ts b/packages/backend/src/migration/1710304584214-note-file.ts index 29c340f2cf..be0458d297 100644 --- a/packages/backend/src/migration/1710304584214-note-file.ts +++ b/packages/backend/src/migration/1710304584214-note-file.ts @@ -16,7 +16,7 @@ export class NoteFile1710304584214 implements MigrationInterface { SELECT "t"."id", "t"."fid" FROM ( SELECT ROW_NUMBER() OVER () AS "rn", * FROM ( SELECT "id", UNNEST("fileIds") AS "fid" FROM "note" - ) + ) AS "s" ) AS "t" INNER JOIN "drive_file" ON "drive_file"."id" = "t"."fid" ORDER BY "rn" From 65797a934ac02bd7ad31ec6060f070196d19e2c4 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Wed, 24 Aug 2022 23:57:34 +0200 Subject: [PATCH 11/18] refactor: remove note.mentionedRemoteUsers column The column mentionedRemoteUsers on the note table in the database is firstly in the wrong type since it contains JSON data but is typed as text. Secondly it seems redundant, since that data can be acquired by using the note.mentions column to fetch the respective data instead. Co-authored-by: Francis Dinh --- ...688552234-remove-mentioned-users-column.ts | 20 +++++++++ packages/backend/src/models/entities/note.ts | 13 ------ .../src/remote/activitypub/renderer/note.ts | 43 ++++++++++--------- .../backend/src/services/messages/create.ts | 7 --- packages/backend/src/services/note/create.ts | 16 ------- packages/backend/src/services/note/delete.ts | 15 ++++--- 6 files changed, 50 insertions(+), 64 deletions(-) create mode 100644 packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts diff --git a/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts new file mode 100644 index 0000000000..f2730f3cde --- /dev/null +++ b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class RemoveMentionedUsersColumn1710688552234 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "note" DROP COLUMN "mentionedRemoteUsers"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`, + ); + await queryRunner.query( + `UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`, + ); + } +} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index a31dd7dd46..e6fd892af3 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -172,12 +172,6 @@ export class Note { }) public mentions: User["id"][]; - // FIXME: WHAT IS THIS - @Column("text", { - default: "[]", - }) - public mentionedRemoteUsers: string; - @Column("varchar", { length: 128, array: true, @@ -307,10 +301,3 @@ export class Note { } } } - -export type IMentionedRemoteUsers = { - uri: string; - url?: string; - username: string; - host: string; -}[]; diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index f3bce41aa7..bfa424242a 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,6 +1,6 @@ import { In, IsNull } from "typeorm"; import config from "@/config/index.js"; -import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; +import type { Note } from "@/models/entities/note.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import { DriveFiles, Notes, Users, Emojis, Polls } from "@/models/index.js"; import type { Emoji } from "@/models/entities/emoji.js"; @@ -61,26 +61,6 @@ export default async function renderNote( const attributedTo = `${config.url}/users/${note.userId}`; - const mentions = ( - JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers - ).map((x) => x.uri); - - let to: string[] = []; - let cc: string[] = []; - - if (note.visibility === "public") { - to = ["https://www.w3.org/ns/activitystreams#Public"]; - cc = [`${attributedTo}/followers`].concat(mentions); - } else if (note.visibility === "home") { - to = [`${attributedTo}/followers`]; - cc = ["https://www.w3.org/ns/activitystreams#Public"].concat(mentions); - } else if (note.visibility === "followers") { - to = [`${attributedTo}/followers`]; - cc = mentions; - } else { - to = mentions; - } - const mentionedUsers = note.mentions.length > 0 ? await Users.findBy({ @@ -88,6 +68,27 @@ export default async function renderNote( }) : []; + const mentionUris = mentionedUsers + // only remote users + .filter((user) => Users.isRemoteUser(user)) + .map((user) => user.uri); + + let to: string[] = []; + let cc: string[] = []; + + if (note.visibility === "public") { + to = ["https://www.w3.org/ns/activitystreams#Public"]; + cc = [`${attributedTo}/followers`].concat(mentionUris); + } else if (note.visibility === "home") { + to = [`${attributedTo}/followers`]; + cc = ["https://www.w3.org/ns/activitystreams#Public"].concat(mentionUris); + } else if (note.visibility === "followers") { + to = [`${attributedTo}/followers`]; + cc = mentionUris; + } else { + to = mentionUris; + } + const hashtagTags = (note.tags || []).map((tag) => renderHashtag(tag)); const mentionTags = mentionedUsers.map((u) => renderMention(u)); diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index 0b3f8eded9..81d1614cb0 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -134,13 +134,6 @@ export async function createMessage( userId: message.userId, visibility: "specified", mentions: [recipientUser].map((u) => u.id), - mentionedRemoteUsers: JSON.stringify( - [recipientUser].map((u) => ({ - uri: u.uri, - username: u.username, - host: u.host, - })), - ), } as Note; let renderedNote: Record = await renderNote( diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index fc9913e985..16b2c1deed 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -18,7 +18,6 @@ import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instanc import { extractMentions } from "@/misc/extract-mentions.js"; import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; import { extractHashtags } from "@/misc/extract-hashtags.js"; -import type { IMentionedRemoteUsers } from "@/models/entities/note.js"; import { Note } from "@/models/entities/note.js"; import { Mutings, @@ -751,21 +750,6 @@ async function insertNote( // Append mentions data if (mentionedUsers.length > 0) { insert.mentions = mentionedUsers.map((u) => u.id); - const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) }); - insert.mentionedRemoteUsers = JSON.stringify( - mentionedUsers - .filter((u) => Users.isRemoteUser(u)) - .map((u) => { - const profile = profiles.find((p) => p.userId === u.id); - const url = profile != null ? profile.url : null; - return { - uri: u.uri, - url: url == null ? undefined : url, - username: u.username, - host: u.host, - } as IMentionedRemoteUsers[0]; - }), - ); } // 投稿を作成 diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 988e1c8c48..8a42ddf2ae 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,4 +1,4 @@ -import { Brackets, In } from "typeorm"; +import { Brackets, In, IsNull, Not } from "typeorm"; import { publishNoteStream } from "@/services/stream.js"; import renderDelete from "@/remote/activitypub/renderer/delete.js"; import renderAnnounce from "@/remote/activitypub/renderer/announce.js"; @@ -7,7 +7,7 @@ import { renderActivity } from "@/remote/activitypub/renderer/index.js"; import renderTombstone from "@/remote/activitypub/renderer/tombstone.js"; import config from "@/config/index.js"; import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; -import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; +import type { Note } from "@/models/entities/note.js"; import { Notes, Users, Instances } from "@/models/index.js"; import { deliverToFollowers, @@ -147,11 +147,12 @@ async function getMentionedRemoteUsers(note: Note) { const where = [] as any[]; // mention / reply / dm - const uris = ( - JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers - ).map((x) => x.uri); - if (uris.length > 0) { - where.push({ uri: In(uris) }); + if (note.mentions.length > 0) { + where.push({ + id: In(note.mentions), + // only remote users, local users are on the server and do not need to be notified + host: Not(IsNull()), + }); } // renote / quote From 347344287540a663d2ea4e7414a6e6e9bc8e6501 Mon Sep 17 00:00:00 2001 From: Norm Date: Sat, 22 Jul 2023 14:10:37 +0000 Subject: [PATCH 12/18] backend: improve removeMentionedRemoteUsersColumn revert query (#403) This is an optimized version made by Volpeon that should run faster if the instance has a lot of notes. See for a comparison of the EXPLAIN ANALYZE of the old and new queries. Co-authored-by: Volpeon Reviewed-on: https://akkoma.dev/FoundKeyGang/FoundKey/pulls/403 --- .../1710688552234-remove-mentioned-users-column.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts index f2730f3cde..e2b1eed950 100644 --- a/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts +++ b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts @@ -13,8 +13,16 @@ export class RemoveMentionedUsersColumn1710688552234 await queryRunner.query( `ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`, ); + await queryRunner.query(`CREATE TEMP TABLE IF NOT EXISTS "temp_mentions" AS + SELECT "id", "url", "uri", "username", "host" + FROM "user" + JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL`); await queryRunner.query( - `UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`, + `CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions"("id")`, ); + await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = ( + SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions" AS "data" + WHERE "data"."id" = ANY("note"."mentions") + )`); } } From 387bfd5af89883040013c23115a1fcc98e8933ad Mon Sep 17 00:00:00 2001 From: naskya Date: Mon, 18 Mar 2024 00:32:48 +0900 Subject: [PATCH 13/18] chore: regenerate entities, update downgrade.sql --- docs/downgrade.sql | 14 ++++++++++++++ packages/backend-rs/src/model/entity/note.rs | 2 -- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 192800257b..149e82fafe 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'RemoveMentionedUsersColumn1710688552234', 'NoteFile1710304584214', 'RenameMetaColumns1705944717480', 'SeparateHardMuteWordsAndPatterns1706413792769', @@ -17,6 +18,19 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); +-- remove-mentioned-users-column +ALTER TABLE "note" ADD "mentionedRemoteUsers" text NOT NULL DEFAULT '[]'::text; +CREATE TABLE "temp_mentions_1710688552234" AS + SELECT "id", "url", "uri", "username", "host" + FROM "user" + JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL; +CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions_1710688552234" ("id"); +UPDATE "note" SET "mentionedRemoteUsers" = ( + SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions_1710688552234" AS "data" + WHERE "data"."id" = ANY("note"."mentions") +); +DROP TABLE "temp_mentions_1710688552234"; + -- note-file DROP TABLE "note_file"; diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs index c851c776e2..f713e2e47e 100644 --- a/packages/backend-rs/src/model/entity/note.rs +++ b/packages/backend-rs/src/model/entity/note.rs @@ -38,8 +38,6 @@ pub struct Model { #[sea_orm(column_name = "visibleUserIds")] pub visible_user_ids: Vec, pub mentions: Vec, - #[sea_orm(column_name = "mentionedRemoteUsers", column_type = "Text")] - pub mentioned_remote_users: String, pub emojis: Vec, pub tags: Vec, #[sea_orm(column_name = "hasPoll")] From c61e6cd255b0208b4d7796403162010227dffb22 Mon Sep 17 00:00:00 2001 From: naskya Date: Mon, 18 Mar 2024 01:01:48 +0900 Subject: [PATCH 14/18] fix (backend): incorrect database indices (renote_mute & reply_mute) --- docs/downgrade.sql | 12 ++++ .../1710690239308-fix-muting-indices.ts | 57 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 packages/backend/src/migration/1710690239308-fix-muting-indices.ts diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 149e82fafe..a96d8c49db 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'FixMutingIndices1710690239308', 'RemoveMentionedUsersColumn1710688552234', 'NoteFile1710304584214', 'RenameMetaColumns1705944717480', @@ -18,6 +19,17 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); +-- fix-muting-indices +DROP INDEX "IDX_renote_muting_createdAt"; +DROP INDEX "IDX_renote_muting_muteeId"; +DROP INDEX "IDX_renote_muting_muterId"; +DROP INDEX "IDX_reply_muting_createdAt"; +DROP INDEX "IDX_reply_muting_muteeId"; +DROP INDEX "IDX_reply_muting_muterId"; +CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt"); +CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId"); +CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId"); + -- remove-mentioned-users-column ALTER TABLE "note" ADD "mentionedRemoteUsers" text NOT NULL DEFAULT '[]'::text; CREATE TABLE "temp_mentions_1710688552234" AS diff --git a/packages/backend/src/migration/1710690239308-fix-muting-indices.ts b/packages/backend/src/migration/1710690239308-fix-muting-indices.ts new file mode 100644 index 0000000000..3dc24c2531 --- /dev/null +++ b/packages/backend/src/migration/1710690239308-fix-muting-indices.ts @@ -0,0 +1,57 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class FixMutingIndices1710690239308 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_muterId"`); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_createdAt" ON "renote_muting" ("createdAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muteeId" ON "renote_muting" ("muteeId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muterId" ON "renote_muting" ("muterId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_createdAt" ON "reply_muting" ("createdAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_muteeId" ON "reply_muting" ("muteeId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_muterId" ON "reply_muting" ("muterId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_renote_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_renote_muting_muterId"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_muteeId"`); + await queryRunner.query(`DROP INDEX "IDX_reply_muting_muterId"`); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_createdAt" ON "muting" ("createdAt")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_muteeId" ON "muting" ("muteeId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_reply_muting_muterId" ON "muting" ("muterId")`, + ); + } +} From 9fd9eb01004aed9c0e751b1f42738728b2446c2e Mon Sep 17 00:00:00 2001 From: sup39 Date: Mon, 18 Mar 2024 00:58:40 +0900 Subject: [PATCH 15/18] fix (backend): incorrect database queries --- .../1705877093218-remove-native-utils-migration.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/migration/1705877093218-remove-native-utils-migration.ts b/packages/backend/src/migration/1705877093218-remove-native-utils-migration.ts index df9569baeb..a155a4ff78 100644 --- a/packages/backend/src/migration/1705877093218-remove-native-utils-migration.ts +++ b/packages/backend/src/migration/1705877093218-remove-native-utils-migration.ts @@ -105,10 +105,10 @@ export class RemoveNativeUtilsMigration1705877093218 `CREATE INDEX "IDX_9937ea48d7ae97ffb4f3f063a4" ON "antenna_note" ("read")`, ); await queryRunner.query( - `ALTER TABLE "antenna_note" ADD CONSTRAINT IF NOT EXISTS "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_0d775946662d2575dfd2068a5f5" FOREIGN KEY ("antennaId") REFERENCES "antenna"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - `ALTER TABLE "antenna_note" ADD CONSTRAINT IF NOT EXISTS "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "antenna_note" ADD CONSTRAINT "FK_bd0397be22147e17210940e125b" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query(`DROP INDEX IF EXISTS "IDX_note_url"`); await queryRunner.query( @@ -124,10 +124,10 @@ export class RemoveNativeUtilsMigration1705877093218 `CREATE INDEX IF NOT EXISTS "IDX_e247b23a3c9b45f89ec1299d06" ON "reversi_matching" ("childId")`, ); await queryRunner.query( - `ALTER TABLE "reversi_matching" ADD CONSTRAINT IF NOT EXISTS "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_3b25402709dd9882048c2bbade0" FOREIGN KEY ("parentId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - `ALTER TABLE "reversi_matching" ADD CONSTRAINT IF NOT EXISTS "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "reversi_matching" ADD CONSTRAINT "FK_e247b23a3c9b45f89ec1299d066" FOREIGN KEY ("childId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( `COMMENT ON COLUMN "reversi_matching"."createdAt" IS 'The created date of the ReversiMatching.'`, @@ -139,10 +139,10 @@ export class RemoveNativeUtilsMigration1705877093218 `CREATE INDEX IF NOT EXISTS "IDX_b46ec40746efceac604142be1c" ON "reversi_game" ("createdAt")`, ); await queryRunner.query( - `ALTER TABLE "reversi_game" ADD CONSTRAINT IF NOT EXISTS "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_f7467510c60a45ce5aca6292743" FOREIGN KEY ("user1Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( - `ALTER TABLE "reversi_game" ADD CONSTRAINT IF NOT EXISTS "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + `ALTER TABLE "reversi_game" ADD CONSTRAINT "FK_6649a4e8c5d5cf32fb03b5da9f6" FOREIGN KEY ("user2Id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, ); await queryRunner.query( `COMMENT ON COLUMN "reversi_game"."createdAt" IS 'The created date of the ReversiGame.'`, From e4a72bbfe5bc2257bc1f85bbb6e8c8edd4892072 Mon Sep 17 00:00:00 2001 From: naskya Date: Mon, 18 Mar 2024 03:29:17 +0900 Subject: [PATCH 16/18] Revert "backend: improve removeMentionedRemoteUsersColumn revert query (#403)" This reverts commit 347344287540a663d2ea4e7414a6e6e9bc8e6501. --- .../1710688552234-remove-mentioned-users-column.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts index e2b1eed950..f2730f3cde 100644 --- a/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts +++ b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts @@ -13,16 +13,8 @@ export class RemoveMentionedUsersColumn1710688552234 await queryRunner.query( `ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`, ); - await queryRunner.query(`CREATE TEMP TABLE IF NOT EXISTS "temp_mentions" AS - SELECT "id", "url", "uri", "username", "host" - FROM "user" - JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL`); await queryRunner.query( - `CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions"("id")`, + `UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`, ); - await queryRunner.query(`UPDATE "note" SET "mentionedRemoteUsers" = ( - SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions" AS "data" - WHERE "data"."id" = ANY("note"."mentions") - )`); } } From f7a8a58cb2fe1abf198b1acacfabf7f0ddcfdadf Mon Sep 17 00:00:00 2001 From: naskya Date: Mon, 18 Mar 2024 03:29:19 +0900 Subject: [PATCH 17/18] Revert "refactor: remove note.mentionedRemoteUsers column" This reverts commit 65797a934ac02bd7ad31ec6060f070196d19e2c4. --- ...688552234-remove-mentioned-users-column.ts | 20 ------------ packages/backend/src/models/entities/note.ts | 13 ++++++++ .../src/remote/activitypub/renderer/note.ts | 31 +++++++++---------- .../backend/src/services/messages/create.ts | 7 +++++ packages/backend/src/services/note/create.ts | 16 ++++++++++ packages/backend/src/services/note/delete.ts | 15 +++++---- 6 files changed, 58 insertions(+), 44 deletions(-) delete mode 100644 packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts diff --git a/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts b/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts deleted file mode 100644 index f2730f3cde..0000000000 --- a/packages/backend/src/migration/1710688552234-remove-mentioned-users-column.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class RemoveMentionedUsersColumn1710688552234 - implements MigrationInterface -{ - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "note" DROP COLUMN "mentionedRemoteUsers"`, - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "note" ADD "mentionedRemoteUsers" TEXT NOT NULL DEFAULT '[]'::text`, - ); - await queryRunner.query( - `UPDATE "note" SET "mentionedRemoteUsers" = (SELECT COALESCE(json_agg(row_to_json("data"))::text, '[]') FROM (SELECT "url", "uri", "username", "host" FROM "user" JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL AND "user"."id" = ANY("note"."mentions")) AS "data")`, - ); - } -} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index e6fd892af3..a31dd7dd46 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -172,6 +172,12 @@ export class Note { }) public mentions: User["id"][]; + // FIXME: WHAT IS THIS + @Column("text", { + default: "[]", + }) + public mentionedRemoteUsers: string; + @Column("varchar", { length: 128, array: true, @@ -301,3 +307,10 @@ export class Note { } } } + +export type IMentionedRemoteUsers = { + uri: string; + url?: string; + username: string; + host: string; +}[]; diff --git a/packages/backend/src/remote/activitypub/renderer/note.ts b/packages/backend/src/remote/activitypub/renderer/note.ts index bfa424242a..f3bce41aa7 100644 --- a/packages/backend/src/remote/activitypub/renderer/note.ts +++ b/packages/backend/src/remote/activitypub/renderer/note.ts @@ -1,6 +1,6 @@ import { In, IsNull } from "typeorm"; import config from "@/config/index.js"; -import type { Note } from "@/models/entities/note.js"; +import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import { DriveFiles, Notes, Users, Emojis, Polls } from "@/models/index.js"; import type { Emoji } from "@/models/entities/emoji.js"; @@ -61,34 +61,33 @@ export default async function renderNote( const attributedTo = `${config.url}/users/${note.userId}`; - const mentionedUsers = - note.mentions.length > 0 - ? await Users.findBy({ - id: In(note.mentions), - }) - : []; - - const mentionUris = mentionedUsers - // only remote users - .filter((user) => Users.isRemoteUser(user)) - .map((user) => user.uri); + const mentions = ( + JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers + ).map((x) => x.uri); let to: string[] = []; let cc: string[] = []; if (note.visibility === "public") { to = ["https://www.w3.org/ns/activitystreams#Public"]; - cc = [`${attributedTo}/followers`].concat(mentionUris); + cc = [`${attributedTo}/followers`].concat(mentions); } else if (note.visibility === "home") { to = [`${attributedTo}/followers`]; - cc = ["https://www.w3.org/ns/activitystreams#Public"].concat(mentionUris); + cc = ["https://www.w3.org/ns/activitystreams#Public"].concat(mentions); } else if (note.visibility === "followers") { to = [`${attributedTo}/followers`]; - cc = mentionUris; + cc = mentions; } else { - to = mentionUris; + to = mentions; } + const mentionedUsers = + note.mentions.length > 0 + ? await Users.findBy({ + id: In(note.mentions), + }) + : []; + const hashtagTags = (note.tags || []).map((tag) => renderHashtag(tag)); const mentionTags = mentionedUsers.map((u) => renderMention(u)); diff --git a/packages/backend/src/services/messages/create.ts b/packages/backend/src/services/messages/create.ts index 81d1614cb0..0b3f8eded9 100644 --- a/packages/backend/src/services/messages/create.ts +++ b/packages/backend/src/services/messages/create.ts @@ -134,6 +134,13 @@ export async function createMessage( userId: message.userId, visibility: "specified", mentions: [recipientUser].map((u) => u.id), + mentionedRemoteUsers: JSON.stringify( + [recipientUser].map((u) => ({ + uri: u.uri, + username: u.username, + host: u.host, + })), + ), } as Note; let renderedNote: Record = await renderNote( diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index 16b2c1deed..fc9913e985 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -18,6 +18,7 @@ import { registerOrFetchInstanceDoc } from "@/services/register-or-fetch-instanc import { extractMentions } from "@/misc/extract-mentions.js"; import { extractCustomEmojisFromMfm } from "@/misc/extract-custom-emojis-from-mfm.js"; import { extractHashtags } from "@/misc/extract-hashtags.js"; +import type { IMentionedRemoteUsers } from "@/models/entities/note.js"; import { Note } from "@/models/entities/note.js"; import { Mutings, @@ -750,6 +751,21 @@ async function insertNote( // Append mentions data if (mentionedUsers.length > 0) { insert.mentions = mentionedUsers.map((u) => u.id); + const profiles = await UserProfiles.findBy({ userId: In(insert.mentions) }); + insert.mentionedRemoteUsers = JSON.stringify( + mentionedUsers + .filter((u) => Users.isRemoteUser(u)) + .map((u) => { + const profile = profiles.find((p) => p.userId === u.id); + const url = profile != null ? profile.url : null; + return { + uri: u.uri, + url: url == null ? undefined : url, + username: u.username, + host: u.host, + } as IMentionedRemoteUsers[0]; + }), + ); } // 投稿を作成 diff --git a/packages/backend/src/services/note/delete.ts b/packages/backend/src/services/note/delete.ts index 8a42ddf2ae..988e1c8c48 100644 --- a/packages/backend/src/services/note/delete.ts +++ b/packages/backend/src/services/note/delete.ts @@ -1,4 +1,4 @@ -import { Brackets, In, IsNull, Not } from "typeorm"; +import { Brackets, In } from "typeorm"; import { publishNoteStream } from "@/services/stream.js"; import renderDelete from "@/remote/activitypub/renderer/delete.js"; import renderAnnounce from "@/remote/activitypub/renderer/announce.js"; @@ -7,7 +7,7 @@ import { renderActivity } from "@/remote/activitypub/renderer/index.js"; import renderTombstone from "@/remote/activitypub/renderer/tombstone.js"; import config from "@/config/index.js"; import type { User, ILocalUser, IRemoteUser } from "@/models/entities/user.js"; -import type { Note } from "@/models/entities/note.js"; +import type { Note, IMentionedRemoteUsers } from "@/models/entities/note.js"; import { Notes, Users, Instances } from "@/models/index.js"; import { deliverToFollowers, @@ -147,12 +147,11 @@ async function getMentionedRemoteUsers(note: Note) { const where = [] as any[]; // mention / reply / dm - if (note.mentions.length > 0) { - where.push({ - id: In(note.mentions), - // only remote users, local users are on the server and do not need to be notified - host: Not(IsNull()), - }); + const uris = ( + JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers + ).map((x) => x.uri); + if (uris.length > 0) { + where.push({ uri: In(uris) }); } // renote / quote From af800833b4218eccf88e6c0e8ae3b73efa6aefe0 Mon Sep 17 00:00:00 2001 From: naskya Date: Mon, 18 Mar 2024 03:47:57 +0900 Subject: [PATCH 18/18] fix: reflect revert to downgrade.sql --- docs/downgrade.sql | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/downgrade.sql b/docs/downgrade.sql index a96d8c49db..1d26a6fa3b 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -2,7 +2,6 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( 'FixMutingIndices1710690239308', - 'RemoveMentionedUsersColumn1710688552234', 'NoteFile1710304584214', 'RenameMetaColumns1705944717480', 'SeparateHardMuteWordsAndPatterns1706413792769', @@ -30,19 +29,6 @@ CREATE INDEX "IDX_renote_muting_createdAt" ON "muting" ("createdAt"); CREATE INDEX "IDX_renote_muting_muteeId" ON "muting" ("muteeId"); CREATE INDEX "IDX_renote_muting_muterId" ON "muting" ("muterId"); --- remove-mentioned-users-column -ALTER TABLE "note" ADD "mentionedRemoteUsers" text NOT NULL DEFAULT '[]'::text; -CREATE TABLE "temp_mentions_1710688552234" AS - SELECT "id", "url", "uri", "username", "host" - FROM "user" - JOIN "user_profile" ON "user"."id" = "user_profile". "userId" WHERE "user"."host" IS NOT NULL; -CREATE UNIQUE INDEX "temp_mentions_id" ON "temp_mentions_1710688552234" ("id"); -UPDATE "note" SET "mentionedRemoteUsers" = ( - SELECT COALESCE(json_agg(row_to_json("data")::jsonb - 'id')::text, '[]') FROM "temp_mentions_1710688552234" AS "data" - WHERE "data"."id" = ANY("note"."mentions") -); -DROP TABLE "temp_mentions_1710688552234"; - -- note-file DROP TABLE "note_file";