URLプレビューのサムネイルを隠す機能を追加 (MisskeyIO#214)

This commit is contained in:
CyberRex 2023-11-07 02:31:26 +09:00 committed by GitHub
parent f229e26312
commit ec5e1df9f5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 76 additions and 1 deletions

2
locales/index.d.ts vendored
View file

@ -1113,6 +1113,8 @@ export interface Locale {
"refreshing": string; "refreshing": string;
"pullDownToRefresh": string; "pullDownToRefresh": string;
"disableStreamingTimeline": string; "disableStreamingTimeline": string;
"urlPreviewDenyList": string;
"urlPreviewDenyListDescription": string;
"_announcement": { "_announcement": {
"forExistingUsers": string; "forExistingUsers": string;
"forExistingUsersDescription": string; "forExistingUsersDescription": string;

View file

@ -1110,6 +1110,8 @@ releaseToRefresh: "離してリロード"
refreshing: "リロード中" refreshing: "リロード中"
pullDownToRefresh: "引っ張ってリロード" pullDownToRefresh: "引っ張ってリロード"
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする" disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
urlPreviewDenyList: "サムネイルの表示を制限するURL"
urlPreviewDenyListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルがぼかされて表示されます。"
_announcement: _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"

View file

@ -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"`);
}
}

View file

@ -468,4 +468,9 @@ export class MiMeta {
default: 300, default: 300,
}) })
public perUserListTimelineCacheMax: number; public perUserListTimelineCacheMax: number;
@Column('varchar', {
length: 3072, array: true, default: '{}',
})
public urlPreviewDenyList: string[];
} }

View file

@ -298,6 +298,14 @@ export const meta = {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
}, },
urlPreviewDenyList: {
type: 'array',
optional: true, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
}, },
}, },
} as const; } as const;
@ -404,6 +412,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax, perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
urlPreviewDenyList: instance.urlPreviewDenyList,
}; };
}); });
} }

View file

@ -110,6 +110,9 @@ export const paramDef = {
perRemoteUserUserTimelineCacheMax: { type: 'integer' }, perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' }, perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' }, perUserListTimelineCacheMax: { type: 'integer' },
urlPreviewDenyList: { type: 'array', nullable: true, items: {
type: 'string',
} },
}, },
required: [], required: [],
} as const; } as const;
@ -147,6 +150,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.sensitiveWords = ps.sensitiveWords.filter(Boolean); set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
} }
if (Array.isArray(ps.urlPreviewDenyList)) {
set.urlPreviewDenyList = ps.urlPreviewDenyList.filter(Boolean);
}
if (ps.themeColor !== undefined) { if (ps.themeColor !== undefined) {
set.themeColor = ps.themeColor; set.themeColor = ps.themeColor;
} }

View file

@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { summaly } from 'summaly'; import { summaly } from 'summaly';
import RE2 from 're2';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
@ -94,6 +95,23 @@ export class UrlPreviewService {
summary.icon = this.wrap(summary.icon); summary.icon = this.wrap(summary.icon);
summary.thumbnail = this.wrap(summary.thumbnail); 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 // Cache 7days
reply.header('Cache-Control', 'max-age=604800, immutable'); reply.header('Cache-Control', 'max-age=604800, immutable');

View file

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<div v-else> <div v-else>
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`"> <div v-if="thumbnail" :class="[$style.thumbnail, { [$style.thumbnailBlur]: sensitive }]" :style="`background-image: url('${thumbnail}')`">
</div> </div>
<article :class="$style.body"> <article :class="$style.body">
<header :class="$style.header"> <header :class="$style.header">
@ -118,6 +118,7 @@ let description = $ref<string | null>(null);
let thumbnail = $ref<string | null>(null); let thumbnail = $ref<string | null>(null);
let icon = $ref<string | null>(null); let icon = $ref<string | null>(null);
let sitename = $ref<string | null>(null); let sitename = $ref<string | null>(null);
let sensitive = $ref<boolean | undefined>(undefined);
let player = $ref({ let player = $ref({
url: null, url: null,
width: null, width: null,
@ -170,6 +171,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
icon = info.icon; icon = info.icon;
sitename = info.sitename; sitename = info.sitename;
player = info.player; player = info.player;
sensitive = info.sensitive;
}); });
function adjustTweetHeight(message: any) { function adjustTweetHeight(message: any) {
@ -319,6 +321,10 @@ onUnmounted(() => {
margin-top: 6px; margin-top: 6px;
} }
.thumbnailBlur {
filter: blur(8px);
}
@container (max-width: 400px) { @container (max-width: 400px) {
.link { .link {
font-size: 12px; font-size: 12px;

View file

@ -34,6 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.sensitiveWords }}</template> <template #label>{{ i18n.ts.sensitiveWords }}</template>
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template> <template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
</MkTextarea> </MkTextarea>
<MkTextarea v-model="urlPreviewDenyList">
<template #label>{{ i18n.ts.urlPreviewDenyList }}</template>
<template #caption>{{ i18n.ts.urlPreviewDenyListDescription }}</template>
</MkTextarea>
</div> </div>
</FormSuspense> </FormSuspense>
</MkSpacer> </MkSpacer>
@ -69,6 +74,7 @@ let emailRequiredForSignup: boolean = $ref(false);
let sensitiveWords: string = $ref(''); let sensitiveWords: string = $ref('');
let preservedUsernames: string = $ref(''); let preservedUsernames: string = $ref('');
let tosUrl: string | null = $ref(null); let tosUrl: string | null = $ref(null);
let urlPreviewDenyList: string = $ref('');
async function init() { async function init() {
const meta = await os.api('admin/meta'); const meta = await os.api('admin/meta');
@ -77,6 +83,7 @@ async function init() {
sensitiveWords = meta.sensitiveWords.join('\n'); sensitiveWords = meta.sensitiveWords.join('\n');
preservedUsernames = meta.preservedUsernames.join('\n'); preservedUsernames = meta.preservedUsernames.join('\n');
tosUrl = meta.tosUrl; tosUrl = meta.tosUrl;
urlPreviewDenyList = meta.urlPreviewDenyList.join('\n');
} }
function save() { function save() {
@ -86,6 +93,7 @@ function save() {
tosUrl, tosUrl,
sensitiveWords: sensitiveWords.split('\n'), sensitiveWords: sensitiveWords.split('\n'),
preservedUsernames: preservedUsernames.split('\n'), preservedUsernames: preservedUsernames.split('\n'),
urlPreviewDenyList: urlPreviewDenyList.split('\n'),
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance();
}); });

View file

@ -20,6 +20,7 @@ type Ad = TODO_2;
// @public (undocumented) // @public (undocumented)
type AdminInstanceMetadata = DetailedInstanceMetadata & { type AdminInstanceMetadata = DetailedInstanceMetadata & {
blockedHosts: string[]; blockedHosts: string[];
urlPreviewDenyList: string[];
}; };
// @public (undocumented) // @public (undocumented)

View file

@ -355,6 +355,7 @@ export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
export type AdminInstanceMetadata = DetailedInstanceMetadata & { export type AdminInstanceMetadata = DetailedInstanceMetadata & {
// TODO: There are more fields. // TODO: There are more fields.
blockedHosts: string[]; blockedHosts: string[];
urlPreviewDenyList: string[];
}; };
export type ServerInfo = { export type ServerInfo = {