diff --git a/locales/index.d.ts b/locales/index.d.ts index 114247b43..6ce0fee33 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 907cbf0f5..ca678e305 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 000000000..4b921ad57 --- /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 6015296a2..58251430b 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 d4da22e1a..7d0ea6595 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 5d5812ff3..f94b8d3ba 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 d590244e3..76e847b7c 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 19bbb3882..a40957786 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 313d8412b..95ab3b5b3 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 c6084ce2c..76bb34d97 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 d61e4204b..b09fd0170 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 = {