From 82a7cdb204a20e02fbce013ba357faedd23f5a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=95=84=EB=A5=B4=ED=8E=98?= Date: Fri, 16 Feb 2024 08:21:04 +0900 Subject: [PATCH] feat: auto note removal --- locales/ja-JP.yml | 10 +++ locales/ko-KR.yml | 19 ++++-- packages/backend/src/core/SignupService.ts | 9 +++ .../src/core/entities/UserEntityService.ts | 2 + .../backend/src/models/json-schema/user.ts | 28 +++++++++ .../backend/src/server/api/EndpointsModule.ts | 4 ++ packages/backend/src/server/api/endpoints.ts | 2 + .../endpoints/i/update-removal-condition.ts | 61 +++++++++++++++++++ .../src/server/api/endpoints/i/update.ts | 10 +-- .../frontend/src/pages/settings/privacy.vue | 35 +++++++++++ 10 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/i/update-removal-condition.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 39c7f3d20..a1b55acc4 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1299,6 +1299,7 @@ mindControlDescription: "必要に応じて、Misskeyの疲労感を少なくす hideCounters: "すべてのカウンターを隠す" hideCountersDescription: "ユーザーページのノート、フォロー、フォロワー数、およびこれらの統計をすべて非表示にします。" youAreOnVacation: "現在、休暇モードを使用しています。 このメッセージを閉じるにはここをクリックしてください。" +autoRemoval: "노트 자동 삭제" _bubbleGame: howToPlay: "遊び方" @@ -1356,6 +1357,15 @@ _announcement: silence: "非通知" silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。" +_autoRemoval: + use: "노트 자동 삭제를 사용하기" + deleteAfter: "이 기간이 지난 후에 자동으로 삭제" + deleteAfterDescription: "여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다." + noPiningNotes: "고정된 노트를 제외하기" + noPiningNotesDescription: "프로필에 고정된 노트를 제외하고 삭제합니다." + noSpecifiedNotes: "다이렉트 노트를 제외하기" + noSpecifiedNotesDescription: "다이렉트 노트를 제외하고 삭제합니다." + _initialTutorial: launchTutorial: "チュートリアルを見る" title: "チュートリアル" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 5683bb34d..52833a676 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1284,6 +1284,12 @@ mindControlDescription: "필요한 경우 Misskey를 이용하면서 피로감 hideCounters: "모든 카운터를 가리기" hideCountersDescription: "유저 페이지의 노트, 팔로잉, 팔로워 수 및 이러한 통계를 모두 숨깁니다." youAreOnVacation: "현재 휴가 모드를 사용 중입니다. 이 메시지를 닫으려면 여기를 클릭하세요." +autoRemoval: "노트 자동 삭제" +abuseReportCategory: "신고 유형" +selectCategory: "카테고리 선택" +reportComplete: "신고 완료" +blockThisUser: "이 유저 차단하기" +muteThisUser: "이 유저 뮤트하기" _bubbleGame: howToPlay: "설명" hold: "홀드" @@ -1299,11 +1305,6 @@ _bubbleGame: section1: "위치를 조정하여 상자에 물건을 떨어뜨립니다." section2: "같은 종류의 물건이 붙으면 다른 물건으로 바뀌면서 점수를 얻게 됩니다." section3: "상자에서 물건이 넘치면 게임 오버입니다. 상자에서 물건이 넘치지 않도록 하면서 물건을 융합하여 높은 점수를 획득하세요!" -abuseReportCategory: "신고 유형" -selectCategory: "카테고리 선택" -reportComplete: "신고 완료" -blockThisUser: "이 유저 차단하기" -muteThisUser: "이 유저 뮤트하기" _abuseReportCategory: nsfw: "NSFW 가이드라인에 반하는 민감한 콘텐츠" nsfw_description: "NSFW(열람주의 / 민감한 콘텐츠) 플래그가 없는 미디어 게시물, CW(내용을 숨김) 없이 노출된 텍스트 게시물, 실제 성기가 포함된 미디어 등" @@ -1342,6 +1343,14 @@ _announcement: dialogAnnouncementUxWarn: "다이얼로그 형태의 알림이 동시에 2개 이상 존재하는 경우, 유저 경험에 악영향을 끼칠 수 있으므로 신중히 결정하십시오." silence: "조용히 알림" silenceDescription: "활성화하면 공지사항에 대한 알림이 가지 않게 되며, 확인 버튼을 누를 필요가 없게 됩니다." +_autoRemoval: + use: "노트 자동 삭제를 사용하기" + deleteAfter: "이 기간이 지난 후에 자동으로 삭제" + deleteAfterDescription: 여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다. + noPiningNotes: "고정된 노트를 제외하기" + noPiningNotesDescription: "프로필에 고정된 노트를 제외하고 삭제합니다." + noSpecifiedNotes: "다이렉트 노트를 제외하기" + noSpecifiedNotesDescription: "다이렉트 노트를 제외하고 삭제합니다." _initialTutorial: launchTutorial: "튜토리얼 보기" title: "튜토리얼" diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 76c1dc322..1ad07c271 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import generateUserToken from '@/misc/generate-native-user-token.js'; import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; +import { MiAutoRemovalCondition } from '@/models/AutoRemovalCondition.js'; import { MiUser } from '@/models/User.js'; import { MiUserProfile } from '@/models/UserProfile.js'; import { MiUserKeypair } from '@/models/UserKeypair.js'; @@ -137,12 +138,20 @@ export class SignupService { if (exist) throw new Error(' the username is already used'); + const condition = await transactionalEntityManager.save(new MiAutoRemovalCondition({ + id: this.idService.gen(), + deleteAfter: 7, + noPiningNotes: false, + noSpecifiedNotes: false, + })); + account = await transactionalEntityManager.save(new MiUser({ id: this.idService.gen(), username: username, usernameLower: username.toLowerCase(), host: this.utilityService.toPunyNullable(host), token: secret, + autoRemovalConditionId: condition.id, isRoot: isTheFirstUser, })); diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 5cd4f85b8..b45370e99 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -612,6 +612,8 @@ export class UserEntityService implements OnModuleInit { notificationRecieveConfig: profile?.notificationRecieveConfig, emailNotificationTypes: profile?.emailNotificationTypes, achievements: profile?.achievements, + autoRemoval: user.autoRemoval, + autoRemovalCondition: user.autoRemovalCondition, loggedInDays: profile?.loggedInDates.length, policies: policies, } : {}), diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index e6bb86390..0fb9a97f2 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -641,6 +641,34 @@ export const packedMeDetailedOnlySchema = { }, }, }, + autoRemoval: { + type: 'boolean', + nullable: false, optional: false, + }, + autoRemovalCondition: { + type: 'object', + nullable: false, optional: false, + properties: { + id: { + type: 'string', + nullable: false, optional: false, + format: 'id', + example: 'xxxxxxxxxx', + }, + deleteAfter: { + type: 'number', + nullable: false, optional: false, + }, + noPiningNotes: { + type: 'boolean', + nullable: false, optional: false, + }, + noSpecifiedNotes: { + type: 'boolean', + nullable: false, optional: false, + }, + }, + }, loggedInDays: { type: 'number', nullable: false, optional: false, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index ce80f06c0..7b9d59f99 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -259,6 +259,7 @@ import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; import * as ep___i_unpin from './endpoints/i/unpin.js'; import * as ep___i_updateEmail from './endpoints/i/update-email.js'; import * as ep___i_update from './endpoints/i/update.js'; +import * as ep___i_updateRemovalCondition from './endpoints/i/update-removal-condition.js'; import * as ep___i_move from './endpoints/i/move.js'; import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; @@ -652,6 +653,7 @@ const $i_signinHistory: Provider = { provide: 'ep:i/signin-history', useClass: e const $i_unpin: Provider = { provide: 'ep:i/unpin', useClass: ep___i_unpin.default }; const $i_updateEmail: Provider = { provide: 'ep:i/update-email', useClass: ep___i_updateEmail.default }; const $i_update: Provider = { provide: 'ep:i/update', useClass: ep___i_update.default }; +const $i_updateRemovalCondition: Provider = { provide: 'ep:i/update-removal-condition', useClass: ep___i_updateRemovalCondition.default }; const $i_move: Provider = { provide: 'ep:i/move', useClass: ep___i_move.default }; const $i_webhooks_create: Provider = { provide: 'ep:i/webhooks/create', useClass: ep___i_webhooks_create.default }; const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep___i_webhooks_list.default }; @@ -1049,6 +1051,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $i_unpin, $i_updateEmail, $i_update, + $i_updateRemovalCondition, $i_move, $i_webhooks_create, $i_webhooks_list, @@ -1440,6 +1443,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $i_unpin, $i_updateEmail, $i_update, + $i_updateRemovalCondition, $i_move, $i_webhooks_create, $i_webhooks_list, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index bac986e9c..34a5cbc3a 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -259,6 +259,7 @@ import * as ep___i_signinHistory from './endpoints/i/signin-history.js'; import * as ep___i_unpin from './endpoints/i/unpin.js'; import * as ep___i_updateEmail from './endpoints/i/update-email.js'; import * as ep___i_update from './endpoints/i/update.js'; +import * as ep___i_update_removal_condition from './endpoints/i/update-removal-condition.js'; import * as ep___i_move from './endpoints/i/move.js'; import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js'; import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js'; @@ -650,6 +651,7 @@ const eps = [ ['i/unpin', ep___i_unpin], ['i/update-email', ep___i_updateEmail], ['i/update', ep___i_update], + ['i/update-removal-condition', ep___i_update_removal_condition], ['i/move', ep___i_move], ['i/webhooks/create', ep___i_webhooks_create], ['i/webhooks/list', ep___i_webhooks_list], diff --git a/packages/backend/src/server/api/endpoints/i/update-removal-condition.ts b/packages/backend/src/server/api/endpoints/i/update-removal-condition.ts new file mode 100644 index 000000000..f8d0b097d --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/update-removal-condition.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import type { AutoRemovalConditionRepository, UsersRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + + kind: 'write:account', + + errors: { + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'fcd2eef9-a9b2-4c4f-8624-038099e90aa5', + }, + }, + + res: { + type: 'object', + ref: 'MeDetailed', + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + active: { type: 'boolean' }, + deleteAfter: { type: 'number' }, + noPiningNotes: { type: 'boolean' }, + noSpecifiedNotes: { type: 'boolean' }, + }, +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.autoRemovalConditionRepository) + private autoRemovalConditionRepository: AutoRemovalConditionRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + ) { + super(meta, paramDef, async (ps, me) => { + await this.usersRepository.update(me.id, { autoRemoval: false }); + const updated = { + deleteAfter: ps.deleteAfter, + noPiningNotes: ps.noPiningNotes, + noSpecifiedNotes: ps.noSpecifiedNotes, + }; + await this.autoRemovalConditionRepository.update(me.autoRemovalConditionId, updated); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index f04fcf46c..a38e31b92 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; -import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository, AutoRemovalConditionRepository } from '@/models/_.js'; +import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; @@ -280,9 +280,6 @@ export default class extends Endpoint { // eslint- @Inject(DI.pagesRepository) private pagesRepository: PagesRepository, - @Inject(DI.autoRemovalConditionRepository) - private autoRemovalConditionRepository: AutoRemovalConditionRepository, - private idService: IdService, private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, @@ -350,7 +347,6 @@ export default class extends Endpoint { // eslint- if (typeof ps.isVacation === 'boolean') updates.isVacation = ps.isVacation; if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; - if (typeof ps.autoRemoval === 'boolean') updates.autoRemoval = ps.autoRemoval; if (typeof ps.alwaysMarkNsfw === 'boolean') { if (policy.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; @@ -527,10 +523,6 @@ export default class extends Endpoint { // eslint- this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id }); } - if (ps.autoRemovalCondition !== undefined) { - await this.autoRemovalConditionRepository.update(user.autoRemovalConditionId, ps.autoRemovalCondition); - } - await this.userProfilesRepository.update(user.id, { ...profileUpdates, verifiedLinks: [], diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue index d50dcff3f..9cc5c1423 100644 --- a/packages/frontend/src/pages/settings/privacy.vue +++ b/packages/frontend/src/pages/settings/privacy.vue @@ -67,6 +67,31 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.keepCw }} + + + + +
+ {{ i18n.ts._autoRemoval.use }} + + +
+
@@ -93,6 +118,10 @@ const hideOnlineStatus = ref($i.hideOnlineStatus); const publicReactions = ref($i.publicReactions); const followingVisibility = ref($i.followingVisibility); const followersVisibility = ref($i.followersVisibility); +const autoRemoval = ref($i.autoRemoval); +const deleteAfter = ref($i.autoRemovalCondition.deleteAfter); +const noPiningNotes = ref($i.autoRemovalCondition.noPiningNotes); +const noSpecifiedNotes = ref($i.autoRemovalCondition.noSpecifiedNotes); const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); @@ -111,6 +140,12 @@ function save() { followingVisibility: followingVisibility.value, followersVisibility: followersVisibility.value, }); + misskeyApi('i/update-removal-condition', { + active: autoRemoval.value, + deleteAfter: deleteAfter.value, + noPiningNotes: noPiningNotes.value, + noSpecifiedNotes: noSpecifiedNotes.value, + }); } const headerActions = computed(() => []);