URLプレビューのサムネイルを隠す機能を追加 (MisskeyIO#214)
This commit is contained in:
parent
f229e26312
commit
ec5e1df9f5
2
locales/index.d.ts
vendored
2
locales/index.d.ts
vendored
|
@ -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;
|
||||||
|
|
|
@ -1110,6 +1110,8 @@ releaseToRefresh: "離してリロード"
|
||||||
refreshing: "リロード中"
|
refreshing: "リロード中"
|
||||||
pullDownToRefresh: "引っ張ってリロード"
|
pullDownToRefresh: "引っ張ってリロード"
|
||||||
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
|
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
|
||||||
|
urlPreviewDenyList: "サムネイルの表示を制限するURL"
|
||||||
|
urlPreviewDenyListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルがぼかされて表示されます。"
|
||||||
|
|
||||||
_announcement:
|
_announcement:
|
||||||
forExistingUsers: "既存ユーザーのみ"
|
forExistingUsers: "既存ユーザーのみ"
|
||||||
|
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Loading…
Reference in a new issue