diff --git a/CHANGELOG.md b/CHANGELOG.md index 421237c32def..d56abb29b44c 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 e00254030746..8350297a79a0 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 f3f7e5c77f0e..93ed879a0832 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 000000000000..5fe4886b0433 --- /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 254d961040f0..c826a28963ae 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 8235d7ba303d..5617a29bab22 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 447f7ef3db60..94cb0785cbb2 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 c7915ed94fe9..0e2934301ba9 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 8a860335faf3..7496315f0970 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 62016936a263..96cc6b028ec0 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) { + if (packedNote.user.requireSigninToViewContents && meId == null) { + hide = true; + } + + 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; - } else if (meId === packedNote.userId) { - hide = false; - } else { - // 指定されているかどうか - const specified = packedNote.visibleUserIds!.some(id => meId === id); + } + } - if (specified) { - hide = false; - } else { + // 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 747ffc780f5d..d3c087a15376 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 6fcff77854d2..96de30c4c2c9 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 817f8e929243..38631f907dd5 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 6680c96f3f2d..2183beac7c51 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 8bd02000e679..eeadd4993698 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 e277dfad71dd..da3d36b31add 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