From ebf291084f9490f0345a488592e2b4cd2ffb9d91 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 9 May 2025 17:44:35 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20UGC=E3=81=AE=E5=85=AC=E9=96=8B=E7=AF=84?= =?UTF-8?q?=E5=9B=B2=E8=A8=AD=E5=AE=9A=E6=A9=9F=E8=83=BD=20(#15938)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * Update CHANGELOG.md * wip * wip * Update show.ts --- CHANGELOG.md | 5 +++- locales/index.d.ts | 26 +++++++++++++++++++ locales/ja-JP.yml | 8 ++++++ ...GeneratedContentsForNonLoggedInVisitors.js | 16 ++++++++++++ packages/backend/src/models/Meta.ts | 6 +++++ .../src/server/api/endpoints/admin/meta.ts | 6 +++++ .../server/api/endpoints/admin/update-meta.ts | 8 ++++++ .../src/server/api/endpoints/notes/show.ts | 15 ++++++++++- .../src/server/api/endpoints/users/show.ts | 17 +++++++++++- .../src/server/web/ClientServerService.ts | 15 +++++++++-- .../frontend/src/pages/admin/moderation.vue | 24 ++++++++++++++++- packages/misskey-js/src/autogen/types.ts | 4 +++ 12 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 packages/backend/migration/1746330901644-visibleUserGeneratedContentsForNonLoggedInVisitors.js diff --git a/CHANGELOG.md b/CHANGELOG.md index efde2ed192..dd5ede9be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ ## Unreleased ### General -- +- Feat: 非ログインでサーバーを閲覧された際に、サーバー内のコンテンツを非公開にすることができるようになりました + - モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます + - 「全て公開(今までの挙動)」「ローカルのコンテンツだけ公開(=サーバー内で受信されたリモートのコンテンツは公開しない)」「何も公開しない」から選択できます + - デフォルト値は「ローカルのコンテンツだけ公開」になっています ### Client - Feat: Websocket接続を行わずにMisskeyを利用するNo Websocketモードが実装されました(beta) diff --git a/locales/index.d.ts b/locales/index.d.ts index 2b92a0ae7c..e10506bfe3 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6412,6 +6412,32 @@ export interface Locale extends ILocale { * 脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。 */ "deliverSuspendedSoftwareDescription": string; + /** + * 非利用者に対するユーザー作成コンテンツの公開範囲 + */ + "userGeneratedContentsVisibilityForVisitor": string; + /** + * モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。 + */ + "userGeneratedContentsVisibilityForVisitor_description": string; + /** + * サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。 + */ + "userGeneratedContentsVisibilityForVisitor_description2": string; + "_userGeneratedContentsVisibilityForVisitor": { + /** + * 全て公開 + */ + "all": string; + /** + * ローカルコンテンツのみ公開し、リモートコンテンツは非公開 + */ + "localOnly": string; + /** + * 全て非公開 + */ + "none": string; + }; }; "_accountMigration": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 6c5306dfa3..aeaff7e875 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1629,6 +1629,14 @@ _serverSettings: thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。" deliverSuspendedSoftware: "配信停止中のソフトウェア" deliverSuspendedSoftwareDescription: "脆弱性などの理由で、サーバーのソフトウェアの名前及びバージョンの範囲を指定して配信を停止できます。このバージョン情報はサーバーが提供したものであり、信頼性は保証されません。バージョン指定には semver の範囲指定が使用できますが、>= 2024.3.1 と指定すると 2024.3.1-custom.0 のようなカスタムバージョンが含まれないため、>= 2024.3.1-0 のように prerelease の指定を行うことを推奨します。" + userGeneratedContentsVisibilityForVisitor: "非利用者に対するユーザー作成コンテンツの公開範囲" + userGeneratedContentsVisibilityForVisitor_description: "モデレーションが行き届きにくい不適切なリモートコンテンツなどが、自サーバー経由で図らずもインターネットに公開されてしまうことによるトラブル防止などに役立ちます。" + userGeneratedContentsVisibilityForVisitor_description2: "サーバーで受信したリモートのコンテンツを含め、サーバー内の全てのコンテンツを無条件でインターネットに公開することはリスクが伴います。特に、分散型の特性を知らない閲覧者にとっては、リモートのコンテンツであってもサーバー内で作成されたコンテンツであると誤って認識してしまう可能性があるため、注意が必要です。" + + _userGeneratedContentsVisibilityForVisitor: + all: "全て公開" + localOnly: "ローカルコンテンツのみ公開し、リモートコンテンツは非公開" + none: "全て非公開" _accountMigration: moveFrom: "別のアカウントからこのアカウントに移行" diff --git a/packages/backend/migration/1746330901644-visibleUserGeneratedContentsForNonLoggedInVisitors.js b/packages/backend/migration/1746330901644-visibleUserGeneratedContentsForNonLoggedInVisitors.js new file mode 100644 index 0000000000..115698a420 --- /dev/null +++ b/packages/backend/migration/1746330901644-visibleUserGeneratedContentsForNonLoggedInVisitors.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class VisibleUserGeneratedContentsForNonLoggedInVisitors1746330901644 { + name = 'VisibleUserGeneratedContentsForNonLoggedInVisitors1746330901644' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "ugcVisibilityForVisitor" character varying(128) NOT NULL DEFAULT 'local'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "ugcVisibilityForVisitor"`); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index 46f3b2e3c0..95d19c8075 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -659,6 +659,12 @@ export class MiMeta { }) public federationHosts: string[]; + @Column('varchar', { + length: 128, + default: 'local', + }) + public ugcVisibilityForVisitor: 'all' | 'local' | 'none'; + @Column('varchar', { length: 64, nullable: true, diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 4a106e7175..cb48a1bc95 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -546,6 +546,11 @@ export const meta = { }, }, }, + ugcVisibilityForVisitor: { + type: 'string', + enum: ['all', 'local', 'none'], + optional: false, nullable: false, + }, }, }, } as const; @@ -691,6 +696,7 @@ export default class extends Endpoint { // eslint- federation: instance.federation, federationHosts: instance.federationHosts, deliverSuspendedSoftware: instance.deliverSuspendedSoftware, + ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor, }; }); } 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 31eeaa5e38..fc6b890b69 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -196,6 +196,10 @@ export const paramDef = { required: ['software', 'versionRange'], }, }, + ugcVisibilityForVisitor: { + type: 'string', + enum: ['all', 'local', 'none'], + }, }, required: [], } as const; @@ -690,6 +694,10 @@ export default class extends Endpoint { // eslint- set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase()); } + if (ps.ugcVisibilityForVisitor !== undefined) { + set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor; + } + const before = await this.metaService.fetch(true); await this.metaService.update(set); diff --git a/packages/backend/src/server/api/endpoints/notes/show.ts b/packages/backend/src/server/api/endpoints/notes/show.ts index 11839bce36..b93c73b0c5 100644 --- a/packages/backend/src/server/api/endpoints/notes/show.ts +++ b/packages/backend/src/server/api/endpoints/notes/show.ts @@ -3,10 +3,12 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { GetterService } from '@/server/api/GetterService.js'; +import { DI } from '@/di-symbols.js'; +import { MiMeta } from '@/models/Meta.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -46,6 +48,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private noteEntityService: NoteEntityService, private getterService: GetterService, ) { @@ -59,6 +64,14 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.signinRequired); } + if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) { + throw new ApiError(meta.errors.signinRequired); + } + + if (this.serverSettings.ugcVisibilityForVisitor === 'local' && note.userHost != null && me == null) { + throw new ApiError(meta.errors.signinRequired); + } + return await this.noteEntityService.pack(note, me, { detail: true, }); diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts index 062326e28d..431869d47f 100644 --- a/packages/backend/src/server/api/endpoints/users/show.ts +++ b/packages/backend/src/server/api/endpoints/users/show.ts @@ -5,7 +5,7 @@ import { In, IsNull } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsersRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -82,6 +82,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -92,6 +95,10 @@ export default class extends Endpoint { // eslint- private apiLoggerService: ApiLoggerService, ) { super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => { + if (this.serverSettings.ugcVisibilityForVisitor === 'none' && me == null) { + throw new ApiError(meta.errors.noSuchUser); + } + let user; const isModerator = await this.roleService.isModerator(me); @@ -123,6 +130,10 @@ export default class extends Endpoint { // eslint- } else { // Lookup user if (typeof ps.host === 'string' && typeof ps.username === 'string') { + if (this.serverSettings.ugcVisibilityForVisitor === 'local' && me == null) { + throw new ApiError(meta.errors.noSuchUser); + } + user = await this.remoteUserResolveService.resolveUser(ps.username, ps.host).catch(err => { this.apiLoggerService.logger.warn(`failed to resolve remote user: ${err}`); throw new ApiError(meta.errors.failedToResolveRemoteUser); @@ -139,6 +150,10 @@ export default class extends Endpoint { // eslint- throw new ApiError(meta.errors.noSuchUser); } + if (this.serverSettings.ugcVisibilityForVisitor === 'local' && user.host != null && me == null) { + throw new ApiError(meta.errors.noSuchUser); + } + if (user.host == null) { if (me == null && ip != null) { this.perUserPvChart.commitByVisitor(user, ip); diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 30a911088e..9a33d27d86 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -513,7 +513,12 @@ export class ClientServerService { vary(reply.raw, 'Accept'); - if (user != null) { + if ( + user != null && ( + this.meta.ugcVisibilityForVisitor === 'all' || + (this.meta.ugcVisibilityForVisitor === 'local' && user.host == null) + ) + ) { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); const me = profile.fields ? profile.fields @@ -577,7 +582,13 @@ export class ClientServerService { relations: ['user'], }); - if (note && !note.user!.requireSigninToViewContents) { + if ( + note && + !note.user!.requireSigninToViewContents && + (this.meta.ugcVisibilityForVisitor === 'all' || + (this.meta.ugcVisibilityForVisitor === 'local' && note.userHost == null) + ) + ) { const _note = await this.noteEntityService.pack(note); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); reply.header('Cache-Control', 'public, max-age=15'); diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 2157b4ca14..819f229c10 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -17,9 +17,20 @@ SPDX-License-Identifier: AGPL-3.0-only - + + + + + + + + + {{ i18n.ts.serverRules }} @@ -137,9 +148,11 @@ import { definePage } from '@/page.js'; import MkButton from '@/components/MkButton.vue'; import FormLink from '@/components/form/link.vue'; import MkFolder from '@/components/MkFolder.vue'; +import MkSelect from '@/components/MkSelect.vue'; const enableRegistration = ref(false); const emailRequiredForSignup = ref(false); +const ugcVisibilityForVisitor = ref('all'); const sensitiveWords = ref(''); const prohibitedWords = ref(''); const prohibitedWordsForNameOfUser = ref(''); @@ -153,6 +166,7 @@ async function init() { const meta = await misskeyApi('admin/meta'); enableRegistration.value = !meta.disableRegistration; emailRequiredForSignup.value = meta.emailRequiredForSignup; + ugcVisibilityForVisitor.value = meta.ugcVisibilityForVisitor; sensitiveWords.value = meta.sensitiveWords.join('\n'); prohibitedWords.value = meta.prohibitedWords.join('\n'); prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n'); @@ -189,6 +203,14 @@ function onChange_emailRequiredForSignup(value: boolean) { }); } +function onChange_ugcVisibilityForVisitor(value: string) { + os.apiWithDialog('admin/update-meta', { + ugcVisibilityForVisitor: value, + }).then(() => { + fetchInstance(true); + }); +} + function save_preservedUsernames() { os.apiWithDialog('admin/update-meta', { preservedUsernames: preservedUsernames.value.split('\n'), diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 6db66477f7..4a71dd5465 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -8780,6 +8780,8 @@ export type operations = { software: string; versionRange: string; }[]; + /** @enum {string} */ + ugcVisibilityForVisitor: 'all' | 'local' | 'none'; }; }; }; @@ -11450,6 +11452,8 @@ export type operations = { software: string; versionRange: string; }[]; + /** @enum {string} */ + ugcVisibilityForVisitor?: 'all' | 'local' | 'none'; }; }; };