refactor(account): account delete, truncate, auto note removal

This commit is contained in:
オスカー、 2024-09-07 22:32:45 +09:00
parent 4e99008e8d
commit d84bff37ec
Signed by: SWREI
GPG Key ID: 139D6573F92DA9F7
17 changed files with 387 additions and 260 deletions

View File

@ -1273,6 +1273,8 @@ temporarilySeeThis: "Nevermind, just show me this"
sensitiveDoubleClickRequired: "Require double-click to open sensitive media"
mutualLink: "Mutual Link"
saveThisFile: "Save this file to Drive"
autoRemoval: "Automatic note deletion"
autoRemovalDescription: "You can delete your note when it exceeds period you set."
_bubbleGame:
howToPlay: "How to play"
hold: "Hold"
@ -1331,6 +1333,14 @@ _announcement:
dialogAnnouncementUxWarn: "Having two or more dialog-style notifications simultaneously can significantly impact the user experience, so please use them carefully."
silence: "No notification"
silenceDescription: "Turning this on will skip the notification of this announcement and the user won't need to read it."
_autoRemoval:
use: "Use automatic note deletion"
deleteAfter: "Period threshold"
deleteAfterDescription: "When your notes exceeds this period, notes will be deleted."
noPiningNotes: "Exclude pinned notes"
noPiningNotesDescription: "Delete notes except that are not pinned in profile."
noSpecifiedNotes: "Exclude direct notes"
noSpecifiedNotesDescription: "Delete notes without direct visibility."
_initialAccountSetting:
accountCreated: "Your account was successfully created!"
letsStartAccountSetup: "For starters, let's set up your profile."
@ -1742,29 +1752,31 @@ _role:
canCreateContent: "Can create contents"
canUpdateContent: "Can edit contents"
canDeleteContent: "Can delete contents"
canUseAutoNoteRemoval: "Can use automatic note deletion"
canUseAccountRemoval: "Can delete account"
canUseAccountTruncate: "Can truncate account"
canPurgeAccount: "Can delete account completely"
canUpdateAvatar: "Can change avatar"
canUpdateBanner: "Can change banner"
mentionMax: "Maximum number of mentions in a note"
mentionLimit: "Maximum number of mentions in a note"
canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown"
inviteExpirationTime: "Invite expiration interval"
canManageCustomEmojis: "Can manage custom emojis"
canManageAvatarDecorations: "Manage avatar decorations"
driveCapacity: "Drive capacity"
driveCapacityMb: "Drive capacity (MB)"
alwaysMarkNsfw: "Always mark files as NSFW"
skipNsfwDetection: "Skip NSFW detection by AI"
pinMax: "Maximum number of pinned notes"
antennaMax: "Maximum number of antennas"
antennaNotesMax: "Maximum number of notes stored in antennas"
wordMuteMax: "Maximum number of characters allowed in word mutes"
webhookMax: "Maximum number of Webhooks"
clipMax: "Maximum number of Clips"
noteEachClipsMax: "Maximum number of notes within a clip"
userListMax: "Maximum number of user lists"
userEachUserListsMax: "Maximum number of users within a user list"
pinLimit: "Maximum number of pinned notes"
antennaLimit: "Maximum number of antennas"
antennaNotesLimit: "Maximum number of notes stored in antennas"
wordMuteLimit: "Maximum number of words allowed in word mutes"
webhookLimit: "Maximum number of Webhooks"
clipLimit: "Maximum number of Clips"
noteEachClipsLimit: "Maximum number of notes within a clip"
userListLimit: "Maximum number of user lists"
userEachUserListsLimit: "Maximum number of users within a user list"
rateLimitFactor: "Rate limit"
descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. "
canHideAds: "Can hide ads"
@ -1822,17 +1834,19 @@ _accountDelete:
accountDelete: "Delete account"
mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded."
sendEmail: "Once account deletion has been completed, an email will be sent to the email address registered to this account."
requestAccountDelete: "Request account deletion"
requestAccountDelete: "Request to delete my account"
started: "Deletion has been started."
inProgress: "Deletion is currently in progress"
inProgress: "Your account is currently being deleted"
youCantUseThisTime: "You can't request account deletion for now."
youAreRootAndCantUseThisTime: "You can't request account deletion 'cause you are root."
_accountTruncate:
accountTruncate: "Truncate account"
purgeDriveFiles: "Also purge drive's files"
mayTakeTime: "As account deletion is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded."
purgeDriveFiles: "Also truncate drive's files"
mayTakeTime: "As account truncate task is a resource-heavy process, it may take some time to complete depending on how much content you have created and how many files you have uploaded."
requestAccountTruncate: "Request to truncate my account"
started: "Truncate task has been started."
inProgress: "Your account is currently being truncated"
youCantUseThisTime: "You can't request account truncate task for now."
_ad:
back: "Back"
reduceFrequencyOfThisAd: "Show this ad less"

72
locales/index.d.ts vendored
View File

@ -5224,9 +5224,21 @@ export interface Locale extends ILocale {
*/
"youAreOnVacation": string;
/**
*
*
*/
"autoRemoval": string;
/**
*
*/
"autoRemovalDescription": string;
/**
*
*/
"dangerZone": string;
/**
*
*/
"dangerZoneDescription": string;
"_bubbleGame": {
/**
*
@ -5432,33 +5444,37 @@ export interface Locale extends ILocale {
};
"_autoRemoval": {
/**
*
* 使
*/
"use": string;
/**
*
*
*/
"deleteAfter": string;
/**
* . .
*
*/
"deleteAfterDescription": string;
/**
*
*
*/
"noPiningNotes": string;
/**
* .
*
*/
"noPiningNotesDescription": string;
/**
*
*
*/
"noSpecifiedNotes": string;
/**
* .
*
*/
"noSpecifiedNotesDescription": string;
/**
*
*/
"youCantUseThisTime": string;
};
"_initialTutorial": {
/**
@ -6974,10 +6990,18 @@ export interface Locale extends ILocale {
*
*/
"canDeleteContent": string;
/**
*
*/
"canUseAutoNoteRemoval": string;
/**
*
*/
"canUseAccountRemoval": string;
/**
*
*/
"canUseAccountTruncate": string;
/**
*
*/
@ -6993,7 +7017,7 @@ export interface Locale extends ILocale {
/**
*
*/
"mentionMax": string;
"mentionLimit": string;
/**
*
*/
@ -7019,9 +7043,9 @@ export interface Locale extends ILocale {
*/
"canManageAvatarDecorations": string;
/**
*
* (MB)
*/
"driveCapacity": string;
"driveCapacityMb": string;
/**
* NSFWを常に付与
*/
@ -7033,39 +7057,39 @@ export interface Locale extends ILocale {
/**
*
*/
"pinMax": string;
"pinLimit": string;
/**
*
*/
"antennaMax": string;
"antennaLimit": string;
/**
*
*/
"antennaNotesMax": string;
"antennaNotesLimit": string;
/**
*
*/
"wordMuteMax": string;
"wordMuteLimit": string;
/**
* Webhookの作成可能数
*/
"webhookMax": string;
"webhookLimit": string;
/**
*
*/
"clipMax": string;
"clipLimit": string;
/**
*
*/
"noteEachClipsMax": string;
"noteEachClipsLimit": string;
/**
*
*/
"userListMax": string;
"userListLimit": string;
/**
*
*/
"userEachUserListsMax": string;
"userEachUserListsLimit": string;
/**
*
*/
@ -7299,6 +7323,10 @@ export interface Locale extends ILocale {
*
*/
"youCantUseThisTime": string;
/**
*
*/
"youAreRootAndCantUseThisTime": string;
};
"_accountTruncate": {
/**
@ -7325,6 +7353,10 @@ export interface Locale extends ILocale {
*
*/
"inProgress": string;
/**
*
*/
"youCantUseThisTime": string;
};
"_ad": {
/**

View File

@ -1301,7 +1301,10 @@ mindControlDescription: "必要に応じて、Misskeyの疲労感を少なくす
hideCounters: "すべてのカウンターを隠す"
hideCountersDescription: "ユーザーページのノート、フォロー、フォロワー数、およびこれらの統計をすべて非表示にします。"
youAreOnVacation: "現在、休暇モードを使用しています。 このメッセージを閉じるにはここをクリックしてください。"
autoRemoval: "노트 자동 삭제"
autoRemoval: "ノート自動削除"
autoRemovalDescription: "ノート自動削除は、作成してから一定期間が経過したノートを削除してくれる機能です。"
dangerZone: "危険区域"
dangerZoneDescription: "以下の機能を利用する際は、特にご注意ください。"
_bubbleGame:
howToPlay: "遊び方"
@ -1360,13 +1363,14 @@ _announcement:
silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。"
_autoRemoval:
use: "노트 자동 삭제를 사용하기"
deleteAfter: "이 기간이 지난 후에 자동으로 삭제"
deleteAfterDescription: "여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다."
noPiningNotes: "고정된 노트를 제외하기"
noPiningNotesDescription: "프로필에 고정된 노트를 제외하고 삭제합니다."
noSpecifiedNotes: "다이렉트 노트를 제외하기"
noSpecifiedNotesDescription: "다이렉트 노트를 제외하고 삭제합니다."
use: "ノート自動削除を使用する"
deleteAfter: "この期間が過ぎたら自動的に削除する"
deleteAfterDescription: "ここに書かれた日数が過ぎるとノートを自動的に削除します。有効化後は、削除対象のノートも一緒に削除されます。"
noPiningNotes: "固定されたノートを除外する"
noPiningNotesDescription: "プロフィールに固定されたノートを除いて削除します。"
noSpecifiedNotes: "ダイレクトノートを除外する"
noSpecifiedNotesDescription: "ダイレクトノートを除いて削除します。"
youCantUseThisTime: "現在、ノートの自動削除を設定することはできません。"
_initialTutorial:
launchTutorial: "チュートリアルを見る"
@ -1801,29 +1805,31 @@ _role:
canCreateContent: "コンテンツの作成"
canUpdateContent: "コンテンツの編集"
canDeleteContent: "コンテンツの削除"
canUseAutoNoteRemoval: "ノート自動削除の利用"
canUseAccountRemoval: "アカウントの削除"
canUseAccountTruncate: "アカウントの整理"
canPurgeAccount: "完全なアカウントの削除"
canUpdateAvatar: "アイコンの変更"
canUpdateBanner: "バナーの変更"
mentionMax: "ノート内の最大メンション数"
mentionLimit: "ノート内の最大メンション数"
canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔"
inviteExpirationTime: "招待コードの有効期限"
canManageCustomEmojis: "カスタム絵文字の管理"
canManageAvatarDecorations: "アバターデコレーションの管理"
driveCapacity: "ドライブ容量"
driveCapacityMb: "ドライブ容量 (MB)"
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
skipNsfwDetection: "AIによるNSFW検出を無視"
pinMax: "ノートのピン留めの最大数"
antennaMax: "アンテナの作成可能数"
antennaNotesMax: "アンテナに保持する最大ノート数"
wordMuteMax: "ワードミュートの最大文字数"
webhookMax: "Webhookの作成可能数"
clipMax: "クリップの作成可能数"
noteEachClipsMax: "クリップ内のノートの最大数"
userListMax: "ユーザーリストの作成可能数"
userEachUserListsMax: "ユーザーリスト内のユーザーの最大数"
pinLimit: "ノートのピン留めの最大数"
antennaLimit: "アンテナの作成可能数"
antennaNotesLimit: "アンテナに保持する最大ノート数"
wordMuteLimit: "ワードミュートの最大文字数"
webhookLimit: "Webhookの作成可能数"
clipLimit: "クリップの作成可能数"
noteEachClipsLimit: "クリップ内のノートの最大数"
userListLimit: "ユーザーリストの作成可能数"
userEachUserListsLimit: "ユーザーリスト内のユーザーの最大数"
rateLimitFactor: "レートリミット"
descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。"
canHideAds: "広告の非表示"
@ -1890,6 +1896,7 @@ _accountDelete:
started: "削除処理が開始されました。"
inProgress: "削除が進行中"
youCantUseThisTime: "現在、アカウントの削除はできません。"
youAreRootAndCantUseThisTime: "あなたは最高管理者であるため、アカウントを削除することはできません。"
_accountTruncate:
accountDelete: "アカウントの整理"
@ -1898,6 +1905,7 @@ _accountTruncate:
requestAccountTruncate: "アカウント整理をリクエスト"
started: "整理処理が開始されました。"
inProgress: "整理が進行中"
youCantUseThisTime: "現在、アカウントの整理はできません。"
_ad:
back: "戻る"

View File

@ -797,7 +797,7 @@ saveConfirm: "저장하시겠습니까?"
deleteConfirm: "삭제하시겠습니까?"
invalidValue: "올바른 값이 아닙니다."
registry: "레지스트리"
closeAccount: "계정 폐쇄"
closeAccount: "계정 삭제"
currentVersion: "현재 버전"
latestVersion: "최신 버전"
youAreRunningUpToDateClient: "사용 중인 클라이언트는 최신입니다."
@ -1287,11 +1287,14 @@ hideCounters: "모든 카운터를 가리기"
hideCountersDescription: "유저 페이지의 노트, 팔로잉, 팔로워 수 및 이러한 통계를 모두 숨깁니다."
youAreOnVacation: "현재 휴가 모드를 사용 중입니다. 이 메시지를 닫으려면 여기를 클릭하세요."
autoRemoval: "노트 자동 삭제"
autoRemovalDescription: "노트 자동 삭제는 작성한 지 일정 기간이 지난 노트를 삭제해주는 기능입니다."
abuseReportCategory: "신고 유형"
selectCategory: "카테고리 선택"
reportComplete: "신고 완료"
blockThisUser: "이 유저 차단하기"
muteThisUser: "이 유저 뮤트하기"
dangerZone: "위험한 것들"
dangerZoneDescription: "함부로 실행하면 어딘가 고장날 수 있는 설정들이니, 실행할 때는 주의하세요."
_bubbleGame:
howToPlay: "설명"
hold: "홀드"
@ -1348,11 +1351,12 @@ _announcement:
_autoRemoval:
use: "노트 자동 삭제를 사용하기"
deleteAfter: "이 기간이 지난 후에 자동으로 삭제"
deleteAfterDescription: 여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다.
deleteAfterDescription: "여기에 적힌 일 수가 지나면 노트를 자동으로 삭제합니다. 활성화 이후에는 삭제 대상인 노트도 같이 제거됩니다."
noPiningNotes: "고정된 노트를 제외하기"
noPiningNotesDescription: "프로필에 고정된 노트를 제외하고 삭제합니다."
noSpecifiedNotes: "다이렉트 노트를 제외하기"
noSpecifiedNotesDescription: "다이렉트 노트를 제외하고 삭제합니다."
youCantUseThisTime: "지금은 노트 자동 삭제를 설정할 수 없습니다."
_initialTutorial:
launchTutorial: "튜토리얼 보기"
title: "튜토리얼"
@ -1777,11 +1781,13 @@ _role:
ltlAvailable: "로컬 타임라인 보이기"
canPublicNote: "공개 노트 허용"
canInitiateConversation: "멘션, 답글, 인용 허용"
mentionMax: "노트에 넣을 수 있는 멘션 수"
mentionLimit: "노트에 넣을 수 있는 멘션 수"
canCreateContent: "컨텐츠 생성 허용"
canUpdateContent: "컨텐츠 수정 허용"
canDeleteContent: "컨텐츠 삭제 허용"
canUseAutoNoteRemoval: "노트 자동 삭제 허용"
canUseAccountRemoval: "계정 삭제 허용"
canUseAccountTruncate: "계정 청소 허용"
canPurgeAccount: "완전한 계정 삭제 허용"
canUpdateAvatar: "아바타 변경 허용"
canUpdateBanner: "배너 변경 허용"
@ -1791,18 +1797,18 @@ _role:
inviteExpirationTime: "초대장 만료 기간"
canManageCustomEmojis: "커스텀 이모지 관리"
canManageAvatarDecorations: "아바타 장식 관리"
driveCapacity: "드라이브 용량"
driveCapacityMb: "드라이브 용량 (MB)"
alwaysMarkNsfw: "파일을 항상 NSFW로 지정"
skipNsfwDetection: "자동 NSFW 감지 기능 미사용"
pinMax: "고정할 수 있는 노트 수"
antennaMax: "만들 수 있는 안테나 수"
antennaNotesMax: "안테나에서 저장할 수 있는 노트 수"
wordMuteMax: "단어 뮤트할 수 있는 문자 수"
webhookMax: "만들 수 있는 웹후크 수"
clipMax: "만들 수 있는 클립 수"
noteEachClipsMax: "클립에 넣을 수 있는 노트 수"
userListMax: "만들 수 있는 유저 리스트 수"
userEachUserListsMax: "유저 리스트에 넣을 수 있는 유저 수"
pinLimit: "고정할 수 있는 노트 수"
antennaLimit: "만들 수 있는 안테나 수"
antennaNotesLimit: "안테나에서 저장할 수 있는 노트 수"
wordMuteLimit: "단어 뮤트할 수 있는 문자 수"
webhookLimit: "만들 수 있는 웹후크 수"
clipLimit: "만들 수 있는 클립 수"
noteEachClipsLimit: "클립에 넣을 수 있는 노트 수"
userListLimit: "만들 수 있는 유저 리스트 수"
userEachUserListsLimit: "유저 리스트에 넣을 수 있는 유저 수"
rateLimitFactor: "요청 빈도 제한"
descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다."
canHideAds: "광고 숨기기"
@ -1859,6 +1865,7 @@ _accountDelete:
started: "삭제 작업이 시작되었습니다."
inProgress: "삭제 진행 중"
youCantUseThisTime: "지금은 계정 삭제를 진행할 수 없습니다."
youAreRootAndCantUseThisTime: "당신은 최고 관리자이므로, 지금은 계정 삭제를 진행할 수 없습니다."
_accountTruncate:
accountTruncate: "계정 청소"
purgeDriveFiles: "드라이브의 파일도 정리하기"
@ -1866,6 +1873,7 @@ _accountTruncate:
requestAccountTruncate: "계정 청소를 요청하기"
started: "청소 작업이 시작되었습니다."
inProgress: "청소 진행 중"
youCantUseThisTime: "지금은 계정 청소를 진행할 수 없습니다."
_ad:
back: "뒤로"
reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기"

View File

@ -2,18 +2,12 @@ export class AutoNoteRemoval1725706236633 {
name = 'AutoNoteRemoval1725706236633'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "auto_removal_condition" ("id" character varying(32) NOT NULL, "deleteAfter" bigint NOT NULL DEFAULT '7', "noPiningNotes" boolean NOT NULL DEFAULT true, "noSpecifiedNotes" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_e6a0b2b5bc8fbc0a07d7e34be7d" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "auto_removal_condition" ("userId" character varying(32) NOT NULL, "deleteAfter" bigint NOT NULL DEFAULT '7', "noPiningNotes" boolean NOT NULL DEFAULT true, "noSpecifiedNotes" boolean NOT NULL DEFAULT true)`);
await queryRunner.query(`ALTER TABLE "user" ADD "autoRemoval" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`COMMENT ON COLUMN "user"."autoRemoval" IS 'Whether the User is using note auto removal.'`);
await queryRunner.query(`ALTER TABLE "user" ADD "autoRemovalConditionId" character varying(32) NOT NULL`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "UQ_051c1df368299ee71088b7508b8" UNIQUE ("autoRemovalConditionId")`);
await queryRunner.query(`ALTER TABLE "user" ADD CONSTRAINT "FK_051c1df368299ee71088b7508b8" FOREIGN KEY ("autoRemovalConditionId") REFERENCES "auto_removal_condition"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "FK_051c1df368299ee71088b7508b8"`);
await queryRunner.query(`ALTER TABLE "user" DROP CONSTRAINT "UQ_051c1df368299ee71088b7508b8"`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "autoRemovalConditionId"`);
await queryRunner.query(`COMMENT ON COLUMN "user"."autoRemoval" IS 'Whether the User is using note auto removal.'`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "autoRemoval"`);
await queryRunner.query(`DROP TABLE "auto_removal_condition"`);

View File

@ -40,6 +40,9 @@ export type RolePolicies = {
canCreateContent: boolean;
canUpdateContent: boolean;
canDeleteContent: boolean;
canUseAutoNoteRemoval: boolean;
canUseAccountRemoval: boolean;
canUseAccountTruncate: boolean;
canPurgeAccount: boolean;
canUpdateAvatar: boolean;
canUpdateBanner: boolean;
@ -69,7 +72,6 @@ export type RolePolicies = {
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
canUseAccountRemoval: boolean;
mutualLinkSectionLimit: number;
mutualLinkLimit: number;
};
@ -82,6 +84,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
canCreateContent: true,
canUpdateContent: true,
canDeleteContent: true,
canUseAutoNoteRemoval: true,
canUseAccountRemoval: true,
canUseAccountTruncate: true,
canPurgeAccount: true,
canUpdateAvatar: true,
canUpdateBanner: true,
@ -111,7 +116,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
canUseAccountRemoval: true,
mutualLinkSectionLimit: 1,
mutualLinkLimit: 3,
};
@ -397,7 +401,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)),
canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)),
canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)),
canUseAutoNoteRemoval: calc('canUseAutoNoteRemoval', vs => vs.some(v => v === true)),
canUseAccountRemoval: calc('canUseAccountRemoval', vs => vs.some(v => v === true)),
canUseAccountTruncate: calc('canUseAccountTruncate', vs => vs.some(v => v === true)),
canPurgeAccount: calc('canPurgeAccount', vs => vs.some(v => v === true)),
canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)),
canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)),

View File

@ -196,6 +196,18 @@ export const packedRolePoliciesSchema = {
type: 'boolean',
optional: false, nullable: false,
},
canUseAutoNoteRemoval: {
type: 'boolean',
optional: false, nullable: false,
},
canUseAccountRemoval: {
type: 'boolean',
optional: false, nullable: false,
},
canUseAccountTruncate: {
type: 'boolean',
optional: false, nullable: false,
},
canPurgeAccount: {
type: 'boolean',
optional: false, nullable: false,
@ -312,10 +324,6 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
canUseAccountRemoval: {
type: 'boolean',
optional: false, nullable: false,
},
mutualLinkSectionLimit: {
type: 'integer',
optional: false, nullable: false,

View File

@ -151,7 +151,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
case 'clean': return this.cleanProcessorService.process();
case 'autoNoteRemoval': return this.autoNoteRemovalProcessorService.process();
case 'autoNoteRemoval': return this.autoNoteRemovalProcessorService.process(job);
default: throw new Error(`unrecognized job type ${job.name} for system`);
}
}, {

View File

@ -12,6 +12,7 @@ import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { IdService } from '@/core/IdService.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@ -33,6 +34,7 @@ export class AutoNoteRemovalProcessorService {
private autoRemovalConditionRepository: AutoRemovalConditionRepository,
private idService: IdService,
private roleService: RoleService,
private noteDeleteService: NoteDeleteService,
private queueLoggerService: QueueLoggerService,
) {
@ -40,7 +42,7 @@ export class AutoNoteRemovalProcessorService {
}
@bindThis
public async process(): Promise<void> {
public async process(job: Bull.Job<Record<string, unknown>>): Promise<void> {
this.logger.info('Checking notes that to remove automatically...');
this.logger.info('Checking users that enabled note auto-removal');
const users = await this.usersRepository.find({ where: { autoRemoval: true } });
@ -51,6 +53,8 @@ export class AutoNoteRemovalProcessorService {
const now = Date.now();
for (const user of users) {
const policies = await this.roleService.getUserPolicies(user.id);
if (!policies.canUseAutoNoteRemoval) continue;
const autoRemovalCondition = await this.autoRemovalConditionRepository.findOneByOrFail({ userId: user.id });
const pinings: MiUserNotePining[] = await this.userNotePiningsRepository.findBy({ userId: user.id });
const piningNoteIds: string[] = pinings.map(pining => pining.noteId); // pining.note always undefined (bug?)
@ -65,11 +69,11 @@ export class AutoNoteRemovalProcessorService {
// Delete notes
let cursor: MiNote['id'] | null = null;
let condition: string[] = [];
if (autoRemovalCondition.noSpecifiedNotes === true) {
if (autoRemovalCondition.noSpecifiedNotes) {
condition = [...condition, ...specifiedNoteIds];
}
if (autoRemovalCondition.noPiningNotes === true) {
if (autoRemovalCondition.noPiningNotes) {
condition = [...condition, ...piningNoteIds];
}
@ -99,11 +103,13 @@ export class AutoNoteRemovalProcessorService {
const createdAt: number = this.idService.parse(note.id).date.getTime();
const delta: number = now - createdAt;
if (delta > deleteAfter) {
await Promise.bind(this.noteDeleteService.delete(user, note, false, user));
Promise.bind(this.noteDeleteService.delete(user, note, false, user));
}
}
await job.updateProgress(100 / users.length * users.indexOf(user));
}
await job.updateProgress(100);
this.logger.succ('All of auto-removable notes deleted');
}

View File

@ -17,13 +17,9 @@ export const meta = {
requireCredential: true,
secure: true,
requireRolePolicy: 'canUseAccountRemoval',
errors: {
removalDisabled: {
message: 'Account removal is disabled by your role.',
code: 'REMOVAL_DISABLED',
id: '453d954b-3d8b-4df0-a261-b26ec6660ea3',
},
authenticationFailed: {
message: 'Your password or 2FA token is invalid.',
code: 'AUTHENTICATION_FAILED',
@ -57,16 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userAuthService: UserAuthService,
private deleteAccountService: DeleteAccountService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
const policies = await this.roleService.getUserPolicies(me.id);
if (!policies.canUseAccountRemoval) {
throw new ApiError(meta.errors.removalDisabled);
}
const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id });
if (userDetailed.isDeleted) {
throw new ApiError(meta.errors.alreadyRemoved);

View File

@ -10,11 +10,26 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { TruncateAccountService } from '@/core/TruncateAccountService.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import {ApiError} from "@/server/api/error.js";
export const meta = {
requireCredential: true,
secure: true,
requireRolePolicy: 'canUseAccountTruncate',
errors: {
authenticationFailed: {
message: 'Your password or 2FA token is invalid.',
code: 'AUTHENTICATION_FAILED',
id: 'ea791cff-63e7-4b2a-92fc-646ab641794e',
},
alreadyRemoved: {
message: 'Your account is removed.',
code: 'ACCOUNT_REMOVED',
id: '59b8f0e6-6eb2-4dc1-a080-1de3108416d0',
},
},
} as const;
export const paramDef = {
@ -40,30 +55,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private truncateAccountService: TruncateAccountService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token;
const purgeDrive = ps.purgeDrive ? true : false;
const purgeDrive = !!ps.purgeDrive;
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
if (profile.twoFactorEnabled) {
if (token == null) {
throw new Error('authentication failed');
}
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
throw new Error('authentication failed');
}
}
const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id });
if (userDetailed.isDeleted) {
return;
throw new ApiError(meta.errors.alreadyRemoved);
}
const passwordMatched = await bcrypt.compare(ps.password, profile.password!);
if (!passwordMatched) {
throw new Error('incorrect password');
throw new ApiError(meta.errors.authenticationFailed);
}
if (profile.twoFactorEnabled) {
const token = ps.token;
if (token == null) {
throw new ApiError(meta.errors.authenticationFailed);
}
await this.userAuthService.twoFactorAuthenticate(profile, token);
}
await this.truncateAccountService.truncateAccount(me, purgeDrive);

View File

@ -79,6 +79,9 @@ export const ROLE_POLICIES = [
'canCreateContent',
'canUpdateContent',
'canDeleteContent',
'canUseAutoNoteRemoval',
'canUseAccountRemoval',
'canUseAccountTruncate',
'canPurgeAccount',
'canUpdateAvatar',
'canUpdateBanner',
@ -108,7 +111,6 @@ export const ROLE_POLICIES = [
'userEachUserListsLimit',
'rateLimitFactor',
'avatarDecorationLimit',
'canUseAccountRemoval',
'mutualLinkSectionLimit',
'mutualLinkLimit',
] as const;

View File

@ -245,6 +245,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseAutoNoteRemoval, 'canUseAutoNoteRemoval'])">
<template #label>{{ i18n.ts._role._options.canUseAutoNoteRemoval }}</template>
<template #suffix>
<span v-if="role.policies.canUseAutoNoteRemoval.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canUseAutoNoteRemoval.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUseAutoNoteRemoval)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canUseAutoNoteRemoval.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canUseAutoNoteRemoval.value" :disabled="role.policies.canUseAutoNoteRemoval.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canUseAutoNoteRemoval.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseAccountRemoval, 'canUseAccountRemoval'])">
<template #label>{{ i18n.ts._role._options.canUseAccountRemoval }}</template>
<template #suffix>
@ -265,6 +285,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseAccountTruncate, 'canUseAccountTruncate'])">
<template #label>{{ i18n.ts._role._options.canUseAccountTruncate }}</template>
<template #suffix>
<span v-if="role.policies.canUseAccountTruncate.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canUseAccountTruncate.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUseAccountTruncate)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canUseAccountTruncate.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canUseAccountTruncate.value" :disabled="role.policies.canUseAccountTruncate.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canUseAccountTruncate.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canPurgeAccount, 'canPurgeAccount'])">
<template #label>{{ i18n.ts._role._options.canPurgeAccount }}</template>
<template #suffix>
@ -325,8 +365,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionLimit, 'mentionLimit'])">
<template #label>{{ i18n.ts._role._options.mentionLimit }}</template>
<template #suffix>
<span v-if="role.policies.mentionLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.mentionLimit.value }}</span>
@ -543,8 +583,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacityMb, 'driveCapacityMb'])">
<template #label>{{ i18n.ts._role._options.driveCapacityMb }}</template>
<template #suffix>
<span v-if="role.policies.driveCapacityMb.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.driveCapacityMb.value + 'MB' }}</span>
@ -603,8 +643,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinLimit, 'pinLimit'])">
<template #label>{{ i18n.ts._role._options.pinLimit }}</template>
<template #suffix>
<span v-if="role.policies.pinLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.pinLimit.value }}</span>
@ -622,8 +662,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaLimit, 'antennaLimit'])">
<template #label>{{ i18n.ts._role._options.antennaLimit }}</template>
<template #suffix>
<span v-if="role.policies.antennaLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.antennaLimit.value }}</span>
@ -641,8 +681,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaNotesMax, 'antennaNotesLimit'])">
<template #label>{{ i18n.ts._role._options.antennaNotesMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaNotesLimit, 'antennaNotesLimit'])">
<template #label>{{ i18n.ts._role._options.antennaNotesLimit }}</template>
<template #suffix>
<span v-if="role.policies.antennaNotesLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.antennaNotesLimit.value }}</span>
@ -660,8 +700,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])">
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteLimit, 'wordMuteLimit'])">
<template #label>{{ i18n.ts._role._options.wordMuteLimit }}</template>
<template #suffix>
<span v-if="role.policies.wordMuteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.wordMuteLimit.value }}</span>
@ -680,8 +720,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])">
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.webhookLimit, 'webhookLimit'])">
<template #label>{{ i18n.ts._role._options.webhookLimit }}</template>
<template #suffix>
<span v-if="role.policies.webhookLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.webhookLimit.value }}</span>
@ -699,8 +739,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])">
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.clipLimit, 'clipLimit'])">
<template #label>{{ i18n.ts._role._options.clipLimit }}</template>
<template #suffix>
<span v-if="role.policies.clipLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.clipLimit.value }}</span>
@ -718,8 +758,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])">
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsLimit, 'noteEachClipsLimit'])">
<template #label>{{ i18n.ts._role._options.noteEachClipsLimit }}</template>
<template #suffix>
<span v-if="role.policies.noteEachClipsLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.noteEachClipsLimit.value }}</span>
@ -737,8 +777,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])">
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.userListLimit, 'userListLimit'])">
<template #label>{{ i18n.ts._role._options.userListLimit }}</template>
<template #suffix>
<span v-if="role.policies.userListLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.userListLimit.value }}</span>
@ -756,8 +796,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])">
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsLimit, 'userEachUserListsLimit'])">
<template #label>{{ i18n.ts._role._options.userEachUserListsLimit }}</template>
<template #suffix>
<span v-if="role.policies.userEachUserListsLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.userEachUserListsLimit.value }}</span>

View File

@ -80,6 +80,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseAutoNoteRemoval, 'canUseAutoNoteRemoval'])">
<template #label>{{ i18n.ts._role._options.canUseAutoNoteRemoval }}</template>
<template #suffix>{{ policies.canUseAutoNoteRemoval ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canUseAutoNoteRemoval">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseAccountRemoval, 'canUseAccountRemoval'])">
<template #label>{{ i18n.ts._role._options.canUseAccountRemoval }}</template>
<template #suffix>{{ policies.canUseAccountRemoval ? i18n.ts.yes : i18n.ts.no }}</template>
@ -88,6 +96,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUseAccountTruncate, 'canUseAccountTruncate'])">
<template #label>{{ i18n.ts._role._options.canUseAccountTruncate }}</template>
<template #suffix>{{ policies.canUseAccountTruncate ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canUseAccountTruncate">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canPurgeAccount, 'canPurgeAccount'])">
<template #label>{{ i18n.ts._role._options.canPurgeAccount }}</template>
<template #suffix>{{ policies.canPurgeAccount ? i18n.ts.yes : i18n.ts.no }}</template>
@ -112,8 +128,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionMax, 'mentionLimit'])">
<template #label>{{ i18n.ts._role._options.mentionMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.mentionLimit, 'mentionLimit'])">
<template #label>{{ i18n.ts._role._options.mentionLimit }}</template>
<template #suffix>{{ policies.mentionLimit }}</template>
<MkInput v-model="policies.mentionLimit" type="number">
</MkInput>
@ -198,8 +214,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacity, 'driveCapacityMb'])">
<template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.driveCapacityMb, 'driveCapacityMb'])">
<template #label>{{ i18n.ts._role._options.driveCapacityMb }}</template>
<template #suffix>{{ policies.driveCapacityMb }}MB</template>
<MkInput v-model="policies.driveCapacityMb" type="number">
<template #suffix>MB</template>
@ -222,65 +238,65 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinMax, 'pinLimit'])">
<template #label>{{ i18n.ts._role._options.pinMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.pinLimit, 'pinLimit'])">
<template #label>{{ i18n.ts._role._options.pinLimit }}</template>
<template #suffix>{{ policies.pinLimit }}</template>
<MkInput v-model="policies.pinLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaMax, 'antennaLimit'])">
<template #label>{{ i18n.ts._role._options.antennaMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaLimit, 'antennaLimit'])">
<template #label>{{ i18n.ts._role._options.antennaLimit }}</template>
<template #suffix>{{ policies.antennaLimit }}</template>
<MkInput v-model="policies.antennaLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaNotesMax, 'antennaNotesLimit'])">
<template #label>{{ i18n.ts._role._options.antennaNotesMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.antennaNotesLimit, 'antennaNotesLimit'])">
<template #label>{{ i18n.ts._role._options.antennaNotesLimit }}</template>
<template #suffix>{{ policies.antennaNotesLimit }}</template>
<MkInput v-model="policies.antennaNotesLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteMax, 'wordMuteLimit'])">
<template #label>{{ i18n.ts._role._options.wordMuteMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.wordMuteLimit, 'wordMuteLimit'])">
<template #label>{{ i18n.ts._role._options.wordMuteLimit }}</template>
<template #suffix>{{ policies.wordMuteLimit }}</template>
<MkInput v-model="policies.wordMuteLimit" type="number">
<template #suffix>items</template>
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.webhookMax, 'webhookLimit'])">
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.webhookLimit, 'webhookLimit'])">
<template #label>{{ i18n.ts._role._options.webhookLimit }}</template>
<template #suffix>{{ policies.webhookLimit }}</template>
<MkInput v-model="policies.webhookLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.clipMax, 'clipLimit'])">
<template #label>{{ i18n.ts._role._options.clipMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.clipLimit, 'clipLimit'])">
<template #label>{{ i18n.ts._role._options.clipLimit }}</template>
<template #suffix>{{ policies.clipLimit }}</template>
<MkInput v-model="policies.clipLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsMax, 'noteEachClipsLimit'])">
<template #label>{{ i18n.ts._role._options.noteEachClipsMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.noteEachClipsLimit, 'noteEachClipsLimit'])">
<template #label>{{ i18n.ts._role._options.noteEachClipsLimit }}</template>
<template #suffix>{{ policies.noteEachClipsLimit }}</template>
<MkInput v-model="policies.noteEachClipsLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.userListMax, 'userListLimit'])">
<template #label>{{ i18n.ts._role._options.userListMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.userListLimit, 'userListLimit'])">
<template #label>{{ i18n.ts._role._options.userListLimit }}</template>
<template #suffix>{{ policies.userListLimit }}</template>
<MkInput v-model="policies.userListLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsMax, 'userEachUserListsLimit'])">
<template #label>{{ i18n.ts._role._options.userEachUserListsMax }}</template>
<MkFolder v-if="matchQuery([i18n.ts._role._options.userEachUserListsLimit, 'userEachUserListsLimit'])">
<template #label>{{ i18n.ts._role._options.userEachUserListsLimit }}</template>
<template #suffix>{{ policies.userEachUserListsLimit }}</template>
<MkInput v-model="policies.userEachUserListsLimit" type="number">
</MkInput>

View File

@ -16,6 +16,40 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<FormSection>
<template #label><i class="ti ti-time-duration-off"></i> {{ i18n.ts.autoRemoval }}</template>
<template #description>{{ i18n.ts.autoRemovalDescription }}</template>
<div v-if="$i.policies.canUseAutoNoteRemoval" class="_gaps_m">
<MkInfo warn rounded>
{{ i18n.ts.thisIsExperimentalFeature }}
</MkInfo>
<MkSwitch v-model="autoRemoval" @update:modelValue="saveRemovalCondition()">{{ i18n.ts._autoRemoval.use }}</MkSwitch>
<template v-if="autoRemoval">
<MkInput v-model="deleteAfter" :type="'number'" :placeholder="'7'" :required="true" @update:modelValue="periodChanged = true">
<template #label>{{ i18n.ts._autoRemoval.deleteAfter }}</template>
<template #caption>{{ i18n.ts._autoRemoval.deleteAfterDescription }}</template>
</MkInput>
<MkButton v-if="periodChanged" primary class="save" @click="saveRemovalCondition"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkSwitch v-model="noPiningNotes" @update:modelValue="saveRemovalCondition()">
<template #label>{{ i18n.ts._autoRemoval.noPiningNotes }}</template>
<template #caption>{{ i18n.ts._autoRemoval.noPiningNotesDescription }}</template>
</MkSwitch>
<MkSwitch v-model="noSpecifiedNotes" @update:modelValue="saveRemovalCondition()">
<template #label>{{ i18n.ts._autoRemoval.noSpecifiedNotes }}</template>
<template #caption>{{ i18n.ts._autoRemoval.noSpecifiedNotesDescription }}</template>
</MkSwitch>
</template>
</div>
<div v-else class="_gaps_m">
<MkInfo>{{ i18n.ts._autoRemoval.youCantUseThisTime }}</MkInfo>
</div>
</FormSection>
<FormSection>
<template #label><i class="ti ti-lock-access"></i> {{ i18n.ts.hideSensitiveInformation }}</template>
<template #description>{{ i18n.ts._hideSensitiveInformation.about }}</template>
@ -97,39 +131,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</div>
</FormSection>
<!--
<MkSelect v-model="followingVisibility" @update:modelValue="save()">
<template #label>{{ i18n.ts.followingVisibility }}</template>
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
<option value="private">{{ i18n.ts._ffVisibility.private }}</option>
</MkSelect>
<MkSelect v-model="followersVisibility" @update:modelValue="save()">
<template #label>{{ i18n.ts.followersVisibility }}</template>
<option value="public">{{ i18n.ts._ffVisibility.public }}</option>
<option value="followers">{{ i18n.ts._ffVisibility.followers }}</option>
<option value="private">{{ i18n.ts._ffVisibility.private }}</option>
</MkSelect>
<MkSwitch v-model="hideOnlineStatus" @update:modelValue="save()">
{{ i18n.ts.hideOnlineStatus }}
<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template>
</MkSwitch>
<MkSwitch v-model="noCrawle" @update:modelValue="save()">
{{ i18n.ts.noCrawle }}
<template #caption>{{ i18n.ts.noCrawleDescription }}</template>
</MkSwitch>
<MkSwitch v-model="preventAiLearning" @update:modelValue="save()">
{{ i18n.ts.preventAiLearning }}<span class="_beta">{{ i18n.ts.beta }}</span>
<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template>
</MkSwitch>
<MkSwitch v-model="isExplorable" @update:modelValue="save()">
{{ i18n.ts.makeExplorable }}
<template #caption>{{ i18n.ts.makeExplorableDescription }}</template>
</MkSwitch>
-->
</div>
</template>
@ -147,10 +148,16 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import MkButton from "@/components/MkButton.vue";
import MkInput from "@/components/MkInput.vue";
const $i = signinRequired();
const isVacation = ref<boolean | undefined>($i.isVacation !== null ? $i.isVacation : undefined);
const autoRemoval = ref<boolean>($i.autoRemovalCondition.active);
const deleteAfter = ref<number>($i.autoRemovalCondition.deleteAfter || 7);
const noPiningNotes = ref<boolean>($i.autoRemovalCondition.noPiningNotes);
const noSpecifiedNotes = ref<boolean>($i.autoRemovalCondition.noSpecifiedNotes);
const periodChanged = ref<boolean>(false);
const hideCounters = computed(defaultStore.makeGetterSetter('hideCounters'));
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
@ -160,6 +167,15 @@ const hideDriveFileList = computed(defaultStore.makeGetterSetter('hideDriveFileL
const hideModerationLog = computed(defaultStore.makeGetterSetter('hideModerationLog'));
const hideRoleList = computed(defaultStore.makeGetterSetter('hideRoleList'));
function saveRemovalCondition() {
misskeyApi('i/update-removal-condition', {
active: autoRemoval.value,
deleteAfter: deleteAfter.value,
noPiningNotes: noPiningNotes.value,
noSpecifiedNotes: noSpecifiedNotes.value,
});
}
function save() {
if (isVacation.value === true) {
defaultStore.set('vacationAlert', true);

View File

@ -36,61 +36,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts.autoRemoval }}</template>
<div class="_gaps_m">
<MkSwitch v-model="autoRemoval">{{ i18n.ts._autoRemoval.use }}</MkSwitch>
<template v-if="autoRemoval">
<MkInput v-model="deleteAfter" :type="'number'" :placeholder="'7'" :required="true">
<template #label>{{ i18n.ts._autoRemoval.deleteAfter }}</template>
<template #caption>{{ i18n.ts._autoRemoval.deleteAfterDescription }}</template>
</MkInput>
<MkSwitch v-model="noPiningNotes">
<template #label>{{ i18n.ts._autoRemoval.noPiningNotes }}</template>
<template #caption>{{ i18n.ts._autoRemoval.noPiningNotesDescription }}</template>
</MkSwitch>
<MkSwitch v-model="noSpecifiedNotes">
<template #label>{{ i18n.ts._autoRemoval.noSpecifiedNotes }}</template>
<template #caption>{{ i18n.ts._autoRemoval.noSpecifiedNotesDescription }}</template>
</MkSwitch>
</template>
<MkButton primary class="save" @click="saveRemovalCondition"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-alert-triangle"></i></template>
<template #label>{{ i18n.ts.closeAccount }}</template>
<div v-if="$i.policies.canUseAccountRemoval" class="_gaps_m">
<MkInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</MkInfo>
<MkInfo>{{ i18n.ts._accountDelete.sendEmail }}</MkInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div>
<div v-else class="_gaps_m">
<MkInfo warn>{{ i18n.ts._accountDelete.youCantUseThisTime }}</MkInfo>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-recycle"></i></template>
<template #label>{{ i18n.ts.truncateAccount }}</template>
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountTruncate.mayTakeTime }}</FormInfo>
<MkSwitch v-model="purgeDrive">
<template #label>{{ i18n.ts._accountTruncate.purgeDriveFiles }}</template>
</MkSwitch>
<MkButton v-if="!$i.isDeleted" danger @click="truncateAccount">{{ i18n.ts._accountTruncate.requestAccountTruncate }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountTruncate.inProgress }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-code"></i></template>
<template #label>{{ i18n.ts.developer }}</template>
@ -115,6 +60,48 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton danger @click="updateRepliesAll(false)"><i class="ti ti-messages-off"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
</div>
</FormSection>
<FormSection>
<template #label><i class="ti ti-alert-circle"></i> {{ i18n.ts.dangerZone }}</template>
<template #description>{{ i18n.ts.dangerZoneDescription }}</template>
<div class="_gaps_s">
<MkFolder>
<template #icon><i class="ti ti-trash"></i></template>
<template #label>{{ i18n.ts.closeAccount }}</template>
<div v-if="$i.policies.canUseAccountRemoval && !$i.isRoot" class="_gaps_m">
<MkInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</MkInfo>
<MkInfo>{{ i18n.ts._accountDelete.sendEmail }}</MkInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div>
<div v-else-if="$i.isRoot === true" class="_gaps_m">
<MkInfo>{{ i18n.ts._accountDelete.youAreRootAndCantUseThisTime }}</MkInfo>
</div>
<div v-else class="_gaps_m">
<MkInfo>{{ i18n.ts._accountDelete.youCantUseThisTime }}</MkInfo>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-bomb"></i></template>
<template #label>{{ i18n.ts.truncateAccount }}</template>
<div v-if="$i.policies.canUseAccountTruncate" class="_gaps_m">
<MkInfo warn>{{ i18n.ts._accountTruncate.mayTakeTime }}</MkInfo>
<MkSwitch v-model="purgeDrive">
<template #label>{{ i18n.ts._accountTruncate.purgeDriveFiles }}</template>
</MkSwitch>
<MkButton v-if="!$i.isDeleted" danger @click="truncateAccount">{{ i18n.ts._accountTruncate.requestAccountTruncate }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountTruncate.inProgress }}</MkButton>
</div>
<div v-else class="_gaps_m">
<MkInfo>{{ i18n.ts._accountTruncate.youCantUseThisTime }}</MkInfo>
</div>
</MkFolder>
</div>
</FormSection>
</div>
</template>
@ -140,19 +127,6 @@ const $i = signinRequired();
const purgeDrive = ref<boolean>(false);
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
const autoRemoval = ref<boolean>($i.autoRemovalCondition.active);
const deleteAfter = ref<number>($i.autoRemovalCondition.deleteAfter || 7);
const noPiningNotes = ref<boolean>($i.autoRemovalCondition.noPiningNotes);
const noSpecifiedNotes = ref<boolean>($i.autoRemovalCondition.noSpecifiedNotes);
function saveRemovalCondition() {
misskeyApi('i/update-removal-condition', {
active: autoRemoval.value,
deleteAfter: deleteAfter.value,
noPiningNotes: noPiningNotes.value,
noSpecifiedNotes: noSpecifiedNotes.value,
});
}
async function deleteAccount() {
{

View File

@ -4972,6 +4972,9 @@ export type components = {
canCreateContent: boolean;
canUpdateContent: boolean;
canDeleteContent: boolean;
canUseAutoNoteRemoval: boolean;
canUseAccountRemoval: boolean;
canUseAccountTruncate: boolean;
canPurgeAccount: boolean;
canUpdateAvatar: boolean;
canUpdateBanner: boolean;
@ -5001,7 +5004,6 @@ export type components = {
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
canUseAccountRemoval: boolean;
mutualLinkSectionLimit: number;
mutualLinkLimit: number;
};