From ec5e1df9f56aabf24815d8a0b036f51ea863f31f Mon Sep 17 00:00:00 2001 From: CyberRex Date: Tue, 7 Nov 2023 02:31:26 +0900 Subject: [PATCH] =?UTF-8?q?URL=E3=83=97=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E3=82=B5=E3=83=A0=E3=83=8D=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=82=92=E9=9A=A0=E3=81=99=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=20(MisskeyIO#214)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/index.d.ts | 2 ++ locales/ja-JP.yml | 2 ++ .../1699284486293-urlPreviewDenyList.js | 16 ++++++++++++++++ packages/backend/src/models/entities/Meta.ts | 5 +++++ .../src/server/api/endpoints/admin/meta.ts | 9 +++++++++ .../server/api/endpoints/admin/update-meta.ts | 7 +++++++ .../src/server/web/UrlPreviewService.ts | 18 ++++++++++++++++++ .../frontend/src/components/MkUrlPreview.vue | 8 +++++++- .../frontend/src/pages/admin/moderation.vue | 8 ++++++++ packages/misskey-js/etc/misskey-js.api.md | 1 + packages/misskey-js/src/entities.ts | 1 + 11 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 packages/backend/migration/1699284486293-urlPreviewDenyList.js diff --git a/locales/index.d.ts b/locales/index.d.ts index 114247b437..6ce0fee334 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1113,6 +1113,8 @@ export interface Locale { "refreshing": string; "pullDownToRefresh": string; "disableStreamingTimeline": string; + "urlPreviewDenyList": string; + "urlPreviewDenyListDescription": string; "_announcement": { "forExistingUsers": string; "forExistingUsersDescription": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 907cbf0f5f..ca678e3058 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1110,6 +1110,8 @@ releaseToRefresh: "離してリロード" refreshing: "リロード中" pullDownToRefresh: "引っ張ってリロード" disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" +urlPreviewDenyList: "サムネイルの表示を制限するURL" +urlPreviewDenyListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルがぼかされて表示されます。" _announcement: forExistingUsers: "既存ユーザーのみ" diff --git a/packages/backend/migration/1699284486293-urlPreviewDenyList.js b/packages/backend/migration/1699284486293-urlPreviewDenyList.js new file mode 100644 index 0000000000..4b921ad579 --- /dev/null +++ b/packages/backend/migration/1699284486293-urlPreviewDenyList.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class UrlPreviewDenyList1699284486293 { + name = 'UrlPreviewDenyList1699284486293' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewDenyList" character varying(3072) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewDenyList"`); + } +} diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts index 6015296a20..58251430b0 100644 --- a/packages/backend/src/models/entities/Meta.ts +++ b/packages/backend/src/models/entities/Meta.ts @@ -468,4 +468,9 @@ export class MiMeta { default: 300, }) public perUserListTimelineCacheMax: number; + + @Column('varchar', { + length: 3072, array: true, default: '{}', + }) + public urlPreviewDenyList: string[]; } diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index d4da22e1a0..7d0ea65959 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -298,6 +298,14 @@ export const meta = { type: 'number', optional: false, nullable: false, }, + urlPreviewDenyList: { + type: 'array', + optional: true, nullable: false, + items: { + type: 'string', + optional: false, nullable: false, + }, + }, }, }, } as const; @@ -404,6 +412,7 @@ export default class extends Endpoint { perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, + urlPreviewDenyList: instance.urlPreviewDenyList, }; }); } 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 5d5812ff38..f94b8d3ba9 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -110,6 +110,9 @@ export const paramDef = { perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' }, perUserListTimelineCacheMax: { type: 'integer' }, + urlPreviewDenyList: { type: 'array', nullable: true, items: { + type: 'string', + } }, }, required: [], } as const; @@ -147,6 +150,10 @@ export default class extends Endpoint { set.sensitiveWords = ps.sensitiveWords.filter(Boolean); } + if (Array.isArray(ps.urlPreviewDenyList)) { + set.urlPreviewDenyList = ps.urlPreviewDenyList.filter(Boolean); + } + if (ps.themeColor !== undefined) { set.themeColor = ps.themeColor; } diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index d590244e34..76e847b7ce 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -5,6 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { summaly } from 'summaly'; +import RE2 from 're2'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MetaService } from '@/core/MetaService.js'; @@ -94,6 +95,23 @@ export class UrlPreviewService { summary.icon = this.wrap(summary.icon); summary.thumbnail = this.wrap(summary.thumbnail); + const includeDenyList = meta.urlPreviewDenyList.some(filter => { + // represents RegExp + const regexp = /^\/(.+)\/(.*)$/.exec(filter); + // This should never happen due to input sanitisation. + if (!regexp) { + const words = filter.split(' '); + return words.every(keyword => summary.url.includes(keyword)); + } + try { + return new RE2(regexp[1], regexp[2]).test(summary.url); + } catch (err) { + // This should never happen due to input sanitisation. + return false; + } + }); + if (includeDenyList) summary.sensitive = true; + // Cache 7days reply.header('Cache-Control', 'max-age=604800, immutable'); diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 19bbb3882f..a40957786b 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
@@ -118,6 +118,7 @@ let description = $ref(null); let thumbnail = $ref(null); let icon = $ref(null); let sitename = $ref(null); +let sensitive = $ref(undefined); let player = $ref({ url: null, width: null, @@ -170,6 +171,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa icon = info.icon; sitename = info.sitename; player = info.player; + sensitive = info.sensitive; }); function adjustTweetHeight(message: any) { @@ -319,6 +321,10 @@ onUnmounted(() => { margin-top: 6px; } +.thumbnailBlur { + filter: blur(8px); +} + @container (max-width: 400px) { .link { font-size: 12px; diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 313d8412b2..95ab3b5b39 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -34,6 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + +
@@ -69,6 +74,7 @@ let emailRequiredForSignup: boolean = $ref(false); let sensitiveWords: string = $ref(''); let preservedUsernames: string = $ref(''); let tosUrl: string | null = $ref(null); +let urlPreviewDenyList: string = $ref(''); async function init() { const meta = await os.api('admin/meta'); @@ -77,6 +83,7 @@ async function init() { sensitiveWords = meta.sensitiveWords.join('\n'); preservedUsernames = meta.preservedUsernames.join('\n'); tosUrl = meta.tosUrl; + urlPreviewDenyList = meta.urlPreviewDenyList.join('\n'); } function save() { @@ -86,6 +93,7 @@ function save() { tosUrl, sensitiveWords: sensitiveWords.split('\n'), preservedUsernames: preservedUsernames.split('\n'), + urlPreviewDenyList: urlPreviewDenyList.split('\n'), }).then(() => { fetchInstance(); }); diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index c6084ce2c0..76bb34d97f 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -20,6 +20,7 @@ type Ad = TODO_2; // @public (undocumented) type AdminInstanceMetadata = DetailedInstanceMetadata & { blockedHosts: string[]; + urlPreviewDenyList: string[]; }; // @public (undocumented) diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index d61e4204bb..b09fd01707 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -355,6 +355,7 @@ export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata; export type AdminInstanceMetadata = DetailedInstanceMetadata & { // TODO: There are more fields. blockedHosts: string[]; + urlPreviewDenyList: string[]; }; export type ServerInfo = {