From 6424b685e971578427c8db5b14710494c5cdc0fd Mon Sep 17 00:00:00 2001 From: naskya Date: Tue, 7 Nov 2023 09:09:57 +0900 Subject: [PATCH] feat: ability to pin custom pages to the help menu --- README.md | 1 + locales/en-US.yml | 2 + locales/ja-JP.yml | 2 + neko/revert.sql | 3 ++ .../migration-neko/1699305365258-more-urls.js | 13 ++++++ .../native-utils/src/model/entity/meta.rs | 2 + packages/backend/src/models/entities/meta.ts | 6 +++ .../src/server/api/endpoints/admin/meta.ts | 2 +- .../server/api/endpoints/admin/update-meta.ts | 33 +++++++++++++- .../backend/src/server/api/endpoints/meta.ts | 6 +++ packages/client/src/pages/admin/settings.vue | 43 ++++++++++++++++++- packages/client/src/scripts/helpMenu.ts | 38 +++++++++++----- 12 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 packages/backend/migration-neko/1699305365258-more-urls.js diff --git a/README.md b/README.md index 771921472..cb344210d 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ ## 細かい変更点 +- サーバーの管理者が左下のヘルプメニューに利用規約以外のページも固定できるように - 依存ライブラリのバージョンをアップデート - AiScript のバージョンも上がりました! - 絵文字ピッカーに表示されるカスタム絵文字の検索結果の件数を最大 100 件に変更(Misskey の変更を取り込み) diff --git a/locales/en-US.yml b/locales/en-US.yml index 033d0a272..6e1c33232 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2188,3 +2188,5 @@ _iconSets: regular: "Regular" fill: "Filled" duotone: "Duotone" +moreUrls: "pinned pages" +moreUrlsDescription: "Enter the pages you want to pin to the help menu in the lower left corner using this notation:\n\"Display name\": https://example.com/" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 45c5b9aca..bbe07c23f 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2030,3 +2030,5 @@ _iconSets: regular: "標準" fill: "塗りつぶし" duotone: "2色" +moreUrls: "固定するページ" +moreUrlsDescription: "左下のヘルプメニューに固定したいページを以下の形式で、改行区切りで入力してください:\n\"表示名\": https://example.com/" diff --git a/neko/revert.sql b/neko/revert.sql index 387bd582c..8350f37d3 100644 --- a/neko/revert.sql +++ b/neko/revert.sql @@ -1,3 +1,6 @@ +-- more-urls +ALTER TABLE "meta" DROP COLUMN "moreUrls"; + -- pgroonga DROP INDEX "public"."IDX_f27f5d88941e57442be75ba9c8"; DROP INDEX "public"."IDX_065d4d8f3b5adb4a08841eae3c"; diff --git a/packages/backend/migration-neko/1699305365258-more-urls.js b/packages/backend/migration-neko/1699305365258-more-urls.js new file mode 100644 index 000000000..6ef1dcd2b --- /dev/null +++ b/packages/backend/migration-neko/1699305365258-more-urls.js @@ -0,0 +1,13 @@ +export class MoreUrls1699305365258 { + name = "MoreUrls1699305365258"; + + async up(queryRunner) { + queryRunner.query( + `ALTER TABLE "meta" ADD "moreUrls" jsonb NOT NULL DEFAULT '[]'`, + ); + } + + async down(queryRunner) { + queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "moreUrls"`); + } +} diff --git a/packages/backend/native-utils/src/model/entity/meta.rs b/packages/backend/native-utils/src/model/entity/meta.rs index 3d203a015..79ff8477a 100644 --- a/packages/backend/native-utils/src/model/entity/meta.rs +++ b/packages/backend/native-utils/src/model/entity/meta.rs @@ -75,6 +75,8 @@ pub struct Model { pub pinned_users: StringVec, #[sea_orm(column_name = "ToSUrl")] pub to_s_url: Option, + #[sea_orm(column_name = "moreUrls", column_type = "JsonBinary")] + pub more_urls: Json, #[sea_orm(column_name = "repositoryUrl")] pub repository_url: String, #[sea_orm(column_name = "feedbackUrl")] diff --git a/packages/backend/src/models/entities/meta.ts b/packages/backend/src/models/entities/meta.ts index 26648caa0..25112ed44 100644 --- a/packages/backend/src/models/entities/meta.ts +++ b/packages/backend/src/models/entities/meta.ts @@ -383,6 +383,12 @@ export class Meta { }) public ToSUrl: string | null; + @Column("jsonb", { + default: [], + nullable: false, + }) + public moreUrls: [string, string][]; + @Column("varchar", { length: 512, default: "https://code.naskya.net/naskya/firefish", diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 2a0a7cfbb..8cffb34f0 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -2,7 +2,6 @@ import config from "@/config/index.js"; import { fetchMeta } from "@/misc/fetch-meta.js"; import { MAX_NOTE_TEXT_LENGTH, MAX_CAPTION_TEXT_LENGTH } from "@/const.js"; import define from "../../define.js"; -import { Exp } from "@tensorflow/tfjs"; export const meta = { tags: ["meta"], @@ -473,6 +472,7 @@ export default define(meta, paramDef, async (ps, me) => { description: instance.description, langs: instance.langs, tosUrl: instance.ToSUrl, + moreUrls: instance.moreUrls, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, disableRegistration: instance.disableRegistration, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 00a472ed3..422cf49f7 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -1,7 +1,7 @@ import { Meta } from "@/models/entities/meta.js"; import { insertModerationLog } from "@/services/insert-moderation-log.js"; import { db } from "@/db/postgre.js"; -import define from "../../define.js"; +import define from "@/server/api/define.js"; export const meta = { tags: ["admin"], @@ -143,6 +143,17 @@ export const paramDef = { swPublicKey: { type: "string", nullable: true }, swPrivateKey: { type: "string", nullable: true }, tosUrl: { type: "string", nullable: true }, + moreUrls: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + url: { type: "string" }, + }, + }, + nullable: true, + }, repositoryUrl: { type: "string" }, feedbackUrl: { type: "string" }, useObjectStorage: { type: "boolean" }, @@ -174,6 +185,18 @@ export const paramDef = { required: [], } as const; +function isValidHttpUrl(src: string) { + let url; + + try { + url = new URL(src); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; +} + export default define(meta, paramDef, async (ps, me) => { const set = {} as Partial; @@ -434,6 +457,14 @@ export default define(meta, paramDef, async (ps, me) => { set.ToSUrl = ps.tosUrl; } + if (ps.moreUrls !== undefined) { + const areUrlsVaild = ps.moreUrls.every( + (obj: { name: string; url: string }) => isValidHttpUrl(String(obj.url)), + ); + if (!areUrlsVaild) throw new Error("invalid URL"); + set.moreUrls = ps.moreUrls; + } + if (ps.repositoryUrl !== undefined) { set.repositoryUrl = ps.repositoryUrl; } diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts index 3e1530598..5889d00a7 100644 --- a/packages/backend/src/server/api/endpoints/meta.ts +++ b/packages/backend/src/server/api/endpoints/meta.ts @@ -64,6 +64,11 @@ export const meta = { optional: false, nullable: true, }, + moreUrls: { + type: "object", + optional: false, + nullable: false, + }, repositoryUrl: { type: "string", optional: false, @@ -416,6 +421,7 @@ export default define(meta, paramDef, async (ps, me) => { description: instance.description, langs: instance.langs, tosUrl: instance.ToSUrl, + moreUrls: instance.moreUrls, repositoryUrl: instance.repositoryUrl, feedbackUrl: instance.feedbackUrl, diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index 8de9f749e..b54e5349b 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -24,11 +24,18 @@ + + + + + (null); const description = ref(null); const tosUrl = ref(null); +const moreUrls = ref(null); const maintainerName = ref(null); const maintainerEmail = ref(null); const donationLink = ref(null); @@ -480,12 +488,44 @@ const defaultReactionCustom = ref(""); const enableServerMachineStats = ref(false); const enableIdenticonGeneration = ref(false); +function isValidHttpUrl(src: string) { + let url: URL; + try { + url = new URL(src); + } catch (_) { + return false; + } + return url.protocol === "http:" || url.protocol === "https:"; +} + +function parseMoreUrls(src: string): { name: string; url: string }[] { + const toReturn: { name: string; url: string }[] = []; + const pattern = /"(.+)"\s*:\s*(http.+)/; + src.trim() + .split("\n") + .forEach((line) => { + const match = pattern.exec(line); + if (match != null && isValidHttpUrl(match[2])) + toReturn.push({ name: match[1], url: match[2] }); + else console.error(`invalid syntax or invalid URL: ${line}`); + }); + return toReturn; +} + +function stringifyMoreUrls(src: { name: string; url: string }[]): string { + let toReturn = ""; + for (const { name, url } of src) + toReturn = toReturn.concat(`"${name}": ${url}`, "\n"); + return toReturn; +} + async function init() { const meta = await os.api("admin/meta"); if (!meta) throw new Error("No meta"); name.value = meta.name; description.value = meta.description; tosUrl.value = meta.tosUrl; + moreUrls.value = stringifyMoreUrls(meta.moreUrls); iconUrl.value = meta.iconUrl; bannerUrl.value = meta.bannerUrl; logoImageUrl.value = meta.logoImageUrl; @@ -535,6 +575,7 @@ function save() { name: name.value, description: description.value, tosUrl: tosUrl.value, + moreUrls: parseMoreUrls(moreUrls.value ?? ""), iconUrl: iconUrl.value, bannerUrl: bannerUrl.value, logoImageUrl: logoImageUrl.value, diff --git a/packages/client/src/scripts/helpMenu.ts b/packages/client/src/scripts/helpMenu.ts index ab5bc09d4..2291afa7d 100644 --- a/packages/client/src/scripts/helpMenu.ts +++ b/packages/client/src/scripts/helpMenu.ts @@ -6,6 +6,31 @@ import { host } from "@/config"; import * as os from "@/os"; import { i18n } from "@/i18n"; import icon from "@/scripts/icon"; +import type { MenuItem } from "@/types/menu"; + +const instanceSpecificItems: MenuItem[] = []; + +if (instance.tosUrl != null) { + instanceSpecificItems.push({ + type: "button", + text: i18n.ts.tos, + icon: `${icon("ph-scroll")}`, + action: () => { + window.open(instance.tosUrl, "_blank"); + }, + }); +} + +for (const { name, url } of instance.moreUrls) { + instanceSpecificItems.push({ + type: "button", + text: name, + icon: `${icon("ph-link-simple")}`, + action: () => { + window.open(url, "_blank"); + }, + }); +} export function openHelpMenu_(ev: MouseEvent) { os.popupMenu( @@ -26,16 +51,9 @@ export function openHelpMenu_(ev: MouseEvent) { icon: `${icon("ph-lightbulb")}`, to: "/about-firefish", }, - instance.tosUrl - ? { - type: "button", - text: i18n.ts.tos, - icon: `${icon("ph-scroll")}`, - action: () => { - window.open(instance.tosUrl, "_blank"); - }, - } - : null, + ...(instanceSpecificItems.length >= 2 ? [null] : []), + ...instanceSpecificItems, + null, { type: "button", text: i18n.ts.apps,