feat: 안되겠소, 쏩시다!
This commit is contained in:
parent
3e90f8995e
commit
cda3d93db8
16 changed files with 322 additions and 20 deletions
|
@ -1287,6 +1287,10 @@ autoRemoval: "Automatic note deletion"
|
||||||
autoRemovalDescription: "You can delete your note when it exceeds period you set."
|
autoRemovalDescription: "You can delete your note when it exceeds period you set."
|
||||||
CheckedByHIBP: "In addition to ensuring your passwords are secure, HIBP scans for password leaks."
|
CheckedByHIBP: "In addition to ensuring your passwords are secure, HIBP scans for password leaks."
|
||||||
changeUserName: "Change name"
|
changeUserName: "Change name"
|
||||||
|
normalize: "Normalize"
|
||||||
|
normalizeConfirm: "After normalization, the account will be irreversible. Are you sure you want to do this?"
|
||||||
|
normalizeDescription: "Normalization is a feature for bulk data wiping and account suspension of users, which has a similar effect to deleting an account and closes all reports after it is run. Please note that this is an irreversible action."
|
||||||
|
useNormalization: "Show the Normalize menu"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "How to play"
|
howToPlay: "How to play"
|
||||||
hold: "Hold"
|
hold: "Hold"
|
||||||
|
@ -2578,6 +2582,7 @@ _moderationLogTypes:
|
||||||
deleteAvatarDecoration: "Avatar decoration deleted"
|
deleteAvatarDecoration: "Avatar decoration deleted"
|
||||||
unsetUserAvatar: "Unset this user's avatar"
|
unsetUserAvatar: "Unset this user's avatar"
|
||||||
unsetUserBanner: "Unset this user's banner"
|
unsetUserBanner: "Unset this user's banner"
|
||||||
|
normalize: "Normalization"
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "File details"
|
title: "File details"
|
||||||
type: "File type"
|
type: "File type"
|
||||||
|
|
20
locales/index.d.ts
vendored
20
locales/index.d.ts
vendored
|
@ -5312,6 +5312,22 @@ export interface Locale extends ILocale {
|
||||||
* 名前を変更
|
* 名前を変更
|
||||||
*/
|
*/
|
||||||
"changeUserName": string;
|
"changeUserName": string;
|
||||||
|
/**
|
||||||
|
* 正常化
|
||||||
|
*/
|
||||||
|
"normalize": string;
|
||||||
|
/**
|
||||||
|
* 正常化すると元に戻せなくなり、これはアカウントの削除と同様の効力を持ちます。実行しますか?
|
||||||
|
*/
|
||||||
|
"normalizeConfirm": string;
|
||||||
|
/**
|
||||||
|
* 正常化は、ユーザーの一括的なデータ抹消やアカウント制裁が必要な場合に使用する機能です。アカウントを正常化した後は取り返しのつかないことに留意してください。
|
||||||
|
*/
|
||||||
|
"normalizeDescription": string;
|
||||||
|
/**
|
||||||
|
* 正規化機能を使用する
|
||||||
|
*/
|
||||||
|
"useNormalization": string;
|
||||||
"_bubbleGame": {
|
"_bubbleGame": {
|
||||||
/**
|
/**
|
||||||
* 遊び方
|
* 遊び方
|
||||||
|
@ -10283,6 +10299,10 @@ export interface Locale extends ILocale {
|
||||||
* ユーザーのバナーを解除
|
* ユーザーのバナーを解除
|
||||||
*/
|
*/
|
||||||
"unsetUserBanner": string;
|
"unsetUserBanner": string;
|
||||||
|
/**
|
||||||
|
* 正 常 化
|
||||||
|
*/
|
||||||
|
"normalize": string;
|
||||||
};
|
};
|
||||||
"_fileViewer": {
|
"_fileViewer": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1322,6 +1322,10 @@ dangerZone: "危険区域"
|
||||||
dangerZoneDescription: "以下の機能を利用する際は、特にご注意ください。"
|
dangerZoneDescription: "以下の機能を利用する際は、特にご注意ください。"
|
||||||
checkedByHIBP: "パスワードの安全性に加え、HIBPを通じてパスワードの漏洩を検査します。"
|
checkedByHIBP: "パスワードの安全性に加え、HIBPを通じてパスワードの漏洩を検査します。"
|
||||||
changeUserName: "名前を変更"
|
changeUserName: "名前を変更"
|
||||||
|
normalize: "正常化"
|
||||||
|
normalizeConfirm: "正常化すると元に戻せなくなり、これはアカウントの削除と同様の効力を持ちます。実行しますか?"
|
||||||
|
normalizeDescription: "正常化は、ユーザーの一括的なデータ抹消やアカウント制裁が必要な場合に使用する機能です。アカウントを正常化した後は取り返しのつかないことに留意してください。"
|
||||||
|
useNormalization: "正規化機能を使用する"
|
||||||
|
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "遊び方"
|
howToPlay: "遊び方"
|
||||||
|
@ -2717,6 +2721,7 @@ _moderationLogTypes:
|
||||||
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
deleteAvatarDecoration: "アイコンデコレーションを削除"
|
||||||
unsetUserAvatar: "ユーザーのアイコンを解除"
|
unsetUserAvatar: "ユーザーのアイコンを解除"
|
||||||
unsetUserBanner: "ユーザーのバナーを解除"
|
unsetUserBanner: "ユーザーのバナーを解除"
|
||||||
|
normalize: "正 常 化"
|
||||||
|
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "ファイルの詳細"
|
title: "ファイルの詳細"
|
||||||
|
|
|
@ -610,12 +610,12 @@ removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합
|
||||||
userSuspended: "이 계정은 정지된 상태입니다."
|
userSuspended: "이 계정은 정지된 상태입니다."
|
||||||
userLimited: "이 계정은 제한된 상태입니다."
|
userLimited: "이 계정은 제한된 상태입니다."
|
||||||
userSilenced: "이 계정은 사일런스된 상태입니다."
|
userSilenced: "이 계정은 사일런스된 상태입니다."
|
||||||
yourAccountSuspendedTitle: "계정이 정지되었습니다"
|
yourAccountSuspendedTitle: "당신의 계정이 정지되었습니다"
|
||||||
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
|
yourAccountSuspendedDescription: "당신의 계정이 규정 위반 또는 관리자 재량에 의해 정지되었습니다. 잘못되었다고 생각할 경우 관리자에게 문의해주십시오."
|
||||||
tokenRevoked: "유효하지 않은 토큰입니다"
|
tokenRevoked: "유효하지 않은 토큰입니다"
|
||||||
tokenRevokedDescription: "로그인 토큰이 비활성화되었습니다. 다시 로그인하여 주십시오."
|
tokenRevokedDescription: "다른 곳에서 비밀번호를 변경했거나, 다른 모든 세션을 종료했습니다. 계속하려면 다시 로그인하세요."
|
||||||
accountDeleted: "계정이 정지되었습니다"
|
accountDeleted: "당신의 계정이 정지되었습니다"
|
||||||
accountDeletedDescription: "이 계정이 삭제되었습니다."
|
accountDeletedDescription: "당신의 계정이 일정 기간 이상 비활동 또는 관리자 재량에 의해 삭제되었습니다."
|
||||||
menu: "메뉴"
|
menu: "메뉴"
|
||||||
divider: "구분선"
|
divider: "구분선"
|
||||||
addItem: "항목 추가"
|
addItem: "항목 추가"
|
||||||
|
@ -1310,6 +1310,10 @@ dangerZoneDescription: "함부로 실행하면 어딘가 고장날 수 있는
|
||||||
checkedByHIBP: "비밀번호의 안전성과 더불어, HIBP를 통해 비밀번호 유출을 검사합니다."
|
checkedByHIBP: "비밀번호의 안전성과 더불어, HIBP를 통해 비밀번호 유출을 검사합니다."
|
||||||
changeUserName: "이름 변경"
|
changeUserName: "이름 변경"
|
||||||
pleaseSelectAccount: "사용할 계정을 선택해주십시오"
|
pleaseSelectAccount: "사용할 계정을 선택해주십시오"
|
||||||
|
normalize: "정상화"
|
||||||
|
normalizeConfirm: "정상화 이후에는 계정을 되돌릴 수 없게 됩니다. 실행하시겠습니까?"
|
||||||
|
normalizeDescription: "정상화는 유저의 일괄적인 데이터 말소 및 계정 정지를 위한 기능으로, 계정 삭제와 비슷한 효과를 가지며, 실행 후에는 모든 신고가 닫히게 됩니다. 기능을 실행하고 나면 되돌릴 수 없는 점을 유의하시기 바랍니다."
|
||||||
|
useNormalization: "정상화 메뉴를 표시하기"
|
||||||
_bubbleGame:
|
_bubbleGame:
|
||||||
howToPlay: "설명"
|
howToPlay: "설명"
|
||||||
hold: "홀드"
|
hold: "홀드"
|
||||||
|
@ -2609,6 +2613,7 @@ _moderationLogTypes:
|
||||||
deleteAvatarDecoration: "아바타 장식 삭제"
|
deleteAvatarDecoration: "아바타 장식 삭제"
|
||||||
unsetUserAvatar: "유저 아바타 제거"
|
unsetUserAvatar: "유저 아바타 제거"
|
||||||
unsetUserBanner: "유저 배너 제거"
|
unsetUserBanner: "유저 배너 제거"
|
||||||
|
normalize: "정 상 화"
|
||||||
_fileViewer:
|
_fileViewer:
|
||||||
title: "파일 상세"
|
title: "파일 상세"
|
||||||
type: "파일 유형"
|
type: "파일 유형"
|
||||||
|
|
|
@ -64,6 +64,7 @@ import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'
|
||||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||||
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
||||||
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
||||||
|
import * as ep___admin_normalization from './endpoints/admin/normalization.js';
|
||||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||||
|
@ -462,6 +463,7 @@ const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', us
|
||||||
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
|
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
|
||||||
const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
|
const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
|
||||||
const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
|
const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
|
||||||
|
const $admin_normalization: Provider = { provide: 'ep:admin/normalization', useClass: ep___admin_normalization.default };
|
||||||
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
|
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
|
||||||
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
|
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
|
||||||
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
|
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
|
||||||
|
@ -864,6 +866,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_getUserIps,
|
$admin_getUserIps,
|
||||||
$admin_invite_create,
|
$admin_invite_create,
|
||||||
$admin_invite_list,
|
$admin_invite_list,
|
||||||
|
$admin_normalization,
|
||||||
$admin_promo_create,
|
$admin_promo_create,
|
||||||
$admin_queue_clear,
|
$admin_queue_clear,
|
||||||
$admin_queue_deliverDelayed,
|
$admin_queue_deliverDelayed,
|
||||||
|
@ -1260,6 +1263,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
||||||
$admin_getUserIps,
|
$admin_getUserIps,
|
||||||
$admin_invite_create,
|
$admin_invite_create,
|
||||||
$admin_invite_list,
|
$admin_invite_list,
|
||||||
|
$admin_normalization,
|
||||||
$admin_promo_create,
|
$admin_promo_create,
|
||||||
$admin_queue_clear,
|
$admin_queue_clear,
|
||||||
$admin_queue_deliverDelayed,
|
$admin_queue_deliverDelayed,
|
||||||
|
|
|
@ -64,6 +64,7 @@ import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js'
|
||||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||||
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
||||||
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
||||||
|
import * as ep___admin_normalization from './endpoints/admin/normalization.js';
|
||||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||||
|
@ -460,6 +461,7 @@ const eps = [
|
||||||
['admin/get-user-ips', ep___admin_getUserIps],
|
['admin/get-user-ips', ep___admin_getUserIps],
|
||||||
['admin/invite/create', ep___admin_invite_create],
|
['admin/invite/create', ep___admin_invite_create],
|
||||||
['admin/invite/list', ep___admin_invite_list],
|
['admin/invite/list', ep___admin_invite_list],
|
||||||
|
['admin/normalization', ep___admin_normalization],
|
||||||
['admin/promo/create', ep___admin_promo_create],
|
['admin/promo/create', ep___admin_promo_create],
|
||||||
['admin/queue/clear', ep___admin_queue_clear],
|
['admin/queue/clear', ep___admin_queue_clear],
|
||||||
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
||||||
|
|
139
packages/backend/src/server/api/endpoints/admin/normalization.ts
Normal file
139
packages/backend/src/server/api/endpoints/admin/normalization.ts
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import type { AbuseUserReportsRepository, FollowingsRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import type { MiUser, MiLocalUser } from '@/models/User.js';
|
||||||
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
|
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||||
|
import { RoleService } from '@/core/RoleService.js';
|
||||||
|
import { QueueService } from '@/core/QueueService.js';
|
||||||
|
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
|
||||||
|
import { InstanceActorService } from '@/core/InstanceActorService.js';
|
||||||
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['admin'],
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
kind: 'write:admin:suspend-user',
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
noSuchUser: {
|
||||||
|
message: 'No such user.',
|
||||||
|
code: 'NO_SUCH_USER',
|
||||||
|
id: '7cc4f851-e2f1-4621-9633-ec9e1d00c01e',
|
||||||
|
},
|
||||||
|
noModerator: {
|
||||||
|
message: 'Can\'t normalize user with moderator permission.',
|
||||||
|
code: 'NO_MODERATOR_NORMALIZATION',
|
||||||
|
id: '5b68a1d3-8ee3-4862-8294-6c7d2d2edd63',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
userId: { type: 'string', format: 'misskey:id' },
|
||||||
|
},
|
||||||
|
required: ['userId'],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.usersRepository)
|
||||||
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@Inject(DI.followingsRepository)
|
||||||
|
private followingsRepository: FollowingsRepository,
|
||||||
|
|
||||||
|
@Inject(DI.abuseUserReportsRepository)
|
||||||
|
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||||
|
|
||||||
|
private userSuspendService: UserSuspendService,
|
||||||
|
private roleService: RoleService,
|
||||||
|
private moderationLogService: ModerationLogService,
|
||||||
|
private queueService: QueueService,
|
||||||
|
private deleteAccountService: DeleteAccountService,
|
||||||
|
private instanceActorService: InstanceActorService,
|
||||||
|
private apRendererService: ApRendererService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (ps, me) => {
|
||||||
|
const user = await this.usersRepository.findOneBy({ id: ps.userId });
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
throw new ApiError(meta.errors.noSuchUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await this.roleService.isModerator(user)) {
|
||||||
|
throw new ApiError(meta.errors.noModerator);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.usersRepository.update(user.id, {
|
||||||
|
isSuspended: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.moderationLogService.log(me, 'normalize', {
|
||||||
|
userId: user.id,
|
||||||
|
userUsername: user.username,
|
||||||
|
userHost: user.host,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.resolveAllReports(user, me).catch(e => {});
|
||||||
|
await this.userSuspendService.doPostSuspend(user).catch(e => {});
|
||||||
|
await this.unFollowAll(user).catch(e => {});
|
||||||
|
await this.deleteAccountService.deleteAccount(user, true, me);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async resolveAllReports(user: MiUser, me: MiLocalUser) {
|
||||||
|
const reports = await this.abuseUserReportsRepository.findBy({ targetUserId: user.id });
|
||||||
|
|
||||||
|
for (const report of reports) {
|
||||||
|
if (report.targetUserHost != null) {
|
||||||
|
const actor = await this.instanceActorService.getInstanceActor();
|
||||||
|
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||||
|
|
||||||
|
this.queueService.deliver(actor, this.apRendererService.addContext(this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment)), targetUser.inbox, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.abuseUserReportsRepository.update(report.id, {
|
||||||
|
resolved: true,
|
||||||
|
assigneeId: me.id,
|
||||||
|
forwarded: user.host !== null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
private async unFollowAll(user: MiUser) {
|
||||||
|
const followings = await this.followingsRepository.findBy({
|
||||||
|
followerId: user.id,
|
||||||
|
});
|
||||||
|
const followers = await this.followingsRepository.findBy({
|
||||||
|
followeeId: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const followingPairs = await Promise.all(followings.map(f => Promise.all([
|
||||||
|
this.usersRepository.findOneByOrFail({ id: f.followerId }),
|
||||||
|
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
|
||||||
|
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
|
||||||
|
const followerPairs = await Promise.all(followers.map(f => Promise.all([
|
||||||
|
this.usersRepository.findOneByOrFail({ id: f.followerId }),
|
||||||
|
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
|
||||||
|
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
|
||||||
|
|
||||||
|
await this.queueService.createUnfollowJob(followingPairs.map(p => ({ from: p[0], to: p[1], silent: true })));
|
||||||
|
await this.queueService.createUnfollowJob(followerPairs.map(p => ({ from: p[0], to: p[1], silent: true })));
|
||||||
|
}
|
||||||
|
}
|
|
@ -99,6 +99,7 @@ export const moderationLogTypes = [
|
||||||
'unsetUserAvatar',
|
'unsetUserAvatar',
|
||||||
'unsetUserBanner',
|
'unsetUserBanner',
|
||||||
'unsetUserMutualLink',
|
'unsetUserMutualLink',
|
||||||
|
'normalize',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ModerationLogPayloads = {
|
export type ModerationLogPayloads = {
|
||||||
|
@ -333,7 +334,12 @@ export type ModerationLogPayloads = {
|
||||||
userId: string;
|
userId: string;
|
||||||
userUsername: string;
|
userUsername: string;
|
||||||
userMutualLinkSections: { name: string | null; mutualLinks: { id: string; url: string; fileId: string; description: string | null; imgSrc: string; }[]; }[] | []
|
userMutualLinkSections: { name: string | null; mutualLinks: { id: string; url: string; fileId: string; description: string | null; imgSrc: string; }[]; }[] | []
|
||||||
}
|
};
|
||||||
|
normalize: {
|
||||||
|
userId: string;
|
||||||
|
userUsername: string;
|
||||||
|
userHost: string | null;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Serialized<T> = {
|
export type Serialized<T> = {
|
||||||
|
|
|
@ -34,12 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkInput>
|
</MkInput>
|
||||||
<MkButton v-if="periodChanged" primary class="save" @click="saveRemovalCondition"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
<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()">
|
<MkSwitch v-model="noPiningNotes" @update:modelValue="saveRemovalCondition">
|
||||||
<template #label>{{ i18n.ts._autoRemoval.noPiningNotes }}</template>
|
<template #label>{{ i18n.ts._autoRemoval.noPiningNotes }}</template>
|
||||||
<template #caption>{{ i18n.ts._autoRemoval.noPiningNotesDescription }}</template>
|
<template #caption>{{ i18n.ts._autoRemoval.noPiningNotesDescription }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
<MkSwitch v-model="noSpecifiedNotes" @update:modelValue="saveRemovalCondition()">
|
<MkSwitch v-model="noSpecifiedNotes" @update:modelValue="saveRemovalCondition">
|
||||||
<template #label>{{ i18n.ts._autoRemoval.noSpecifiedNotes }}</template>
|
<template #label>{{ i18n.ts._autoRemoval.noSpecifiedNotes }}</template>
|
||||||
<template #caption>{{ i18n.ts._autoRemoval.noSpecifiedNotesDescription }}</template>
|
<template #caption>{{ i18n.ts._autoRemoval.noSpecifiedNotesDescription }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
@ -131,6 +131,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
<FormSection v-if="iAmModerator">
|
||||||
|
<template #label><i class="ti ti-aperture"></i> EZPZ User Normalization Menu™</template>
|
||||||
|
<template #description>{{ i18n.ts.normalizeDescription }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkInfo warn rounded>
|
||||||
|
{{ i18n.ts.thisIsExperimentalFeature }}
|
||||||
|
</MkInfo>
|
||||||
|
|
||||||
|
<MkSwitch v-model="mapleDirectorMode">
|
||||||
|
{{ i18n.ts.useNormalization }}
|
||||||
|
</MkSwitch>
|
||||||
|
</div>
|
||||||
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -141,6 +156,8 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import FormSection from '@/components/form/section.vue';
|
import FormSection from '@/components/form/section.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
import MkInfo from '@/components/MkInfo.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { iAmModerator, signinRequired } from '@/account.js';
|
import { iAmModerator, signinRequired } from '@/account.js';
|
||||||
|
@ -148,11 +165,9 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import { defaultStore } from '@/store.js';
|
import { defaultStore } from '@/store.js';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { unisonReload } from '@/scripts/unison-reload.js';
|
import { unisonReload } from '@/scripts/unison-reload.js';
|
||||||
import MkButton from "@/components/MkButton.vue";
|
|
||||||
import MkInput from "@/components/MkInput.vue";
|
|
||||||
|
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
const isVacation = ref<boolean | undefined>($i.isVacation !== null ? $i.isVacation : undefined);
|
const isVacation = ref<boolean>($i.isVacation ?? false);
|
||||||
const autoRemoval = ref<boolean>($i.autoRemovalCondition.active);
|
const autoRemoval = ref<boolean>($i.autoRemovalCondition.active);
|
||||||
const deleteAfter = ref<number>($i.autoRemovalCondition.deleteAfter || 7);
|
const deleteAfter = ref<number>($i.autoRemovalCondition.deleteAfter || 7);
|
||||||
const noPiningNotes = ref<boolean>($i.autoRemovalCondition.noPiningNotes);
|
const noPiningNotes = ref<boolean>($i.autoRemovalCondition.noPiningNotes);
|
||||||
|
@ -166,6 +181,7 @@ const hideDirectMessages = computed(defaultStore.makeGetterSetter('hideDirectMes
|
||||||
const hideDriveFileList = computed(defaultStore.makeGetterSetter('hideDriveFileList'));
|
const hideDriveFileList = computed(defaultStore.makeGetterSetter('hideDriveFileList'));
|
||||||
const hideModerationLog = computed(defaultStore.makeGetterSetter('hideModerationLog'));
|
const hideModerationLog = computed(defaultStore.makeGetterSetter('hideModerationLog'));
|
||||||
const hideRoleList = computed(defaultStore.makeGetterSetter('hideRoleList'));
|
const hideRoleList = computed(defaultStore.makeGetterSetter('hideRoleList'));
|
||||||
|
const mapleDirectorMode = computed(defaultStore.makeGetterSetter('mapleDirectorMode'));
|
||||||
|
|
||||||
function saveRemovalCondition() {
|
function saveRemovalCondition() {
|
||||||
misskeyApi('i/update-removal-condition', {
|
misskeyApi('i/update-removal-condition', {
|
||||||
|
@ -204,6 +220,7 @@ watch([
|
||||||
hideDriveFileList,
|
hideDriveFileList,
|
||||||
hideRoleList,
|
hideRoleList,
|
||||||
hideModerationLog,
|
hideModerationLog,
|
||||||
|
mapleDirectorMode,
|
||||||
], async () => {
|
], async () => {
|
||||||
await reloadAsk();
|
await reloadAsk();
|
||||||
});
|
});
|
||||||
|
|
|
@ -116,7 +116,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
async function userInfoUpdate() {
|
async function userInfoUpdate() {
|
||||||
os.apiWithDialog('federation/update-remote-user', {
|
await os.apiWithDialog('federation/update-remote-user', {
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function immediateUserNormalization() {
|
||||||
|
if (!await getConfirmed(i18n.ts.normalizeConfirm)) return;
|
||||||
|
|
||||||
|
await os.apiWithDialog('admin/normalization', {
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -344,6 +352,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
|
||||||
}]);
|
}]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($i && iAmModerator && defaultStore.state.mapleDirectorMode) {
|
||||||
|
menu = menu.concat([{ type: 'divider' }, {
|
||||||
|
icon: 'ti ti-aperture',
|
||||||
|
text: i18n.ts.normalize,
|
||||||
|
action: immediateUserNormalization,
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
if (user.host !== null) {
|
if (user.host !== null) {
|
||||||
menu = menu.concat([{ type: 'divider' }, {
|
menu = menu.concat([{ type: 'divider' }, {
|
||||||
icon: 'ti ti-refresh',
|
icon: 'ti ti-refresh',
|
||||||
|
|
|
@ -216,7 +216,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
filter: {
|
filter: {
|
||||||
withReplies: true,
|
withReplies: true,
|
||||||
withRenotes: true,
|
withRenotes: true,
|
||||||
withSensitive: true,
|
withSensitive: false,
|
||||||
onlyFiles: false,
|
onlyFiles: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -252,7 +252,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
animatedMfm: {
|
animatedMfm: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: true,
|
default: !window.matchMedia('(prefers-reduced-motion)').matches,
|
||||||
},
|
},
|
||||||
advancedMfm: {
|
advancedMfm: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
@ -288,7 +288,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
emojiStyle: {
|
emojiStyle: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 'twemoji', // twemoji / fluentEmoji / native
|
default: 'fluentEmoji', // twemoji / fluentEmoji / native
|
||||||
},
|
},
|
||||||
disableDrawer: {
|
disableDrawer: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
@ -436,7 +436,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
enableCondensedLineForAcct: {
|
enableCondensedLineForAcct: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
additionalUnicodeEmojiIndexes: {
|
additionalUnicodeEmojiIndexes: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
@ -448,7 +448,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
hideMutedNotes: {
|
hideMutedNotes: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
defaultWithReplies: {
|
defaultWithReplies: {
|
||||||
where: 'account',
|
where: 'account',
|
||||||
|
@ -473,7 +473,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
enableSeasonalScreenEffect: {
|
enableSeasonalScreenEffect: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
dropAndFusion: {
|
dropAndFusion: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
@ -488,7 +488,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
enableHorizontalSwipe: {
|
enableHorizontalSwipe: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: true,
|
default: false,
|
||||||
},
|
},
|
||||||
trustedExternalWebsites: {
|
trustedExternalWebsites: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
@ -504,7 +504,11 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
},
|
},
|
||||||
alwaysConfirmFollow: {
|
alwaysConfirmFollow: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: true,
|
default: false,
|
||||||
|
},
|
||||||
|
mapleDirectorMode: {
|
||||||
|
where: 'deviceAccount',
|
||||||
|
default: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
sound_masterVolume: {
|
sound_masterVolume: {
|
||||||
|
|
|
@ -253,6 +253,9 @@ type AdminInviteListResponse = operations['admin___invite___list']['responses'][
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json'];
|
type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type AdminNormalizationRequest = operations['admin___normalization']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
|
type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -1291,6 +1294,7 @@ declare namespace entities {
|
||||||
AdminInviteCreateResponse,
|
AdminInviteCreateResponse,
|
||||||
AdminInviteListRequest,
|
AdminInviteListRequest,
|
||||||
AdminInviteListResponse,
|
AdminInviteListResponse,
|
||||||
|
AdminNormalizationRequest,
|
||||||
AdminPromoCreateRequest,
|
AdminPromoCreateRequest,
|
||||||
AdminQueueDeliverDelayedResponse,
|
AdminQueueDeliverDelayedResponse,
|
||||||
AdminQueueInboxDelayedResponse,
|
AdminQueueInboxDelayedResponse,
|
||||||
|
|
|
@ -644,6 +644,17 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
|
||||||
|
*/
|
||||||
|
request<E extends 'admin/normalization', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
|
|
@ -78,6 +78,7 @@ import type {
|
||||||
AdminInviteCreateResponse,
|
AdminInviteCreateResponse,
|
||||||
AdminInviteListRequest,
|
AdminInviteListRequest,
|
||||||
AdminInviteListResponse,
|
AdminInviteListResponse,
|
||||||
|
AdminNormalizationRequest,
|
||||||
AdminPromoCreateRequest,
|
AdminPromoCreateRequest,
|
||||||
AdminQueueDeliverDelayedResponse,
|
AdminQueueDeliverDelayedResponse,
|
||||||
AdminQueueInboxDelayedResponse,
|
AdminQueueInboxDelayedResponse,
|
||||||
|
@ -649,6 +650,7 @@ export type Endpoints = {
|
||||||
'admin/get-user-ips': { req: AdminGetUserIpsRequest; res: AdminGetUserIpsResponse };
|
'admin/get-user-ips': { req: AdminGetUserIpsRequest; res: AdminGetUserIpsResponse };
|
||||||
'admin/invite/create': { req: AdminInviteCreateRequest; res: AdminInviteCreateResponse };
|
'admin/invite/create': { req: AdminInviteCreateRequest; res: AdminInviteCreateResponse };
|
||||||
'admin/invite/list': { req: AdminInviteListRequest; res: AdminInviteListResponse };
|
'admin/invite/list': { req: AdminInviteListRequest; res: AdminInviteListResponse };
|
||||||
|
'admin/normalization': { req: AdminNormalizationRequest; res: EmptyResponse };
|
||||||
'admin/promo/create': { req: AdminPromoCreateRequest; res: EmptyResponse };
|
'admin/promo/create': { req: AdminPromoCreateRequest; res: EmptyResponse };
|
||||||
'admin/queue/clear': { req: EmptyRequest; res: EmptyResponse };
|
'admin/queue/clear': { req: EmptyRequest; res: EmptyResponse };
|
||||||
'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse };
|
'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse };
|
||||||
|
|
|
@ -81,6 +81,7 @@ export type AdminInviteCreateRequest = operations['admin___invite___create']['re
|
||||||
export type AdminInviteCreateResponse = operations['admin___invite___create']['responses']['200']['content']['application/json'];
|
export type AdminInviteCreateResponse = operations['admin___invite___create']['responses']['200']['content']['application/json'];
|
||||||
export type AdminInviteListRequest = operations['admin___invite___list']['requestBody']['content']['application/json'];
|
export type AdminInviteListRequest = operations['admin___invite___list']['requestBody']['content']['application/json'];
|
||||||
export type AdminInviteListResponse = operations['admin___invite___list']['responses']['200']['content']['application/json'];
|
export type AdminInviteListResponse = operations['admin___invite___list']['responses']['200']['content']['application/json'];
|
||||||
|
export type AdminNormalizationRequest = operations['admin___normalization']['requestBody']['content']['application/json'];
|
||||||
export type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
|
export type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
|
||||||
export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json'];
|
export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json'];
|
||||||
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
|
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
|
||||||
|
|
|
@ -537,6 +537,15 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['admin___invite___list'];
|
post: operations['admin___invite___list'];
|
||||||
};
|
};
|
||||||
|
'/admin/normalization': {
|
||||||
|
/**
|
||||||
|
* admin/normalization
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
|
||||||
|
*/
|
||||||
|
post: operations['admin___normalization'];
|
||||||
|
};
|
||||||
'/admin/promo/create': {
|
'/admin/promo/create': {
|
||||||
/**
|
/**
|
||||||
* admin/promo/create
|
* admin/promo/create
|
||||||
|
@ -8828,6 +8837,58 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* admin/normalization
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *Yes* / **Permission**: *write:admin:suspend-user*
|
||||||
|
*/
|
||||||
|
admin___normalization: {
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
/** Format: misskey:id */
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description OK (without any results) */
|
||||||
|
204: {
|
||||||
|
content: never;
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* admin/promo/create
|
* admin/promo/create
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue