feat: 안되겠소, 쏩시다!

This commit is contained in:
オスカー、 2024-11-06 17:53:25 +09:00
parent 3e90f8995e
commit cda3d93db8
No known key found for this signature in database
GPG key ID: 139D6573F92DA9F7
16 changed files with 322 additions and 20 deletions

View file

@ -1287,6 +1287,10 @@ autoRemoval: "Automatic note deletion"
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."
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:
howToPlay: "How to play"
hold: "Hold"
@ -2578,6 +2582,7 @@ _moderationLogTypes:
deleteAvatarDecoration: "Avatar decoration deleted"
unsetUserAvatar: "Unset this user's avatar"
unsetUserBanner: "Unset this user's banner"
normalize: "Normalization"
_fileViewer:
title: "File details"
type: "File type"

20
locales/index.d.ts vendored
View file

@ -5312,6 +5312,22 @@ export interface Locale extends ILocale {
*
*/
"changeUserName": string;
/**
*
*/
"normalize": string;
/**
*
*/
"normalizeConfirm": string;
/**
* 使
*/
"normalizeDescription": string;
/**
* 使
*/
"useNormalization": string;
"_bubbleGame": {
/**
*
@ -10283,6 +10299,10 @@ export interface Locale extends ILocale {
*
*/
"unsetUserBanner": string;
/**
*
*/
"normalize": string;
};
"_fileViewer": {
/**

View file

@ -1322,6 +1322,10 @@ dangerZone: "危険区域"
dangerZoneDescription: "以下の機能を利用する際は、特にご注意ください。"
checkedByHIBP: "パスワードの安全性に加え、HIBPを通じてパスワードの漏洩を検査します。"
changeUserName: "名前を変更"
normalize: "正常化"
normalizeConfirm: "正常化すると元に戻せなくなり、これはアカウントの削除と同様の効力を持ちます。実行しますか?"
normalizeDescription: "正常化は、ユーザーの一括的なデータ抹消やアカウント制裁が必要な場合に使用する機能です。アカウントを正常化した後は取り返しのつかないことに留意してください。"
useNormalization: "正規化機能を使用する"
_bubbleGame:
howToPlay: "遊び方"
@ -2717,6 +2721,7 @@ _moderationLogTypes:
deleteAvatarDecoration: "アイコンデコレーションを削除"
unsetUserAvatar: "ユーザーのアイコンを解除"
unsetUserBanner: "ユーザーのバナーを解除"
normalize: "正 常 化"
_fileViewer:
title: "ファイルの詳細"

View file

@ -610,12 +610,12 @@ removeAllFollowingDescription: "{host} 서버의 모든 팔로잉을 해제합
userSuspended: "이 계정은 정지된 상태입니다."
userLimited: "이 계정은 제한된 상태입니다."
userSilenced: "이 계정은 사일런스된 상태입니다."
yourAccountSuspendedTitle: "계정이 정지되었습니다"
yourAccountSuspendedDescription: "이 계정은 서버의 이용 약관을 위반하거나, 기타 다른 이유로 인해 정지되었습니다. 자세한 사항은 관리자에게 문의해 주십시오. 계정을 새로 생성하지 마십시오."
yourAccountSuspendedTitle: "당신의 계정이 정지되었습니다"
yourAccountSuspendedDescription: "당신의 계정이 규정 위반 또는 관리자 재량에 의해 정지되었습니다. 잘못되었다고 생각할 경우 관리자에게 문의해주십시오."
tokenRevoked: "유효하지 않은 토큰입니다"
tokenRevokedDescription: "로그인 토큰이 비활성화되었습니다. 다시 로그인하여 주십시오."
accountDeleted: "계정이 정지되었습니다"
accountDeletedDescription: "이 계정이 삭제되었습니다."
tokenRevokedDescription: "다른 곳에서 비밀번호를 변경했거나, 다른 모든 세션을 종료했습니다. 계속하려면 다시 로그인하세요."
accountDeleted: "당신의 계정이 정지되었습니다"
accountDeletedDescription: "당신의 계정이 일정 기간 이상 비활동 또는 관리자 재량에 의해 삭제되었습니다."
menu: "메뉴"
divider: "구분선"
addItem: "항목 추가"
@ -1310,6 +1310,10 @@ dangerZoneDescription: "함부로 실행하면 어딘가 고장날 수 있는
checkedByHIBP: "비밀번호의 안전성과 더불어, HIBP를 통해 비밀번호 유출을 검사합니다."
changeUserName: "이름 변경"
pleaseSelectAccount: "사용할 계정을 선택해주십시오"
normalize: "정상화"
normalizeConfirm: "정상화 이후에는 계정을 되돌릴 수 없게 됩니다. 실행하시겠습니까?"
normalizeDescription: "정상화는 유저의 일괄적인 데이터 말소 및 계정 정지를 위한 기능으로, 계정 삭제와 비슷한 효과를 가지며, 실행 후에는 모든 신고가 닫히게 됩니다. 기능을 실행하고 나면 되돌릴 수 없는 점을 유의하시기 바랍니다."
useNormalization: "정상화 메뉴를 표시하기"
_bubbleGame:
howToPlay: "설명"
hold: "홀드"
@ -2609,6 +2613,7 @@ _moderationLogTypes:
deleteAvatarDecoration: "아바타 장식 삭제"
unsetUserAvatar: "유저 아바타 제거"
unsetUserBanner: "유저 배너 제거"
normalize: "정 상 화"
_fileViewer:
title: "파일 상세"
type: "파일 유형"

View file

@ -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_invite_create from './endpoints/admin/invite/create.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_queue_clear from './endpoints/admin/queue/clear.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_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_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_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 };
@ -864,6 +866,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_getUserIps,
$admin_invite_create,
$admin_invite_list,
$admin_normalization,
$admin_promo_create,
$admin_queue_clear,
$admin_queue_deliverDelayed,
@ -1260,6 +1263,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_getUserIps,
$admin_invite_create,
$admin_invite_list,
$admin_normalization,
$admin_promo_create,
$admin_queue_clear,
$admin_queue_deliverDelayed,

View file

@ -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_invite_create from './endpoints/admin/invite/create.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_queue_clear from './endpoints/admin/queue/clear.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/invite/create', ep___admin_invite_create],
['admin/invite/list', ep___admin_invite_list],
['admin/normalization', ep___admin_normalization],
['admin/promo/create', ep___admin_promo_create],
['admin/queue/clear', ep___admin_queue_clear],
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],

View 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 })));
}
}

View file

@ -99,6 +99,7 @@ export const moderationLogTypes = [
'unsetUserAvatar',
'unsetUserBanner',
'unsetUserMutualLink',
'normalize',
] as const;
export type ModerationLogPayloads = {
@ -333,7 +334,12 @@ export type ModerationLogPayloads = {
userId: string;
userUsername: 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> = {

View file

@ -34,12 +34,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</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()">
<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()">
<MkSwitch v-model="noSpecifiedNotes" @update:modelValue="saveRemovalCondition">
<template #label>{{ i18n.ts._autoRemoval.noSpecifiedNotes }}</template>
<template #caption>{{ i18n.ts._autoRemoval.noSpecifiedNotesDescription }}</template>
</MkSwitch>
@ -131,6 +131,21 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</div>
</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>
</template>
@ -141,6 +156,8 @@ import MkSwitch from '@/components/MkSwitch.vue';
import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.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 { i18n } from '@/i18n.js';
import { iAmModerator, signinRequired } from '@/account.js';
@ -148,11 +165,9 @@ 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 isVacation = ref<boolean>($i.isVacation ?? false);
const autoRemoval = ref<boolean>($i.autoRemovalCondition.active);
const deleteAfter = ref<number>($i.autoRemovalCondition.deleteAfter || 7);
const noPiningNotes = ref<boolean>($i.autoRemovalCondition.noPiningNotes);
@ -166,6 +181,7 @@ const hideDirectMessages = computed(defaultStore.makeGetterSetter('hideDirectMes
const hideDriveFileList = computed(defaultStore.makeGetterSetter('hideDriveFileList'));
const hideModerationLog = computed(defaultStore.makeGetterSetter('hideModerationLog'));
const hideRoleList = computed(defaultStore.makeGetterSetter('hideRoleList'));
const mapleDirectorMode = computed(defaultStore.makeGetterSetter('mapleDirectorMode'));
function saveRemovalCondition() {
misskeyApi('i/update-removal-condition', {
@ -204,6 +220,7 @@ watch([
hideDriveFileList,
hideRoleList,
hideModerationLog,
mapleDirectorMode,
], async () => {
await reloadAsk();
});

View file

@ -116,7 +116,15 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
}
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,
});
}
@ -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) {
menu = menu.concat([{ type: 'divider' }, {
icon: 'ti ti-refresh',

View file

@ -216,7 +216,7 @@ export const defaultStore = markRaw(new Storage('base', {
filter: {
withReplies: true,
withRenotes: true,
withSensitive: true,
withSensitive: false,
onlyFiles: false,
},
},
@ -252,7 +252,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
animatedMfm: {
where: 'device',
default: true,
default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
advancedMfm: {
where: 'device',
@ -288,7 +288,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
emojiStyle: {
where: 'device',
default: 'twemoji', // twemoji / fluentEmoji / native
default: 'fluentEmoji', // twemoji / fluentEmoji / native
},
disableDrawer: {
where: 'device',
@ -436,7 +436,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
enableCondensedLineForAcct: {
where: 'device',
default: false,
default: true,
},
additionalUnicodeEmojiIndexes: {
where: 'device',
@ -448,7 +448,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
hideMutedNotes: {
where: 'device',
default: false,
default: true,
},
defaultWithReplies: {
where: 'account',
@ -473,7 +473,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
enableSeasonalScreenEffect: {
where: 'device',
default: false,
default: true,
},
dropAndFusion: {
where: 'device',
@ -488,7 +488,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
enableHorizontalSwipe: {
where: 'device',
default: true,
default: false,
},
trustedExternalWebsites: {
where: 'device',
@ -504,7 +504,11 @@ export const defaultStore = markRaw(new Storage('base', {
},
alwaysConfirmFollow: {
where: 'device',
default: true,
default: false,
},
mapleDirectorMode: {
where: 'deviceAccount',
default: false,
},
sound_masterVolume: {

View file

@ -253,6 +253,9 @@ type AdminInviteListResponse = operations['admin___invite___list']['responses'][
// @public (undocumented)
type AdminMetaResponse = operations['admin___meta']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminNormalizationRequest = operations['admin___normalization']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminPromoCreateRequest = operations['admin___promo___create']['requestBody']['content']['application/json'];
@ -1291,6 +1294,7 @@ declare namespace entities {
AdminInviteCreateResponse,
AdminInviteListRequest,
AdminInviteListResponse,
AdminNormalizationRequest,
AdminPromoCreateRequest,
AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse,

View file

@ -644,6 +644,17 @@ declare module '../api.js' {
credential?: string | null,
): 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.
*

View file

@ -78,6 +78,7 @@ import type {
AdminInviteCreateResponse,
AdminInviteListRequest,
AdminInviteListResponse,
AdminNormalizationRequest,
AdminPromoCreateRequest,
AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse,
@ -649,6 +650,7 @@ export type Endpoints = {
'admin/get-user-ips': { req: AdminGetUserIpsRequest; res: AdminGetUserIpsResponse };
'admin/invite/create': { req: AdminInviteCreateRequest; res: AdminInviteCreateResponse };
'admin/invite/list': { req: AdminInviteListRequest; res: AdminInviteListResponse };
'admin/normalization': { req: AdminNormalizationRequest; res: EmptyResponse };
'admin/promo/create': { req: AdminPromoCreateRequest; res: EmptyResponse };
'admin/queue/clear': { req: EmptyRequest; res: EmptyResponse };
'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse };

View file

@ -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 AdminInviteListRequest = operations['admin___invite___list']['requestBody']['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 AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json'];
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];

View file

@ -537,6 +537,15 @@ export type paths = {
*/
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
@ -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
* @description No description provided.