diff --git a/locales/en-US.yml b/locales/en-US.yml index a8dd71c41..eac6d0c49 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1178,6 +1178,7 @@ useCdn: "Get assets from CDN" useCdnDescription: "Load some static assets like Twemoji from the JSDelivr CDN instead of this Firefish server." suggested: "Suggested" noLanguage: "No language" +searchCwAndAlt: "Include content warnings and alt texts" _sensitiveMediaDetection: description: "Reduces the effort of server moderation through automatically recognizing diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 971a8f10b..467d902b0 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2057,3 +2057,4 @@ searchUsersDescription: "投稿検索で投稿者を絞りたい場合、@user@e searchRange: "投稿期間(オプション)" searchRangeDescription: "投稿検索で投稿期間を絞りたい場合、20220615-20231031 のような形式で投稿期間を入力してください。今年の日付を指定する場合には年の指定を省略できます(0105-0106 や 20231105-0110 のように)。\n\n開始日と終了日のどちらか一方は省略可能です。例えば -0102 とすると今年1月2日までの投稿のみを、20231026- とすると2023年10月26日以降の投稿のみを検索します。" searchPostsWithFiles: "添付ファイルのある投稿のみ" +searchCwAndAlt: "閲覧注意の注釈と添付ファイルの代替テキストも検索する" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 3ce09488e..584e67459 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -2050,3 +2050,4 @@ searchUsersDescription: "如欲搜尋特定使用者的貼文,請以「@user@e searchWords: "搜尋關鍵字 / 查詢ID或URL" searchWordsDescription: "如欲搜尋貼文,請在此欄位輸入欲搜尋的關鍵字。以空格分隔關鍵字以進行AND搜尋、在關鍵字之間插入「OR」以進行OR搜尋。\n舉例來說,輸入「早上 晚上」會搜尋包含「早上」和「晚上」的貼文,「早上 OR 晚上」會搜尋包含「早上」或「晚上」(或兩者皆包含)的貼文。\n\n如欲前往特定使用者或貼文的頁面,請在此欄位輸入使用者ID(@user@example.com)或貼文的URL,並點擊「查詢」按鈕。點擊「搜尋」按鈕則會搜尋字面上包含輸入的ID或URL的貼文。" suggested: "建議" +searchCwAndAlt: "包含內容警告及替代文字" diff --git a/neko/revert.sql b/neko/revert.sql index f9ce2c7a8..729992b95 100644 --- a/neko/revert.sql +++ b/neko/revert.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'IndexAltTextAndCw1708872574733', 'SeparateHardMuteWordsAndPatterns1706413792769', 'RenameMetaColumns1705944717480', 'RemoveNativeUtilsMigration1705877093218', @@ -13,6 +14,10 @@ DELETE FROM "migrations" WHERE name IN ( 'EmojiModerator1692825433698' ); +-- index-alt-text-and-cw +DROP INDEX "IDX_f4f7b93d05958527300d79ac82"; +DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"; + -- separate-hard-mute-words-and-patterns UPDATE "user_profile" SET "mutedWords" = "mutedWords" || array_to_json("mutedPatterns")::jsonb; ALTER TABLE "user_profile" DROP "mutedPatterns"; diff --git a/packages/backend/migration-neko/1708872574733-index-alt-text-and-cw.js b/packages/backend/migration-neko/1708872574733-index-alt-text-and-cw.js new file mode 100644 index 000000000..dbe4ee3c1 --- /dev/null +++ b/packages/backend/migration-neko/1708872574733-index-alt-text-and-cw.js @@ -0,0 +1,17 @@ +export class IndexAltTextAndCw1708872574733 { + name = "IndexAltTextAndCw1708872574733"; + + async up(queryRunner) { + await queryRunner.query( + `CREATE INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f" ON "note" USING "pgroonga" ("cw" pgroonga_varchar_full_text_search_ops_v2)`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_f4f7b93d05958527300d79ac82" ON "drive_file" USING "pgroonga" ("comment" pgroonga_varchar_full_text_search_ops_v2)`, + ); + } + + async down(queryRunner) { + await queryRunner.query(`DROP INDEX "IDX_f4f7b93d05958527300d79ac82"`); + await queryRunner.query(`DROP INDEX "IDX_8e3bbbeb3df04d1a8105da4c8f"`); + } +} diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 52ab027be..db45b536b 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -70,6 +70,7 @@ export class DriveFile { }) public size: number; + @Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2 @Column("varchar", { length: DB_MAX_IMAGE_COMMENT_LENGTH, nullable: true, diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 79d9ade78..a61f167c1 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -79,6 +79,7 @@ export class Note { }) public name: string | null; + @Index() // USING pgroonga pgroonga_varchar_full_text_search_ops_v2 @Column("varchar", { length: 512, nullable: true, diff --git a/packages/backend/src/server/api/endpoints/notes/search.ts b/packages/backend/src/server/api/endpoints/notes/search.ts index bc17944bb..cc97e6f62 100644 --- a/packages/backend/src/server/api/endpoints/notes/search.ts +++ b/packages/backend/src/server/api/endpoints/notes/search.ts @@ -1,4 +1,5 @@ // import { FindManyOptions, In } from "typeorm"; +import { Brackets } from "typeorm"; import { Notes } from "@/models/index.js"; import { Note } from "@/models/entities/note.js"; // import config from "@/config/index.js"; @@ -56,6 +57,7 @@ export const paramDef = { default: null, }, withFiles: { type: "boolean", nullable: true }, + searchCwAndAlt: { type: "boolean", nullable: true }, channelId: { type: "string", format: "misskey:id", @@ -92,7 +94,28 @@ export default define(meta, paramDef, async (ps, me) => { } if (ps.query != null) { - query.andWhere("note.text &@~ :q", { q: `${sqlLikeEscape(ps.query)}` }); + 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 }, + ); + }), + ); + } else { + query.andWhere("note.text &@~ :q", { q }); + } } query.innerJoinAndSelect("note.user", "user"); diff --git a/packages/client/src/components/MkSearchBox.vue b/packages/client/src/components/MkSearchBox.vue index bb61a982d..e702fd76a 100644 --- a/packages/client/src/components/MkSearchBox.vue +++ b/packages/client/src/components/MkSearchBox.vue @@ -68,6 +68,12 @@ :class="$style.input" >{{ i18n.ts.searchPostsWithFiles }} + {{ i18n.ts.searchCwAndAlt }}
{{ i18n.ts.search }} @@ -128,7 +134,8 @@ const searchRange = ref( }` : "", ); -const searchPostsWithFiles = ref(searchParams.get("withFiles") === "true"); +const searchPostsWithFiles = ref(searchParams.get("withFiles") === "1"); +const searchCwAndAlt = ref(searchParams.get("detailed") === "1"); function done(canceled: boolean, result?: searchQuery) { emit("done", { canceled, result }); @@ -149,6 +156,7 @@ function search() { from: searchUsers.value === "" ? undefined : searchUsers.value, range: searchRange.value === "" ? undefined : searchRange.value, withFiles: searchPostsWithFiles.value, + searchCwAndAlt: searchCwAndAlt.value, }); } diff --git a/packages/client/src/pages/search.vue b/packages/client/src/pages/search.vue index aab1e8a17..5967b4e54 100644 --- a/packages/client/src/pages/search.vue +++ b/packages/client/src/pages/search.vue @@ -63,7 +63,8 @@ const props = defineProps<{ since?: string; until?: string; channel?: string; - withFiles: "false" | "true"; + withFiles: "0" | "1"; + searchCwAndAlt: "0" | "1"; }>(); const userId = props.user == null ? undefined : await getUserId(props.user); @@ -79,7 +80,8 @@ const notesPagination = { props.since == null ? undefined : getUnixTime(props.since, false), untilDate: props.until == null ? undefined : getUnixTime(props.until, true), - withFiles: props.withFiles === "true", + withFiles: props.withFiles === "1", + searchCwAndAlt: props.searchCwAndAlt === "1", channelId: props.channel, })), }; diff --git a/packages/client/src/router.ts b/packages/client/src/router.ts index 09a1bee36..6dba56ffc 100644 --- a/packages/client/src/router.ts +++ b/packages/client/src/router.ts @@ -310,6 +310,7 @@ export const routes = [ until: "until", withFiles: "withFiles", channel: "channel", + detailed: "searchCwAndAlt", }, }, { diff --git a/packages/client/src/scripts/search.ts b/packages/client/src/scripts/search.ts index bd34f8cb0..afa3e939a 100644 --- a/packages/client/src/scripts/search.ts +++ b/packages/client/src/scripts/search.ts @@ -21,6 +21,7 @@ export async function search() { from?: string; range?: string; withFiles: boolean; + searchCwAndAlt: boolean; }; } >((resolve, _) => { @@ -69,24 +70,35 @@ export async function search() { } if (result.action === "search") { - let paramString = `withFiles=${result.withFiles ? "true" : "false"}`; + const params = new URLSearchParams(); + + params.append("withFiles", result.withFiles ? "1" : "0"); if (result.query != null) { - paramString += `&q=${encodeURIComponent(result.query)}`; + params.append("q", result.query); } if (result.from != null) { if (result.from === "me" || result.from.includes("@")) - paramString += `&user=${encodeURIComponent(result.from)}`; - else paramString += `&host=${encodeURIComponent(result.from)}`; + params.append("user", result.from); + else params.append("host", result.from); } if (result.range != null) { const split = result.range.split("-"); - if (split[0] !== "") paramString += `&since=${split[0]}`; - if (split[1] !== "") paramString += `&until=${split[1]}`; + if (split.length === 1) { + params.append("since", result.range); + params.append("until", result.range); + } else { + if (split[0] !== "") params.append("since", split[0]); + if (split[1] !== "") params.append("until", split[1]); + } } - mainRouter.push(`/search?${paramString}`); + if (result.searchCwAndAlt) { + params.append("detailed", "1"); + } + + mainRouter.push(`/search?${params.toString()}`); } }