refactor (backend): convert jsonb to array

This commit is contained in:
naskya 2024-05-17 17:59:45 +09:00
parent 3d28acb2c9
commit a4779f233b
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
21 changed files with 394 additions and 143 deletions

View file

@ -1,6 +1,9 @@
BEGIN; BEGIN;
DELETE FROM "migrations" WHERE name IN ( DELETE FROM "migrations" WHERE name IN (
'UserprofileJsonbToArray1714270605574',
'DropUnusedUserprofileColumns1714259023878',
'AntennaJsonbToArray1714192520471',
'AddUserProfileLanguage1714888400293', 'AddUserProfileLanguage1714888400293',
'DropUnusedIndexes1714643926317', 'DropUnusedIndexes1714643926317',
'AlterAkaType1714099399879', 'AlterAkaType1714099399879',
@ -27,6 +30,45 @@ DELETE FROM "migrations" WHERE name IN (
'RemoveNativeUtilsMigration1705877093218' 'RemoveNativeUtilsMigration1705877093218'
); );
-- userprofile-jsonb-to-array
ALTER TABLE "user_profile" RENAME COLUMN "mutedInstances" TO "mutedInstances_old";
ALTER TABLE "user_profile" ADD COLUMN "mutedInstances" jsonb NOT NULL DEFAULT '[]';
UPDATE "user_profile" SET "mutedInstances" = to_jsonb("mutedInstances_old");
ALTER TABLE "user_profile" DROP COLUMN "mutedInstances_old";
ALTER TABLE "user_profile" RENAME COLUMN "mutedWords" TO "mutedWords_old";
ALTER TABLE "user_profile" ADD COLUMN "mutedWords" jsonb NOT NULL DEFAULT '[]';
CREATE TEMP TABLE "BCrsGgLCUeMMLARy" ("userId" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]');
INSERT INTO "BCrsGgLCUeMMLARy" ("userId", "kws") SELECT "userId", jsonb_agg("X"."w") FROM (SELECT "userId", to_jsonb(string_to_array(unnest("mutedWords_old"), ' ')) AS "w" FROM "user_profile") AS "X" GROUP BY "userId";
UPDATE "user_profile" SET "mutedWords" = "kws" FROM "BCrsGgLCUeMMLARy" WHERE "user_profile"."userId" = "BCrsGgLCUeMMLARy"."userId";
ALTER TABLE "user_profile" DROP COLUMN "mutedWords_old";
-- drop-unused-userprofile-columns
ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}';
COMMENT ON COLUMN "user_profile"."room" IS 'The room data of the User.';
ALTER TABLE "user_profile" ADD "clientData" jsonb NOT NULL DEFAULT '{}';
COMMENT ON COLUMN "user_profile"."clientData" IS 'The client-specific data of the User.';
-- antenna-jsonb-to-array
UPDATE "antenna" SET "instances" = '{""}' WHERE "instances" = '{}';
ALTER TABLE "antenna" RENAME COLUMN "instances" TO "instances_old";
ALTER TABLE "antenna" ADD COLUMN "instances" jsonb NOT NULL DEFAULT '[]';
UPDATE "antenna" SET "instances" = to_jsonb("instances_old");
ALTER TABLE "antenna" DROP COLUMN "instances_old";
UPDATE "antenna" SET "keywords" = '{""}' WHERE "keywords" = '{}';
ALTER TABLE "antenna" RENAME COLUMN "keywords" TO "keywords_old";
ALTER TABLE "antenna" ADD COLUMN "keywords" jsonb NOT NULL DEFAULT '[]';
CREATE TEMP TABLE "QvPNcMitBFkqqBgm" ("id" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]');
INSERT INTO "QvPNcMitBFkqqBgm" ("id", "kws") SELECT "id", jsonb_agg("X"."w") FROM (SELECT "id", to_jsonb(string_to_array(unnest("keywords_old"), ' ')) AS "w" FROM "antenna") AS "X" GROUP BY "id";
UPDATE "antenna" SET "keywords" = "kws" FROM "QvPNcMitBFkqqBgm" WHERE "antenna"."id" = "QvPNcMitBFkqqBgm"."id";
ALTER TABLE "antenna" DROP COLUMN "keywords_old";
UPDATE "antenna" SET "excludeKeywords" = '{""}' WHERE "excludeKeywords" = '{}';
ALTER TABLE "antenna" RENAME COLUMN "excludeKeywords" TO "excludeKeywords_old";
ALTER TABLE "antenna" ADD COLUMN "excludeKeywords" jsonb NOT NULL DEFAULT '[]';
CREATE TEMP TABLE "MZvVSjHzYcGXmGmz" ("id" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]');
INSERT INTO "MZvVSjHzYcGXmGmz" ("id", "kws") SELECT "id", jsonb_agg("X"."w") FROM (SELECT "id", to_jsonb(string_to_array(unnest("excludeKeywords_old"), ' ')) AS "w" FROM "antenna") AS "X" GROUP BY "id";
UPDATE "antenna" SET "excludeKeywords" = "kws" FROM "MZvVSjHzYcGXmGmz" WHERE "antenna"."id" = "MZvVSjHzYcGXmGmz"."id";
ALTER TABLE "antenna" DROP COLUMN "excludeKeywords_old";
-- drop-unused-indexes -- drop-unused-indexes
CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt"); CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt");
CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId"); CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId");

View file

@ -244,7 +244,7 @@ export interface NoteLikeForCheckWordMute {
renoteId: string | null renoteId: string | null
replyId: string | null replyId: string | null
} }
export function checkWordMute(note: NoteLikeForCheckWordMute, mutedWordLists: Array<Array<string>>, mutedPatterns: Array<string>): Promise<boolean> export function checkWordMute(note: NoteLikeForCheckWordMute, mutedWords: Array<string>, mutedPatterns: Array<string>): Promise<boolean>
export function getFullApAccount(username: string, host?: string | undefined | null): string export function getFullApAccount(username: string, host?: string | undefined | null): string
export function isSelfHost(host?: string | undefined | null): boolean export function isSelfHost(host?: string | undefined | null): boolean
export function isSameOrigin(uri: string): boolean export function isSameOrigin(uri: string): boolean
@ -381,7 +381,6 @@ export interface Antenna {
name: string name: string
src: AntennaSrcEnum src: AntennaSrcEnum
userListId: string | null userListId: string | null
keywords: Json
withFile: boolean withFile: boolean
expression: string | null expression: string | null
notify: boolean notify: boolean
@ -389,8 +388,9 @@ export interface Antenna {
withReplies: boolean withReplies: boolean
userGroupJoiningId: string | null userGroupJoiningId: string | null
users: Array<string> users: Array<string>
excludeKeywords: Json instances: Array<string>
instances: Json keywords: Array<string>
excludeKeywords: Array<string>
} }
export interface App { export interface App {
id: string id: string
@ -1128,7 +1128,6 @@ export interface UserProfile {
twoFactorSecret: string | null twoFactorSecret: string | null
twoFactorEnabled: boolean twoFactorEnabled: boolean
password: string | null password: string | null
clientData: Json
autoAcceptFollowed: boolean autoAcceptFollowed: boolean
alwaysMarkNsfw: boolean alwaysMarkNsfw: boolean
carefulBot: boolean carefulBot: boolean
@ -1136,21 +1135,20 @@ export interface UserProfile {
securityKeysAvailable: boolean securityKeysAvailable: boolean
usePasswordLessLogin: boolean usePasswordLessLogin: boolean
pinnedPageId: string | null pinnedPageId: string | null
room: Json
injectFeaturedNote: boolean injectFeaturedNote: boolean
enableWordMute: boolean enableWordMute: boolean
mutedWords: Json
mutingNotificationTypes: Array<UserProfileMutingnotificationtypesEnum> mutingNotificationTypes: Array<UserProfileMutingnotificationtypesEnum>
noCrawle: boolean noCrawle: boolean
receiveAnnouncementEmail: boolean receiveAnnouncementEmail: boolean
emailNotificationTypes: Json emailNotificationTypes: Json
mutedInstances: Json
publicReactions: boolean publicReactions: boolean
ffVisibility: UserProfileFfvisibilityEnum ffVisibility: UserProfileFfvisibilityEnum
moderationNote: string moderationNote: string
preventAiLearning: boolean preventAiLearning: boolean
isIndexable: boolean isIndexable: boolean
mutedPatterns: Array<string> mutedPatterns: Array<string>
mutedInstances: Array<string>
mutedWords: Array<string>
lang: string | null lang: string | null
} }
export interface UserPublickey { export interface UserPublickey {

View file

@ -87,14 +87,13 @@ fn convert_regex(js_regex: &str) -> String {
fn check_word_mute_impl( fn check_word_mute_impl(
texts: &[String], texts: &[String],
muted_word_lists: &[Vec<String>], muted_words: &[String],
muted_patterns: &[String], muted_patterns: &[String],
) -> bool { ) -> bool {
muted_word_lists.iter().any(|muted_word_list| { muted_words.iter().any(|item| {
texts.iter().any(|text| { texts.iter().any(|text| {
let text_lower = text.to_lowercase(); let text_lower = text.to_lowercase();
muted_word_list item.split_whitespace()
.iter()
.all(|muted_word| text_lower.contains(&muted_word.to_lowercase())) .all(|muted_word| text_lower.contains(&muted_word.to_lowercase()))
}) })
}) || muted_patterns.iter().any(|muted_pattern| { }) || muted_patterns.iter().any(|muted_pattern| {
@ -107,16 +106,16 @@ fn check_word_mute_impl(
#[crate::export] #[crate::export]
pub async fn check_word_mute( pub async fn check_word_mute(
note: NoteLike, note: NoteLike,
muted_word_lists: Vec<Vec<String>>, muted_words: &[String],
muted_patterns: Vec<String>, muted_patterns: &[String],
) -> Result<bool, DbErr> { ) -> Result<bool, DbErr> {
if muted_word_lists.is_empty() && muted_patterns.is_empty() { if muted_words.is_empty() && muted_patterns.is_empty() {
Ok(false) Ok(false)
} else { } else {
Ok(check_word_mute_impl( Ok(check_word_mute_impl(
&all_texts(note).await?, &all_texts(note).await?,
&muted_word_lists, muted_words,
&muted_patterns, muted_patterns,
)) ))
} }
} }

View file

@ -21,8 +21,6 @@ pub struct Model {
pub src: AntennaSrcEnum, pub src: AntennaSrcEnum,
#[sea_orm(column_name = "userListId")] #[sea_orm(column_name = "userListId")]
pub user_list_id: Option<String>, pub user_list_id: Option<String>,
#[sea_orm(column_type = "JsonBinary")]
pub keywords: Json,
#[sea_orm(column_name = "withFile")] #[sea_orm(column_name = "withFile")]
pub with_file: bool, pub with_file: bool,
pub expression: Option<String>, pub expression: Option<String>,
@ -34,10 +32,10 @@ pub struct Model {
#[sea_orm(column_name = "userGroupJoiningId")] #[sea_orm(column_name = "userGroupJoiningId")]
pub user_group_joining_id: Option<String>, pub user_group_joining_id: Option<String>,
pub users: Vec<String>, pub users: Vec<String>,
#[sea_orm(column_name = "excludeKeywords", column_type = "JsonBinary")] pub instances: Vec<String>,
pub exclude_keywords: Json, pub keywords: Vec<String>,
#[sea_orm(column_type = "JsonBinary")] #[sea_orm(column_name = "excludeKeywords")]
pub instances: Json, pub exclude_keywords: Vec<String>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View file

@ -32,8 +32,6 @@ pub struct Model {
#[sea_orm(column_name = "twoFactorEnabled")] #[sea_orm(column_name = "twoFactorEnabled")]
pub two_factor_enabled: bool, pub two_factor_enabled: bool,
pub password: Option<String>, pub password: Option<String>,
#[sea_orm(column_name = "clientData", column_type = "JsonBinary")]
pub client_data: Json,
#[sea_orm(column_name = "autoAcceptFollowed")] #[sea_orm(column_name = "autoAcceptFollowed")]
pub auto_accept_followed: bool, pub auto_accept_followed: bool,
#[sea_orm(column_name = "alwaysMarkNsfw")] #[sea_orm(column_name = "alwaysMarkNsfw")]
@ -48,14 +46,10 @@ pub struct Model {
pub use_password_less_login: bool, pub use_password_less_login: bool,
#[sea_orm(column_name = "pinnedPageId", unique)] #[sea_orm(column_name = "pinnedPageId", unique)]
pub pinned_page_id: Option<String>, pub pinned_page_id: Option<String>,
#[sea_orm(column_type = "JsonBinary")]
pub room: Json,
#[sea_orm(column_name = "injectFeaturedNote")] #[sea_orm(column_name = "injectFeaturedNote")]
pub inject_featured_note: bool, pub inject_featured_note: bool,
#[sea_orm(column_name = "enableWordMute")] #[sea_orm(column_name = "enableWordMute")]
pub enable_word_mute: bool, pub enable_word_mute: bool,
#[sea_orm(column_name = "mutedWords", column_type = "JsonBinary")]
pub muted_words: Json,
#[sea_orm(column_name = "mutingNotificationTypes")] #[sea_orm(column_name = "mutingNotificationTypes")]
pub muting_notification_types: Vec<UserProfileMutingnotificationtypesEnum>, pub muting_notification_types: Vec<UserProfileMutingnotificationtypesEnum>,
#[sea_orm(column_name = "noCrawle")] #[sea_orm(column_name = "noCrawle")]
@ -64,8 +58,6 @@ pub struct Model {
pub receive_announcement_email: bool, pub receive_announcement_email: bool,
#[sea_orm(column_name = "emailNotificationTypes", column_type = "JsonBinary")] #[sea_orm(column_name = "emailNotificationTypes", column_type = "JsonBinary")]
pub email_notification_types: Json, pub email_notification_types: Json,
#[sea_orm(column_name = "mutedInstances", column_type = "JsonBinary")]
pub muted_instances: Json,
#[sea_orm(column_name = "publicReactions")] #[sea_orm(column_name = "publicReactions")]
pub public_reactions: bool, pub public_reactions: bool,
#[sea_orm(column_name = "ffVisibility")] #[sea_orm(column_name = "ffVisibility")]
@ -78,6 +70,10 @@ pub struct Model {
pub is_indexable: bool, pub is_indexable: bool,
#[sea_orm(column_name = "mutedPatterns")] #[sea_orm(column_name = "mutedPatterns")]
pub muted_patterns: Vec<String>, pub muted_patterns: Vec<String>,
#[sea_orm(column_name = "mutedInstances")]
pub muted_instances: Vec<String>,
#[sea_orm(column_name = "mutedWords")]
pub muted_words: Vec<String>,
pub lang: Option<String>, pub lang: Option<String>,
} }

View file

@ -0,0 +1,118 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class AntennaJsonbToArray1714192520471 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "antenna" RENAME COLUMN "instances" TO "instances_old"`,
);
await queryRunner.query(
`ALTER TABLE "antenna" ADD COLUMN "instances" character varying(512)[] NOT NULL DEFAULT '{}'`,
);
await queryRunner.query(
`UPDATE "antenna" SET "instances" = ARRAY(SELECT jsonb_array_elements_text("instances_old"))::character varying(512)[]`,
);
await queryRunner.query(
`UPDATE "antenna" SET "instances" = '{}' WHERE "instances" = '{""}'`,
);
await queryRunner.query(
`ALTER TABLE "antenna" DROP COLUMN "instances_old"`,
);
await queryRunner.query(
`ALTER TABLE "antenna" RENAME COLUMN "keywords" TO "keywords_old"`,
);
await queryRunner.query(
`ALTER TABLE "antenna" ADD COLUMN "keywords" text[] NOT NULL DEFAULT '{}'`,
);
await queryRunner.query(
`CREATE TEMP TABLE "HMyeXPcdtQYGsSrf" ("id" character varying(32), "kws" text[])`,
);
await queryRunner.query(
`INSERT INTO "HMyeXPcdtQYGsSrf" ("id", "kws") SELECT "id", array_agg("X"."w") FROM (SELECT "id", array_to_string(ARRAY(SELECT jsonb_array_elements_text("kw")), ' ') AS "w" FROM (SELECT "id", jsonb_array_elements("keywords_old") AS "kw" FROM "antenna") AS "a") AS "X" GROUP BY "id"`,
);
await queryRunner.query(
`UPDATE "antenna" SET "keywords" = "kws" FROM "HMyeXPcdtQYGsSrf" WHERE "antenna"."id" = "HMyeXPcdtQYGsSrf"."id"`,
);
await queryRunner.query(
`UPDATE "antenna" SET "keywords" = '{}' WHERE "keywords" = '{""}'`,
);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "keywords_old"`);
await queryRunner.query(
`ALTER TABLE "antenna" RENAME COLUMN "excludeKeywords" TO "excludeKeywords_old"`,
);
await queryRunner.query(
`ALTER TABLE "antenna" ADD COLUMN "excludeKeywords" text[] NOT NULL DEFAULT '{}'`,
);
await queryRunner.query(
`CREATE TEMP TABLE "kpdsACdZTRYqLkfK" ("id" character varying(32), "kws" text[])`,
);
await queryRunner.query(
`INSERT INTO "kpdsACdZTRYqLkfK" ("id", "kws") SELECT "id", array_agg("X"."w") FROM (SELECT "id", array_to_string(ARRAY(SELECT jsonb_array_elements_text("kw")), ' ') AS "w" FROM (SELECT "id", jsonb_array_elements("excludeKeywords_old") AS "kw" FROM "antenna") AS "a") AS "X" GROUP BY "id"`,
);
await queryRunner.query(
`UPDATE "antenna" SET "excludeKeywords" = "kws" FROM "kpdsACdZTRYqLkfK" WHERE "antenna"."id" = "kpdsACdZTRYqLkfK"."id"`,
);
await queryRunner.query(
`UPDATE "antenna" SET "excludeKeywords" = '{}' WHERE "excludeKeywords" = '{""}'`,
);
await queryRunner.query(
`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords_old"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`UPDATE "antenna" SET "instances" = '{""}' WHERE "instances" = '{}'`,
);
await queryRunner.query(
`ALTER TABLE "antenna" RENAME COLUMN "instances" TO "instances_old"`,
);
await queryRunner.query(
`ALTER TABLE "antenna" ADD COLUMN "instances" jsonb NOT NULL DEFAULT '[]'`,
);
await queryRunner.query(
`UPDATE "antenna" SET "instances" = to_jsonb("instances_old")`,
);
await queryRunner.query(
`ALTER TABLE "antenna" DROP COLUMN "instances_old"`,
);
await queryRunner.query(
`UPDATE "antenna" SET "keywords" = '{""}' WHERE "keywords" = '{}'`,
);
await queryRunner.query(
`ALTER TABLE "antenna" RENAME COLUMN "keywords" TO "keywords_old"`,
);
await queryRunner.query(
`ALTER TABLE "antenna" ADD COLUMN "keywords" jsonb NOT NULL DEFAULT '[]'`,
);
await queryRunner.query(
`CREATE TEMP TABLE "QvPNcMitBFkqqBgm" ("id" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]')`,
);
await queryRunner.query(
`INSERT INTO "QvPNcMitBFkqqBgm" ("id", "kws") SELECT "id", jsonb_agg("X"."w") FROM (SELECT "id", to_jsonb(string_to_array(unnest("keywords_old"), ' ')) AS "w" FROM "antenna") AS "X" GROUP BY "id"`,
);
await queryRunner.query(
`UPDATE "antenna" SET "keywords" = "kws" FROM "QvPNcMitBFkqqBgm" WHERE "antenna"."id" = "QvPNcMitBFkqqBgm"."id"`,
);
await queryRunner.query(`ALTER TABLE "antenna" DROP COLUMN "keywords_old"`);
await queryRunner.query(
`UPDATE "antenna" SET "excludeKeywords" = '{""}' WHERE "excludeKeywords" = '{}'`,
);
await queryRunner.query(
`ALTER TABLE "antenna" RENAME COLUMN "excludeKeywords" TO "excludeKeywords_old"`,
);
await queryRunner.query(
`ALTER TABLE "antenna" ADD COLUMN "excludeKeywords" jsonb NOT NULL DEFAULT '[]'`,
);
await queryRunner.query(
`CREATE TEMP TABLE "MZvVSjHzYcGXmGmz" ("id" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]')`,
);
await queryRunner.query(
`INSERT INTO "MZvVSjHzYcGXmGmz" ("id", "kws") SELECT "id", jsonb_agg("X"."w") FROM (SELECT "id", to_jsonb(string_to_array(unnest("excludeKeywords_old"), ' ')) AS "w" FROM "antenna") AS "X" GROUP BY "id"`,
);
await queryRunner.query(
`UPDATE "antenna" SET "excludeKeywords" = "kws" FROM "MZvVSjHzYcGXmGmz" WHERE "antenna"."id" = "MZvVSjHzYcGXmGmz"."id"`,
);
await queryRunner.query(
`ALTER TABLE "antenna" DROP COLUMN "excludeKeywords_old"`,
);
}
}

View file

@ -0,0 +1,27 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class DropUnusedUserprofileColumns1714259023878
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_profile" DROP COLUMN "clientData"`,
);
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "room"`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_profile" ADD "room" jsonb NOT NULL DEFAULT '{}'`,
);
await queryRunner.query(
`COMMENT ON COLUMN "user_profile"."room" IS 'The room data of the User.'`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" ADD "clientData" jsonb NOT NULL DEFAULT '{}'`,
);
await queryRunner.query(
`COMMENT ON COLUMN "user_profile"."clientData" IS 'The client-specific data of the User.'`,
);
}
}

View file

@ -0,0 +1,71 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class UserprofileJsonbToArray1714270605574
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_profile" RENAME COLUMN "mutedInstances" TO "mutedInstances_old"`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" ADD COLUMN "mutedInstances" character varying(512)[] NOT NULL DEFAULT '{}'`,
);
await queryRunner.query(
`UPDATE "user_profile" SET "mutedInstances" = ARRAY(SELECT jsonb_array_elements_text("mutedInstances_old"))::character varying(512)[]`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" DROP COLUMN "mutedInstances_old"`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" RENAME COLUMN "mutedWords" TO "mutedWords_old"`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" ADD COLUMN "mutedWords" text[] NOT NULL DEFAULT '{}'`,
);
await queryRunner.query(
`CREATE TEMP TABLE "MmVqAUUgpshTCQcw" ("userId" character varying(32), "kws" text[])`,
);
await queryRunner.query(
`INSERT INTO "MmVqAUUgpshTCQcw" ("userId", "kws") SELECT "userId", array_agg("X"."w") FROM (SELECT "userId", array_to_string(ARRAY(SELECT jsonb_array_elements_text("kw")), ' ') AS "w" FROM (SELECT "userId", jsonb_array_elements("mutedWords_old") AS "kw" FROM "user_profile") AS "a") AS "X" GROUP BY "userId"`,
);
await queryRunner.query(
`UPDATE "user_profile" SET "mutedWords" = "kws" FROM "MmVqAUUgpshTCQcw" WHERE "user_profile"."userId" = "MmVqAUUgpshTCQcw"."userId"`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" DROP COLUMN "mutedWords_old"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_profile" RENAME COLUMN "mutedInstances" TO "mutedInstances_old"`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" ADD COLUMN "mutedInstances" jsonb NOT NULL DEFAULT '[]'`,
);
await queryRunner.query(
`UPDATE "user_profile" SET "mutedInstances" = to_jsonb("mutedInstances_old")`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" DROP COLUMN "mutedInstances_old"`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" RENAME COLUMN "mutedWords" TO "mutedWords_old"`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" ADD COLUMN "mutedWords" jsonb NOT NULL DEFAULT '[]'`,
);
await queryRunner.query(
`CREATE TEMP TABLE "BCrsGgLCUeMMLARy" ("userId" character varying(32), "kws" jsonb NOT NULL DEFAULT '[]')`,
);
await queryRunner.query(
`INSERT INTO "BCrsGgLCUeMMLARy" ("userId", "kws") SELECT "userId", jsonb_agg("X"."w") FROM (SELECT "userId", to_jsonb(string_to_array(unnest("mutedWords_old"), ' ')) AS "w" FROM "user_profile") AS "X" GROUP BY "userId"`,
);
await queryRunner.query(
`UPDATE "user_profile" SET "mutedWords" = "kws" FROM "BCrsGgLCUeMMLARy" WHERE "user_profile"."userId" = "BCrsGgLCUeMMLARy"."userId"`,
);
await queryRunner.query(
`ALTER TABLE "user_profile" DROP COLUMN "mutedWords_old"`,
);
}
}

View file

@ -46,44 +46,38 @@ export async function checkHitAntenna(
if (!instances.includes(noteUser.host?.toLowerCase() ?? "")) return false; if (!instances.includes(noteUser.host?.toLowerCase() ?? "")) return false;
} }
const keywords = antenna.keywords
// Clean up
.map((xs) => xs.filter((x) => x !== ""))
.filter((xs) => xs.length > 0);
let text = `${note.text ?? ""} ${note.cw ?? ""}`; let text = `${note.text ?? ""} ${note.cw ?? ""}`;
if (note.files != null) if (note.files != null)
text += ` ${note.files.map((f) => f.comment ?? "").join(" ")}`; text += ` ${note.files.map((f) => f.comment ?? "").join(" ")}`;
text = text.trim(); text = text.trim();
if (keywords.length > 0) { if (antenna.keywords.length > 0) {
if (note.text == null) return false; if (note.text == null) return false;
const matched = keywords.some((and) => const matched = antenna.keywords.some((item) =>
and.every((keyword) => item
antenna.caseSensitive .split(" ")
? text.includes(keyword) .every((keyword) =>
: text.toLowerCase().includes(keyword.toLowerCase()), antenna.caseSensitive
), ? text.includes(keyword)
: text.toLowerCase().includes(keyword.toLowerCase()),
),
); );
if (!matched) return false; if (!matched) return false;
} }
const excludeKeywords = antenna.excludeKeywords if (antenna.excludeKeywords.length > 0) {
// Clean up
.map((xs) => xs.filter((x) => x !== ""))
.filter((xs) => xs.length > 0);
if (excludeKeywords.length > 0) {
if (note.text == null) return false; if (note.text == null) return false;
const matched = excludeKeywords.some((and) => const matched = antenna.excludeKeywords.some((item) =>
and.every((keyword) => item
antenna.caseSensitive .split(" ")
? note.text?.includes(keyword) .every((keyword) =>
: note.text?.toLowerCase().includes(keyword.toLowerCase()), antenna.caseSensitive
), ? note.text?.includes(keyword)
: note.text?.toLowerCase().includes(keyword.toLowerCase()),
),
); );
if (matched) return false; if (matched) return false;

View file

@ -59,20 +59,30 @@ export class Antenna {
}) })
public users: string[]; public users: string[];
@Column("jsonb", { @Column("varchar", {
default: [], length: 512,
array: true,
default: "{}",
}) })
public instances: string[]; public instances: string[];
@Column("jsonb", { // whitespace: AND condition
default: [], // array items: OR condition
// e.g., ["alpha beta", "gamma"]
// does match "alpha beta", "beta alpha alpha", "gamma alpha", "gamma epsilon"
// does not match "alpha", "beta gamma", "alpha alpha", "eplison"
@Column("text", {
array: true,
default: "{}",
}) })
public keywords: string[][]; public keywords: string[];
@Column("jsonb", { // same match rule as `keywords`, except that this field is for excluded words
default: [], @Column("text", {
array: true,
default: "{}",
}) })
public excludeKeywords: string[][]; public excludeKeywords: string[];
@Column("boolean", { @Column("boolean", {
default: false, default: false,

View file

@ -138,20 +138,6 @@ export class UserProfile {
}) })
public moderationNote: string | null; public moderationNote: string | null;
// TODO: そのうち消す
@Column("jsonb", {
default: {},
comment: "The client-specific data of the User.",
})
public clientData: Record<string, any>;
// TODO: そのうち消す
@Column("jsonb", {
default: {},
comment: "The room data of the User.",
})
public room: Record<string, any>;
@Column("boolean", { @Column("boolean", {
default: false, default: false,
}) })
@ -200,12 +186,6 @@ export class UserProfile {
}) })
public pinnedPageId: Page["id"] | null; public pinnedPageId: Page["id"] | null;
@OneToOne((type) => Page, {
onDelete: "SET NULL",
})
@JoinColumn()
public pinnedPage: Page | null;
@Index() @Index()
@Column("boolean", { @Column("boolean", {
default: false, default: false,
@ -213,19 +193,28 @@ export class UserProfile {
}) })
public enableWordMute: boolean; public enableWordMute: boolean;
@Column("jsonb", { // whitespace: AND condition
default: [], // array items: OR condition
// e.g., ["alpha beta", "gamma"]
// does match "alpha beta", "beta alpha alpha", "gamma alpha", "gamma epsilon"
// does not match "alpha", "beta gamma", "alpha alpha", "eplison"
@Column("text", {
array: true,
default: "{}",
}) })
public mutedWords: string[][]; public mutedWords: string[];
// array of regular expressions
@Column("text", { @Column("text", {
array: true, array: true,
nullable: false, nullable: false,
}) })
public mutedPatterns: string[]; public mutedPatterns: string[];
@Column("jsonb", { @Column("varchar", {
default: [], length: 512,
array: true,
default: "{}",
comment: "List of instances muted by the user.", comment: "List of instances muted by the user.",
}) })
public mutedInstances: string[]; public mutedInstances: string[];
@ -253,6 +242,13 @@ export class UserProfile {
}) })
@JoinColumn() @JoinColumn()
public user: Relation<User>; public user: Relation<User>;
@OneToOne(() => Page, {
onDelete: "SET NULL",
nullable: true,
})
@JoinColumn()
public pinnedPage: Relation<Page | null>;
//#endregion //#endregion
constructor(data: Partial<UserProfile>) { constructor(data: Partial<UserProfile>) {

View file

@ -16,8 +16,8 @@ export const AntennaRepository = db.getRepository(Antenna).extend({
id: antenna.id, id: antenna.id,
createdAt: antenna.createdAt.toISOString(), createdAt: antenna.createdAt.toISOString(),
name: antenna.name, name: antenna.name,
keywords: antenna.keywords, keywords: antenna.keywords.map((row) => row.split(" ")),
excludeKeywords: antenna.excludeKeywords, excludeKeywords: antenna.excludeKeywords.map((row) => row.split(" ")),
src: antenna.src, src: antenna.src,
userListId: antenna.userListId, userListId: antenna.userListId,
userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null, userGroupId: userGroupJoining ? userGroupJoining.userGroupId : null,

View file

@ -573,7 +573,7 @@ export const UserRepository = db.getRepository(User).extend({
hasUnreadNotification: this.getHasUnreadNotification(user.id), hasUnreadNotification: this.getHasUnreadNotification(user.id),
hasPendingReceivedFollowRequest: hasPendingReceivedFollowRequest:
this.getHasPendingReceivedFollowRequest(user.id), this.getHasPendingReceivedFollowRequest(user.id),
mutedWords: profile?.mutedWords, mutedWords: profile?.mutedWords.map((row) => row.split(" ")),
mutedPatterns: profile?.mutedPatterns, mutedPatterns: profile?.mutedPatterns,
mutedInstances: profile?.mutedInstances, mutedInstances: profile?.mutedInstances,
mutingNotificationTypes: profile?.mutingNotificationTypes, mutingNotificationTypes: profile?.mutingNotificationTypes,

View file

@ -42,25 +42,13 @@ export function generateMutedUserQuery(
) )
// mute instances // mute instances
.andWhere( .andWhere(
new Brackets((qb) => { `NOT
qb.andWhere("note.userHost IS NULL").orWhere( ARRAY[
`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.userHost)`, note."userHost",
); note."replyUserHost",
}), note."renoteUserHost"
) ]::character varying[]
.andWhere( && (${mutingInstanceQuery.getQuery()})`,
new Brackets((qb) => {
qb.where("note.replyUserHost IS NULL").orWhere(
`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.replyUserHost)`,
);
}),
)
.andWhere(
new Brackets((qb) => {
qb.where("note.renoteUserHost IS NULL").orWhere(
`NOT ((${mutingInstanceQuery.getQuery()})::jsonb ? note.renoteUserHost)`,
);
}),
); );
q.setParameters(mutingQuery.getParameters()); q.setParameters(mutingQuery.getParameters());

View file

@ -1,5 +1,5 @@
import { Brackets, SelectQueryBuilder } from "typeorm"; import { Brackets, type SelectQueryBuilder } from "typeorm";
import { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import { RenoteMutings } from "@/models/index.js"; import { RenoteMutings } from "@/models/index.js";
export function generateMutedUserRenotesQueryForNotes( export function generateMutedUserRenotesQueryForNotes(

View file

@ -1,5 +1,5 @@
import { Brackets, SelectQueryBuilder } from "typeorm"; import { Brackets, type SelectQueryBuilder } from "typeorm";
import { User } from "@/models/entities/user.js"; import type { User } from "@/models/entities/user.js";
import { ReplyMutings } from "@/models/index.js"; import { ReplyMutings } from "@/models/index.js";
export function generateMutedUserRepliesQueryForNotes( export function generateMutedUserRepliesQueryForNotes(

View file

@ -59,7 +59,7 @@ export default define(meta, paramDef, async (ps, me) => {
carefulBot: profile.carefulBot, carefulBot: profile.carefulBot,
injectFeaturedNote: profile.injectFeaturedNote, injectFeaturedNote: profile.injectFeaturedNote,
receiveAnnouncementEmail: profile.receiveAnnouncementEmail, receiveAnnouncementEmail: profile.receiveAnnouncementEmail,
mutedWords: profile.mutedWords, mutedWords: profile.mutedWords.map((row) => row.split(" ")),
mutedPatterns: profile.mutedPatterns, mutedPatterns: profile.mutedPatterns,
mutedInstances: profile.mutedInstances, mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes, mutingNotificationTypes: profile.mutingNotificationTypes,

View file

@ -104,8 +104,22 @@ export const paramDef = {
} as const; } as const;
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const flatten = (arr: string[][]) =>
JSON.stringify(arr) === "[[]]"
? ([] as string[])
: arr.map((row) => row.join(" "));
const keywords = flatten(
ps.keywords.map((row) => row.filter((word) => word.trim().length > 0)),
);
const excludedWords = flatten(
ps.excludeKeywords.map((row) =>
row.filter((word) => word.trim().length > 0),
),
);
if (user.movedToUri != null) throw new ApiError(meta.errors.noSuchUserGroup); if (user.movedToUri != null) throw new ApiError(meta.errors.noSuchUserGroup);
if (ps.keywords.length === 0) throw new ApiError(meta.errors.noKeywords); if (keywords.length === 0) throw new ApiError(meta.errors.noKeywords);
let userList; let userList;
let userGroupJoining; let userGroupJoining;
@ -146,10 +160,10 @@ export default define(meta, paramDef, async (ps, user) => {
src: ps.src, src: ps.src,
userListId: userList ? userList.id : null, userListId: userList ? userList.id : null,
userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
keywords: ps.keywords, keywords: keywords,
excludeKeywords: ps.excludeKeywords, excludeKeywords: excludedWords,
users: ps.users, users: ps.users,
instances: ps.instances, instances: ps.instances.filter((instance) => instance.trim().length > 0),
caseSensitive: ps.caseSensitive, caseSensitive: ps.caseSensitive,
withReplies: ps.withReplies, withReplies: ps.withReplies,
withFile: ps.withFile, withFile: ps.withFile,

View file

@ -100,6 +100,20 @@ export const paramDef = {
} as const; } as const;
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, user) => {
const flatten = (arr: string[][]) =>
JSON.stringify(arr) === "[[]]"
? ([] as string[])
: arr.map((row) => row.join(" "));
const keywords = flatten(
ps.keywords.map((row) => row.filter((word) => word.trim().length > 0)),
);
const excludedWords = flatten(
ps.excludeKeywords.map((row) =>
row.filter((word) => word.trim().length > 0),
),
);
// Fetch the antenna // Fetch the antenna
const antenna = await Antennas.findOneBy({ const antenna = await Antennas.findOneBy({
id: ps.antennaId, id: ps.antennaId,
@ -138,10 +152,10 @@ export default define(meta, paramDef, async (ps, user) => {
src: ps.src, src: ps.src,
userListId: userList ? userList.id : null, userListId: userList ? userList.id : null,
userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null, userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
keywords: ps.keywords, keywords: keywords,
excludeKeywords: ps.excludeKeywords, excludeKeywords: excludedWords,
users: ps.users, users: ps.users,
instances: ps.instances, instances: ps.instances.filter((instance) => instance.trim().length > 0),
caseSensitive: ps.caseSensitive, caseSensitive: ps.caseSensitive,
withReplies: ps.withReplies, withReplies: ps.withReplies,
withFile: ps.withFile, withFile: ps.withFile,

View file

@ -125,7 +125,8 @@ export default define(meta, paramDef, async (ps, user) => {
query.andWhere( query.andWhere(
new Brackets((qb) => { new Brackets((qb) => {
qb.andWhere("notifier.host IS NULL").orWhere( qb.andWhere("notifier.host IS NULL").orWhere(
`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`, `NOT EXISTS (SELECT 1 FROM "user_profile" WHERE "userId" = :muterId AND notifier.host = ANY("mutedInstances"))`,
{ muterId: user.id },
); );
}), }),
); );

View file

@ -178,26 +178,11 @@ export default define(meta, paramDef, async (ps, _user, token) => {
} }
} }
if (ps.mutedWords !== undefined) { if (ps.mutedWords !== undefined) {
// for backward compatibility const flatten = (arr: string[][]) =>
for (const item of ps.mutedWords) { JSON.stringify(arr) === "[[]]"
if (Array.isArray(item)) continue; ? ([] as string[])
: arr.map((row) => row.join(" "));
const regexp = item.match(/^\/(.+)\/(.*)$/); profileUpdates.mutedWords = flatten(ps.mutedWords);
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
try {
new RegExp(regexp[1], regexp[2]);
} catch (err) {
throw new ApiError(meta.errors.invalidRegexp);
}
profileUpdates.mutedPatterns = profileUpdates.mutedPatterns ?? [];
profileUpdates.mutedPatterns.push(item);
}
profileUpdates.mutedWords = ps.mutedWords.filter((item) =>
Array.isArray(item),
);
} }
if ( if (
profileUpdates.mutedWords !== undefined || profileUpdates.mutedWords !== undefined ||