feat: 過去のノートを非公開化/フォロワーのみ表示可能にできる機能 (#14814)

* wip

* Update CHANGELOG.md

* wip

* wip

* wip

* Update privacy.vue

* wip
This commit is contained in:
syuilo 2024-10-22 17:08:53 +09:00 committed by GitHub
parent 70b2a8f72e
commit 952fec5665
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 282 additions and 43 deletions

View file

@ -2,6 +2,7 @@
### General
- Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように

42
locales/index.d.ts vendored
View file

@ -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": {
/**

View file

@ -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: "転送"

View file

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

View file

@ -84,6 +84,8 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
isHibernated: false,
isDeleted: false,
requireSigninToViewContents: false,
makeNotesFollowersOnlyBefore: null,
makeNotesHiddenBefore: null,
emojis: [],
score: 0,
host: null,

View file

@ -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,

View file

@ -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#',

View file

@ -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;

View file

@ -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;

View file

@ -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<void> {
// 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 みたいなのを提供しても良さそう
}
}

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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<typeof meta, typeof paramDef> { // 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;

View file

@ -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;

View file

@ -45,17 +45,89 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
<FormSection>
<template #label>{{ i18n.ts.lockdown }}</template>
<template #label>{{ i18n.ts.lockdown }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
<div class="_gaps_m">
<MkSwitch v-model="requireSigninToViewContents" @update:modelValue="save()">
{{ i18n.ts._accountSettings.requireSigninToViewContents }}<span class="_beta">{{ i18n.ts.beta }}</span>
{{ i18n.ts._accountSettings.requireSigninToViewContents }}
<template #caption>
<div>{{ i18n.ts._accountSettings.requireSigninToViewContentsDescription1 }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription2 }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.requireSigninToViewContentsDescription3 }}</div>
</template>
</MkSwitch>
<FormSlot>
<template #label>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBefore }}</template>
<div class="_gaps_s">
<MkSelect :modelValue="makeNotesFollowersOnlyBefore_type" @update:modelValue="makeNotesFollowersOnlyBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
<option :value="null">{{ i18n.ts.none }}</option>
<option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
<option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
</MkSelect>
<MkSelect v-if="makeNotesFollowersOnlyBefore_type === 'relative'" v-model="makeNotesFollowersOnlyBefore">
<option :value="-3600">{{ i18n.ts.oneHour }}</option>
<option :value="-86400">{{ i18n.ts.oneDay }}</option>
<option :value="-259200">{{ i18n.ts.threeDays }}</option>
<option :value="-604800">{{ i18n.ts.oneWeek }}</option>
<option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
<option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
<option :value="-31104000">{{ i18n.ts.oneYear }}</option>
</MkSelect>
<MkInput
v-if="makeNotesFollowersOnlyBefore_type === 'absolute'"
:modelValue="formatDateTimeString(new Date(makeNotesFollowersOnlyBefore * 1000), 'yyyy-MM-dd')"
type="date"
:manualSave="true"
@update:modelValue="makeNotesFollowersOnlyBefore = Math.floor(new Date($event).getTime() / 1000)"
>
</MkInput>
</div>
<template #caption>
<div>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
</template>
</FormSlot>
<FormSlot>
<template #label>{{ i18n.ts._accountSettings.makeNotesHiddenBefore }}</template>
<div class="_gaps_s">
<MkSelect :modelValue="makeNotesHiddenBefore_type" @update:modelValue="makeNotesHiddenBefore = $event === 'relative' ? -604800 : $event === 'absolute' ? Math.floor(Date.now() / 1000) : null">
<option :value="null">{{ i18n.ts.none }}</option>
<option value="relative">{{ i18n.ts._accountSettings.notesHavePassedSpecifiedPeriod }}</option>
<option value="absolute">{{ i18n.ts._accountSettings.notesOlderThanSpecifiedDateAndTime }}</option>
</MkSelect>
<MkSelect v-if="makeNotesHiddenBefore_type === 'relative'" v-model="makeNotesHiddenBefore">
<option :value="-3600">{{ i18n.ts.oneHour }}</option>
<option :value="-86400">{{ i18n.ts.oneDay }}</option>
<option :value="-259200">{{ i18n.ts.threeDays }}</option>
<option :value="-604800">{{ i18n.ts.oneWeek }}</option>
<option :value="-2592000">{{ i18n.ts.oneMonth }}</option>
<option :value="-7776000">{{ i18n.ts.threeMonths }}</option>
<option :value="-31104000">{{ i18n.ts.oneYear }}</option>
</MkSelect>
<MkInput
v-if="makeNotesHiddenBefore_type === 'absolute'"
:modelValue="formatDateTimeString(new Date(makeNotesHiddenBefore * 1000), 'yyyy-MM-dd')"
type="date"
:manualSave="true"
@update:modelValue="makeNotesHiddenBefore = Math.floor(new Date($event).getTime() / 1000)"
>
</MkInput>
</div>
<template #caption>
<div>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
</template>
</FormSlot>
</div>
</FormSection>
@ -87,7 +159,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { ref, computed, watch } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue';
@ -97,6 +169,9 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { signinRequired } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import FormSlot from '@/components/form/slot.vue';
import { formatDateTimeString } from '@/scripts/format-time-string.js';
import MkInput from '@/components/MkInput.vue';
const $i = signinRequired();
@ -106,6 +181,8 @@ const noCrawle = ref($i.noCrawle);
const preventAiLearning = ref($i.preventAiLearning);
const isExplorable = ref($i.isExplorable);
const requireSigninToViewContents = ref($i.requireSigninToViewContents ?? false);
const makeNotesFollowersOnlyBefore = ref($i.makeNotesFollowersOnlyBefore ?? null);
const makeNotesHiddenBefore = ref($i.makeNotesHiddenBefore ?? null);
const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions);
const followingVisibility = ref($i.followingVisibility);
@ -116,6 +193,30 @@ const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNote
const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
const makeNotesFollowersOnlyBefore_type = computed(() => {
if (makeNotesFollowersOnlyBefore.value == null) {
return null;
} else if (makeNotesFollowersOnlyBefore.value >= 0) {
return 'absolute';
} else {
return 'relative';
}
});
const makeNotesHiddenBefore_type = computed(() => {
if (makeNotesHiddenBefore.value == null) {
return null;
} else if (makeNotesHiddenBefore.value >= 0) {
return 'absolute';
} else {
return 'relative';
}
});
watch([makeNotesFollowersOnlyBefore, makeNotesHiddenBefore], () => {
save();
});
function save() {
misskeyApi('i/update', {
isLocked: !!isLocked.value,
@ -124,6 +225,8 @@ function save() {
preventAiLearning: !!preventAiLearning.value,
isExplorable: !!isExplorable.value,
requireSigninToViewContents: !!requireSigninToViewContents.value,
makeNotesFollowersOnlyBefore: makeNotesFollowersOnlyBefore.value,
makeNotesHiddenBefore: makeNotesHiddenBefore.value,
hideOnlineStatus: !!hideOnlineStatus.value,
publicReactions: !!publicReactions.value,
followingVisibility: followingVisibility.value,

View file

@ -3737,6 +3737,8 @@ export type components = {
isBot?: boolean;
isCat?: boolean;
requireSigninToViewContents?: boolean;
makeNotesFollowersOnlyBefore?: number | null;
makeNotesHiddenBefore?: number | null;
instance?: {
name: string | null;
softwareName: string | null;
@ -19846,6 +19848,8 @@ export type operations = {
noCrawle?: boolean;
preventAiLearning?: boolean;
requireSigninToViewContents?: boolean;
makeNotesFollowersOnlyBefore?: number | null;
makeNotesHiddenBefore?: number | null;
isBot?: boolean;
isCat?: boolean;
injectFeaturedNote?: boolean;