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;
DELETE FROM "migrations" WHERE name IN (
'UserprofileJsonbToArray1714270605574',
'DropUnusedUserprofileColumns1714259023878',
'AntennaJsonbToArray1714192520471',
'AddUserProfileLanguage1714888400293',
'DropUnusedIndexes1714643926317',
'AlterAkaType1714099399879',
@ -27,6 +30,45 @@ DELETE FROM "migrations" WHERE name IN (
'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
CREATE INDEX "IDX_01f4581f114e0ebd2bbb876f0b" ON "note_reaction" ("createdAt");
CREATE INDEX "IDX_0610ebcfcfb4a18441a9bcdab2" ON "poll" ("userId");

View file

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

View file

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

View file

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

View file

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

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;
}
const keywords = antenna.keywords
// Clean up
.map((xs) => xs.filter((x) => x !== ""))
.filter((xs) => xs.length > 0);
let text = `${note.text ?? ""} ${note.cw ?? ""}`;
if (note.files != null)
text += ` ${note.files.map((f) => f.comment ?? "").join(" ")}`;
text = text.trim();
if (keywords.length > 0) {
if (antenna.keywords.length > 0) {
if (note.text == null) return false;
const matched = keywords.some((and) =>
and.every((keyword) =>
antenna.caseSensitive
? text.includes(keyword)
: text.toLowerCase().includes(keyword.toLowerCase()),
),
const matched = antenna.keywords.some((item) =>
item
.split(" ")
.every((keyword) =>
antenna.caseSensitive
? text.includes(keyword)
: text.toLowerCase().includes(keyword.toLowerCase()),
),
);
if (!matched) return false;
}
const excludeKeywords = antenna.excludeKeywords
// Clean up
.map((xs) => xs.filter((x) => x !== ""))
.filter((xs) => xs.length > 0);
if (excludeKeywords.length > 0) {
if (antenna.excludeKeywords.length > 0) {
if (note.text == null) return false;
const matched = excludeKeywords.some((and) =>
and.every((keyword) =>
antenna.caseSensitive
? note.text?.includes(keyword)
: note.text?.toLowerCase().includes(keyword.toLowerCase()),
),
const matched = antenna.excludeKeywords.some((item) =>
item
.split(" ")
.every((keyword) =>
antenna.caseSensitive
? note.text?.includes(keyword)
: note.text?.toLowerCase().includes(keyword.toLowerCase()),
),
);
if (matched) return false;

View file

@ -59,20 +59,30 @@ export class Antenna {
})
public users: string[];
@Column("jsonb", {
default: [],
@Column("varchar", {
length: 512,
array: true,
default: "{}",
})
public instances: string[];
@Column("jsonb", {
default: [],
// whitespace: AND condition
// 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", {
default: [],
// same match rule as `keywords`, except that this field is for excluded words
@Column("text", {
array: true,
default: "{}",
})
public excludeKeywords: string[][];
public excludeKeywords: string[];
@Column("boolean", {
default: false,

View file

@ -138,20 +138,6 @@ export class UserProfile {
})
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", {
default: false,
})
@ -200,12 +186,6 @@ export class UserProfile {
})
public pinnedPageId: Page["id"] | null;
@OneToOne((type) => Page, {
onDelete: "SET NULL",
})
@JoinColumn()
public pinnedPage: Page | null;
@Index()
@Column("boolean", {
default: false,
@ -213,19 +193,28 @@ export class UserProfile {
})
public enableWordMute: boolean;
@Column("jsonb", {
default: [],
// whitespace: AND condition
// 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", {
array: true,
nullable: false,
})
public mutedPatterns: string[];
@Column("jsonb", {
default: [],
@Column("varchar", {
length: 512,
array: true,
default: "{}",
comment: "List of instances muted by the user.",
})
public mutedInstances: string[];
@ -253,6 +242,13 @@ export class UserProfile {
})
@JoinColumn()
public user: Relation<User>;
@OneToOne(() => Page, {
onDelete: "SET NULL",
nullable: true,
})
@JoinColumn()
public pinnedPage: Relation<Page | null>;
//#endregion
constructor(data: Partial<UserProfile>) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -104,8 +104,22 @@ export const paramDef = {
} as const;
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 (ps.keywords.length === 0) throw new ApiError(meta.errors.noKeywords);
if (keywords.length === 0) throw new ApiError(meta.errors.noKeywords);
let userList;
let userGroupJoining;
@ -146,10 +160,10 @@ export default define(meta, paramDef, async (ps, user) => {
src: ps.src,
userListId: userList ? userList.id : null,
userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
keywords: keywords,
excludeKeywords: excludedWords,
users: ps.users,
instances: ps.instances,
instances: ps.instances.filter((instance) => instance.trim().length > 0),
caseSensitive: ps.caseSensitive,
withReplies: ps.withReplies,
withFile: ps.withFile,

View file

@ -100,6 +100,20 @@ export const paramDef = {
} as const;
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
const antenna = await Antennas.findOneBy({
id: ps.antennaId,
@ -138,10 +152,10 @@ export default define(meta, paramDef, async (ps, user) => {
src: ps.src,
userListId: userList ? userList.id : null,
userGroupJoiningId: userGroupJoining ? userGroupJoining.id : null,
keywords: ps.keywords,
excludeKeywords: ps.excludeKeywords,
keywords: keywords,
excludeKeywords: excludedWords,
users: ps.users,
instances: ps.instances,
instances: ps.instances.filter((instance) => instance.trim().length > 0),
caseSensitive: ps.caseSensitive,
withReplies: ps.withReplies,
withFile: ps.withFile,

View file

@ -125,7 +125,8 @@ export default define(meta, paramDef, async (ps, user) => {
query.andWhere(
new Brackets((qb) => {
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) {
// for backward compatibility
for (const item of ps.mutedWords) {
if (Array.isArray(item)) continue;
const regexp = item.match(/^\/(.+)\/(.*)$/);
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),
);
const flatten = (arr: string[][]) =>
JSON.stringify(arr) === "[[]]"
? ([] as string[])
: arr.map((row) => row.join(" "));
profileUpdates.mutedWords = flatten(ps.mutedWords);
}
if (
profileUpdates.mutedWords !== undefined ||