diff --git a/CHANGELOG.md b/CHANGELOG.md index 421237c32d..d56abb29b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### General - Feat: コンテンツの表示にログインを必須にできるように +- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように ### Client - Enhance: Bull DashboardでRelationship Queueの状態も確認できるように diff --git a/locales/index.d.ts b/locales/index.d.ts index e002540307..8350297a79 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -3806,6 +3806,18 @@ export interface Locale extends ILocale { * 1ヶ月 */ "oneMonth": string; + /** + * 3ヶ月 + */ + "threeMonths": string; + /** + * 1年 + */ + "oneYear": string; + /** + * 3日 + */ + "threeDays": string; /** * 反映されるまで時間がかかる場合があります。 */ @@ -5204,7 +5216,7 @@ export interface Locale extends ILocale { */ "requireSigninToViewContents": string; /** - * あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーから情報を収集されるのを防ぐ効果が期待できます。 + * あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーに情報が収集されるのを防ぐ効果が期待できます。 */ "requireSigninToViewContentsDescription1": string; /** @@ -5215,6 +5227,34 @@ export interface Locale extends ILocale { * リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。 */ "requireSigninToViewContentsDescription3": string; + /** + * 過去のノートをフォロワーのみ表示可能にする + */ + "makeNotesFollowersOnlyBefore": string; + /** + * この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートがフォロワーのみ表示可能になります。無効に戻すと、ノートの公開状態も元に戻ります。 + */ + "makeNotesFollowersOnlyBeforeDescription": string; + /** + * 過去のノートを非公開化する + */ + "makeNotesHiddenBefore": string; + /** + * この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。 + */ + "makeNotesHiddenBeforeDescription": string; + /** + * リモートサーバーに連合されたノートには効果が及ばない場合があります。 + */ + "mayNotEffectForFederatedNotes": string; + /** + * 指定した時間を経過しているノート + */ + "notesHavePassedSpecifiedPeriod": string; + /** + * 指定した日時より前のノート + */ + "notesOlderThanSpecifiedDateAndTime": string; }; "_abuseUserReport": { /** diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f3f7e5c77f..93ed879a08 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -947,6 +947,9 @@ oneHour: "1時間" oneDay: "1日" oneWeek: "1週間" oneMonth: "1ヶ月" +threeMonths: "3ヶ月" +oneYear: "1年" +threeDays: "3日" reflectMayTakeTime: "反映されるまで時間がかかる場合があります。" failedToFetchAccountInformation: "アカウント情報の取得に失敗しました" rateLimitExceeded: "レート制限を超えました" @@ -1298,9 +1301,16 @@ lockdown: "ロックダウン" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" - requireSigninToViewContentsDescription1: "あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーから情報を収集されるのを防ぐ効果が期待できます。" + requireSigninToViewContentsDescription1: "あなたが作成した全てのノートなどのコンテンツを表示するのにログインを必須にします。クローラーに情報が収集されるのを防ぐ効果が期待できます。" requireSigninToViewContentsDescription2: "URLプレビュー(OGP)、Webページへの埋め込み、ノートの引用に対応していないサーバーからの表示も不可になります。" requireSigninToViewContentsDescription3: "リモートサーバーに連合されたコンテンツでは、これらの制限が適用されない場合があります。" + makeNotesFollowersOnlyBefore: "過去のノートをフォロワーのみ表示可能にする" + makeNotesFollowersOnlyBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートがフォロワーのみ表示可能になります。無効に戻すと、ノートの公開状態も元に戻ります。" + makeNotesHiddenBefore: "過去のノートを非公開化する" + makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。" + mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。" + notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート" + notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート" _abuseUserReport: forward: "転送" diff --git a/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js b/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js new file mode 100644 index 0000000000..5fe4886b04 --- /dev/null +++ b/packages/backend/migration/1729486255072-makeNotesHiddenBefore.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class MakeNotesHiddenBefore1729486255072 { + name = 'MakeNotesHiddenBefore1729486255072' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesFollowersOnlyBefore" integer`); + await queryRunner.query(`ALTER TABLE "user" ADD "makeNotesHiddenBefore" integer`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesHiddenBefore"`); + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "makeNotesFollowersOnlyBefore"`); + } +} diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts index 254d961040..c826a28963 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -84,6 +84,8 @@ function generateDummyUser(override?: Partial): MiUser { isHibernated: false, isDeleted: false, requireSigninToViewContents: false, + makeNotesFollowersOnlyBefore: null, + makeNotesHiddenBefore: null, emojis: [], score: 0, host: null, diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 8235d7ba30..5617a29bab 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -496,6 +496,8 @@ export class ApRendererService { _misskey_summary: profile.description, _misskey_followedMessage: profile.followedMessage, _misskey_requireSigninToViewContents: user.requireSigninToViewContents, + _misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore, + _misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore, icon: avatar ? this.renderImage(avatar) : null, image: banner ? this.renderImage(banner) : null, tag, diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 447f7ef3db..94cb0785cb 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -556,6 +556,8 @@ const extension_context_definition = { '_misskey_summary': 'misskey:_misskey_summary', '_misskey_followedMessage': 'misskey:_misskey_followedMessage', '_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents', + '_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore', + '_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore', 'isCat': 'misskey:isCat', // vcard vcard: 'http://www.w3.org/2006/vcard/ns#', diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index c7915ed94f..0e2934301b 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -357,6 +357,8 @@ export class ApPersonService implements OnModuleInit { isBot, isCat: (person as any).isCat === true, requireSigninToViewContents: (person as any).requireSigninToViewContents === true, + makeNotesFollowersOnlyBefore: (person as any).makeNotesFollowersOnlyBefore ?? null, + makeNotesHiddenBefore: (person as any).makeNotesHiddenBefore ?? null, emojis, })) as MiRemoteUser; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 8a860335fa..7496315f09 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -15,6 +15,8 @@ export interface IObject { _misskey_summary?: string; _misskey_followedMessage?: string | null; _misskey_requireSigninToViewContents?: boolean; + _misskey_makeNotesFollowersOnlyBefore?: number | null; + _misskey_makeNotesHiddenBefore?: number | null; published?: string; cc?: ApObject; to?: ApObject; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 62016936a2..96cc6b028e 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -102,57 +102,83 @@ export class NoteEntityService implements OnModuleInit { } @bindThis - private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) { + private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise { + // FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある) + if (packedNote.visibility === 'public' || packedNote.visibility === 'home') { + const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore; + if ((followersOnlyBefore != null) + && ( + (followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000))) + || (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000)) + ) + ) { + packedNote.visibility = 'followers'; + } + } + + if (meId === packedNote.userId) return; + // TODO: isVisibleForMe を使うようにしても良さそう(型違うけど) let hide = false; - // visibility が specified かつ自分が指定されていなかったら非表示 - if (packedNote.visibility === 'specified') { - if (meId == null) { - hide = true; - } else if (meId === packedNote.userId) { - hide = false; - } else { - // 指定されているかどうか - const specified = packedNote.visibleUserIds!.some(id => meId === id); + if (packedNote.user.requireSigninToViewContents && meId == null) { + hide = true; + } - if (specified) { - hide = false; - } else { + if (!hide) { + const hiddenBefore = packedNote.user.makeNotesHiddenBefore; + if ((hiddenBefore != null) + && ( + (hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000))) + || (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000)) + ) + ) { + hide = true; + } + } + + // visibility が specified かつ自分が指定されていなかったら非表示 + if (!hide) { + if (packedNote.visibility === 'specified') { + if (meId == null) { hide = true; + } else { + // 指定されているかどうか + const specified = packedNote.visibleUserIds!.some(id => meId === id); + + if (!specified) { + hide = true; + } } } } // visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示 - if (packedNote.visibility === 'followers') { - if (meId == null) { - hide = true; - } else if (meId === packedNote.userId) { - hide = false; - } else if (packedNote.reply && (meId === packedNote.reply.userId)) { - // 自分の投稿に対するリプライ - hide = false; - } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { - // 自分へのメンション - hide = false; - } else { - // フォロワーかどうか - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: packedNote.userId, - followerId: meId, - }, - }); + if (!hide) { + if (packedNote.visibility === 'followers') { + if (meId == null) { + hide = true; + } else if (packedNote.reply && (meId === packedNote.reply.userId)) { + // 自分の投稿に対するリプライ + hide = false; + } else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) { + // 自分へのメンション + hide = false; + } else { + // フォロワーかどうか + // TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: packedNote.userId, + followerId: meId, + }, + }); - hide = !isFollowing; + hide = !isFollowing; + } } } - if (packedNote.user.requireSigninToViewContents && meId == null) { - hide = true; - } - if (hide) { packedNote.visibleUserIds = undefined; packedNote.fileIds = []; @@ -161,6 +187,7 @@ export class NoteEntityService implements OnModuleInit { packedNote.poll = undefined; packedNote.cw = null; packedNote.isHidden = true; + // TODO: hiddenReason みたいなのを提供しても良さそう } } diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 747ffc780f..d3c087a153 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -491,6 +491,8 @@ export class UserEntityService implements OnModuleInit { isBot: user.isBot, isCat: user.isCat, requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true, + makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined, + makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined, instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? { name: instance.name, softwareName: instance.softwareName, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 6fcff77854..96de30c4c2 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -207,6 +207,18 @@ export class MiUser { }) public requireSigninToViewContents: boolean; + // in sec, マイナスで相対時間 + @Column('integer', { + nullable: true, + }) + public makeNotesFollowersOnlyBefore: number | null; + + // in sec, マイナスで相対時間 + @Column('integer', { + nullable: true, + }) + public makeNotesHiddenBefore: number | null; + // アカウントが削除されたかどうかのフラグだが、完全に削除される際は物理削除なので実質削除されるまでの「削除が進行しているかどうか」のフラグ @Column('boolean', { default: false, diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 817f8e9292..38631f907d 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -119,6 +119,14 @@ export const packedUserLiteSchema = { type: 'boolean', nullable: false, optional: true, }, + makeNotesFollowersOnlyBefore: { + type: 'number', + nullable: true, optional: true, + }, + makeNotesHiddenBefore: { + type: 'number', + nullable: true, optional: true, + }, instance: { type: 'object', nullable: false, optional: true, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 6680c96f3f..2183beac7c 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -180,6 +180,8 @@ export const paramDef = { noCrawle: { type: 'boolean' }, preventAiLearning: { type: 'boolean' }, requireSigninToViewContents: { type: 'boolean' }, + makeNotesFollowersOnlyBefore: { type: 'integer', nullable: true }, + makeNotesHiddenBefore: { type: 'integer', nullable: true }, isBot: { type: 'boolean' }, isCat: { type: 'boolean' }, injectFeaturedNote: { type: 'boolean' }, @@ -336,6 +338,8 @@ export default class extends Endpoint { // eslint- if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning; if (typeof ps.requireSigninToViewContents === 'boolean') updates.requireSigninToViewContents = ps.requireSigninToViewContents; + if ((typeof ps.makeNotesFollowersOnlyBefore === 'number') || (ps.makeNotesFollowersOnlyBefore === null)) updates.makeNotesFollowersOnlyBefore = ps.makeNotesFollowersOnlyBefore; + if ((typeof ps.makeNotesHiddenBefore === 'number') || (ps.makeNotesHiddenBefore === null)) updates.makeNotesHiddenBefore = ps.makeNotesHiddenBefore; if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue index 8bd02000e6..eeadd49936 100644 --- a/packages/frontend/src/components/MkSelect.vue +++ b/packages/frontend/src/components/MkSelect.vue @@ -46,7 +46,7 @@ import type { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; const props = defineProps<{ - modelValue: string | null; + modelValue: string | number | null; required?: boolean; readonly?: boolean; disabled?: boolean; diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index e277dfad71..da3d36b31a 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -45,17 +45,89 @@ SPDX-License-Identifier: AGPL-3.0-only - +
- {{ i18n.ts._accountSettings.requireSigninToViewContents }}{{ i18n.ts.beta }} + {{ i18n.ts._accountSettings.requireSigninToViewContents }} + + + + +
+ + + + + + + + + + + + + + + + + + +
+ + +
+ + + + +
+ + + + + + + + + + + + + + + + + + +
+ + +
@@ -87,7 +159,7 @@ SPDX-License-Identifier: AGPL-3.0-only