Compare commits

...

6 Commits

Author SHA1 Message Date
github-actions[bot]
86b4f49880 Bump version to 2024.7.0-rc.7 2024-07-30 10:25:10 +00:00
syuilo
916ed49441 New translations ja-jp.yml (English) (#14327) 2024-07-30 19:20:37 +09:00
Sayamame-beans
d0b7c74fd1 検索からハッシュタグのページが開けるように、users/searchに@から始まる文字列が与えられた際の処理を修正 等 (#13858)
* enhance(frontend): 検索からハッシュタグのページを開けるように

* fix(frontend): 照会で入力が`#`のみの場合は`/tags/`に遷移しないように

* docs(changelog): update changelog

* enhance(frontend): ユーザー検索からもハッシュタグのページを開けるように

* docs(changelog): update changelog

* enhance(frontend): 検索範囲等が指定されている時は照会/ハッシュタグページを開かないように

* enhance(frontend): 検索内容に空白が含まれている場合は照会/ハッシュタグページを開かないように

* docs(changelog): update changelog

* Revert "enhance(frontend): 検索範囲等が指定されている時は照会/ハッシュタグページを開かないように"

This reverts commit f84eecea96.

* enhance(frontend): 検索から照会/ハッシュタグページを開くかどうか確認するように

* docs(changelog): update changelog

* chore: fix lint

* docs(changelog): update changelog insertion position

* enhance(frontend): 検索から`@user@host`の形式で照会出来るように

* fix(frontend): 照会で入力が`@`のみの場合に`/@`に遷移しないように

* fix(backend): `users/search`において`@`から始まるqueryに対する処理が正しくなかった問題を修正

* docs(changelog): update changelog

* chore(backend): fix lint error

* fix(backend): more improvements for users/search when query startswith `@`

* chore: unify common conditions

* docs(changelog): refine changelog

* chore(backend): fix lint error

* MkInputをpreventに対応させ、enterの意図せぬ伝搬を防ぐ

* chore(frontend/search.user): use .prevent to prevent the propagation of enter instead of setTimeout

---------

Co-authored-by: samunohito <46447427+samunohito@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Co-authored-by: taichanne30 <dev@taichan.site>
2024-07-30 19:18:43 +09:00
おさむのひと
2307849c9a fix(misskey-js): misskey-js.api.mdのコミット漏れ修正 (#14329) 2024-07-30 19:01:47 +09:00
おさむのひと
8bae2ecabd fix(misskey-js): モデログのフィルタに追加漏れがあったのを修正 (#14328) 2024-07-30 18:37:07 +09:00
syuilo
3411b9c16c Update CHANGELOG.md 2024-07-30 17:47:17 +09:00
13 changed files with 184 additions and 106 deletions

View File

@@ -13,6 +13,8 @@
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
- 翻訳の更新
- 依存関係の更新
### Client
- Feat: ユーザーページから「このユーザーのノートを検索」できるように (#14128)
@@ -28,6 +30,10 @@
- Enhance: AiScriptを0.19.0にアップデート
- Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`)
- Enhance: センシティブなメディアを開く際に確認ダイアログを出せるように
- Enhance: 検索(ノート/ユーザー)で `#` から始まる文字列を入力すると、そのハッシュタグのノート/ユーザー一覧ページが表示できるように
- Enhance: 検索(ノート/ユーザー)において、入力に空白が含まれている場合は照会を行わないように
- Enhance: 検索(ノート/ユーザー)において、照会を行うかどうか、ハッシュタグのノート/ユーザー一覧ページを表示するかどうかの確認ダイアログを出すように
- Enhance: 検索(ノート/ユーザー)で `@` から始まる文字列(`@user@host`など)を入力すると、そのユーザーを照会できるように
- Enhance: ドライブのファイル・フォルダをドラッグしなくても移動できるように
(Cherry-picked from https://github.com/nafu-at/misskey/commit/b89c2af6945c6a9f9f10e83f54d2bcf0f240b0b4, https://github.com/nafu-at/misskey/commit/8a7d710c6acb83f50c83f050bd1423c764d60a99)
- Enhance: デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
@@ -55,6 +61,8 @@
- Fix: ダイレクト投稿の"削除して編集"において、宛先が保持されていなかった問題を修正
- Fix: 投稿フォームへのURL貼り付けによる引用が下書きに保存されていなかった問題を修正
- Fix: "削除して編集"や下書きにおいて、リアクションの受け入れ設定が保持/保存されていなかった問題を修正
- Fix: 照会に `#` から始まる文字列を入力してそのハッシュタグのページを表示する際、入力が `#` のみの場合に「指定されたURLに該当するページはありませんでした。」が表示されてしまう問題を修正
- Fix: 照会に `@` から始まる文字列を入力してユーザーを照会する際、入力が `@` のみの場合に「問題が発生しました」が表示されてしまう問題を修正
- Fix: 投稿フォームにートのURLを貼り付けて"引用として添付"した場合、投稿文を空にすることによるRenote化が出来なかった問題を修正
### Server
@@ -94,6 +102,8 @@
- Fix: リノートのミュートが適用されるまでに時間がかかることがある問題を修正
(Cherry-picked from https://github.com/Type4ny-Project/Type4ny/commit/e9601029b52e0ad43d9131b555b614e56c84ebc1)
- Fix: Steaming APIが不正なデータを受けた場合の動作が不安定である問題 #14251
- Fix: `users/search`において `@` から始まる文字列が与えられた際の処理が正しくなかった問題を修正
- 名前や自己紹介に `@` から始まる文言が含まれるユーザーも検索できるようになります
- Fix: 一部のMisskey以外のソフトウェアからファイルを受け取れない問題
(Cherry-picked from https://github.com/Secineralyr/misskey.dream/pull/73/commits/652eaff1e8aa00b890d71d2e1e52c263c1e67c76)
- NOTE: `drive_file``url`, `uri`, `src`の上限が512から1024に変更されます

View File

@@ -60,6 +60,7 @@ copyFileId: "Copy file ID"
copyFolderId: "Copy folder ID"
copyProfileUrl: "Copy profile URL"
searchUser: "Search for a user"
searchThisUsersNotes: "Search this users notes"
reply: "Reply"
loadMore: "Load more"
showMore: "Show more"
@@ -154,6 +155,7 @@ editList: "Edit list"
selectChannel: "Select a channel"
selectAntenna: "Select an antenna"
editAntenna: "Edit antenna"
createAntenna: "Create an antenna"
selectWidget: "Select a widget"
editWidgets: "Edit widgets"
editWidgetsExit: "Done"
@@ -194,6 +196,7 @@ followConfirm: "Are you sure that you want to follow {name}?"
proxyAccount: "Proxy account"
proxyAccountDescription: "A proxy account is an account that acts as a remote follower for users under certain conditions. For example, when a user adds a remote user to the list, the remote user's activity will not be delivered to the instance if no local user is following that user, so the proxy account will follow instead."
host: "Host"
selectSelf: "Select myself"
selectUser: "Select a user"
recipient: "Recipient"
annotation: "Comments"
@@ -1106,6 +1109,8 @@ preservedUsernames: "Reserved usernames"
preservedUsernamesDescription: "List usernames to reserve separated by linebreaks. These will become unable during normal account creation, but can be used by administrators to manually create accounts. Already existing accounts using these usernames will not be affected."
createNoteFromTheFile: "Compose note from this file"
archive: "Archive"
archived: "Archived"
unarchive: "Unarchive"
channelArchiveConfirmTitle: "Really archive {name}?"
channelArchiveConfirmDescription: "An archived channel won't appear in the channel list or search results anymore. New posts can also not be added to it anymore."
thisChannelArchived: "This channel has been archived."
@@ -1116,6 +1121,7 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)"
preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored."
options: "Options"
specifyUser: "Specific user"
specifyHost: "Specify a host"
failedToPreviewUrl: "Could not preview"
update: "Update"
rolesThatCanBeUsedThisEmojiAsReaction: "Roles that can use this emoji as reaction"
@@ -1250,6 +1256,8 @@ inquiry: "Contact"
tryAgain: "Please try again later"
confirmWhenRevealingSensitiveMedia: "Confirm when revealing sensitive media"
sensitiveMediaRevealConfirm: "This might be a sensitive media. Are you sure to reveal?"
createdLists: "Created lists"
createdAntennas: "Created antennas"
_delivery:
status: "Delivery status"
stop: "Suspended"
@@ -2425,6 +2433,7 @@ _webhookSettings:
_systemEvents:
abuseReport: "When received a new abuse report"
abuseReportResolved: "When resolved abuse reports"
userCreated: "When user is created"
deleteConfirm: "Are you sure you want to delete the Webhook?"
_abuseReport:
_notificationRecipient:
@@ -2615,3 +2624,8 @@ _mediaControls:
pip: "Picture in Picture"
playbackRate: "Playback Speed"
loop: "Loop playback"
_contextMenu:
title: "Context menu"
app: "Application"
appWithShift: "Application with shift key"
native: "Native"

8
locales/index.d.ts vendored
View File

@@ -4500,6 +4500,14 @@ export interface Locale extends ILocale {
* ユーザー指定
*/
"specifyUser": string;
/**
* 照会しますか?
*/
"lookupConfirm": string;
/**
* ハッシュタグのページを開きますか?
*/
"openTagPageConfirm": string;
/**
* ホスト指定
*/

View File

@@ -1121,6 +1121,8 @@ preventAiLearning: "生成AIによる学習を拒否"
preventAiLearningDescription: "外部の文章生成AIや画像生成AIに対して、投稿したートや画像などのコンテンツを学習の対象にしないように要求します。これはnoaiフラグをHTMLレスポンスに含めることによって実現されますが、この要求に従うかはそのAI次第であるため、学習を完全に防止するものではありません。"
options: "オプション"
specifyUser: "ユーザー指定"
lookupConfirm: "照会しますか?"
openTagPageConfirm: "ハッシュタグのページを開きますか?"
specifyHost: "ホスト指定"
failedToPreviewUrl: "プレビューできません"
update: "更新"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.7.0-rc.6",
"version": "2024.7.0-rc.7",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@@ -57,88 +57,66 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const activeThreshold = new Date(Date.now() - (1000 * 60 * 60 * 24 * 30)); // 30日
ps.query = ps.query.trim();
const isUsername = ps.query.startsWith('@');
const isUsername = ps.query.startsWith('@') && !ps.query.includes(' ') && ps.query.indexOf('@', 1) === -1;
let users: MiUser[] = [];
if (isUsername) {
const usernameQuery = this.usersRepository.createQueryBuilder('user')
.where('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' })
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
usernameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
usernameQuery.andWhere('user.host IS NOT NULL');
}
users = await usernameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
} else {
const nameQuery = this.usersRepository.createQueryBuilder('user')
.where(new Brackets(qb => {
qb.where('user.name ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
// Also search username if it qualifies as username
if (this.userEntityService.validateLocalUsername(ps.query)) {
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
if (isUsername) {
qb.orWhere('user.usernameLower LIKE :username', { username: sqlLikeEscape(ps.query.replace('@', '').toLowerCase()) + '%' });
} else if (this.userEntityService.validateLocalUsername(ps.query)) { // Also search username if it qualifies as username
qb.orWhere('user.usernameLower LIKE :username', { username: '%' + sqlLikeEscape(ps.query.toLowerCase()) + '%' });
}
}))
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE');
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
if (ps.origin === 'local') {
nameQuery.andWhere('user.host IS NULL');
} else if (ps.origin === 'remote') {
nameQuery.andWhere('user.host IS NOT NULL');
}
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
users = await nameQuery
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany();
if (users.length < ps.limit) {
const profQuery = this.userProfilesRepository.createQueryBuilder('prof')
.select('prof.userId')
.where('prof.description ILIKE :query', { query: '%' + sqlLikeEscape(ps.query) + '%' });
if (ps.origin === 'local') {
profQuery.andWhere('prof.userHost IS NULL');
} else if (ps.origin === 'remote') {
profQuery.andWhere('prof.userHost IS NOT NULL');
}
const query = this.usersRepository.createQueryBuilder('user')
.where(`user.id IN (${ profQuery.getQuery() })`)
.andWhere(new Brackets(qb => {
qb
.where('user.updatedAt IS NULL')
.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
}))
.andWhere('user.isSuspended = FALSE')
.setParameters(profQuery.getParameters());
users = users.concat(await query
.orderBy('user.updatedAt', 'DESC', 'NULLS LAST')
.limit(ps.limit)
.offset(ps.offset)
.getMany(),
);
}
return await this.userEntityService.packMany(users, me, { schema: ps.detail ? 'UserDetailed' : 'UserLite' });

View File

@@ -79,7 +79,7 @@ const props = defineProps<{
const emit = defineEmits<{
(ev: 'change', _ev: KeyboardEvent): void;
(ev: 'keydown', _ev: KeyboardEvent): void;
(ev: 'enter'): void;
(ev: 'enter', _ev: KeyboardEvent): void;
(ev: 'update:modelValue', value: string | number): void;
}>();
@@ -111,7 +111,7 @@ const onKeydown = (ev: KeyboardEvent) => {
emit('keydown', ev);
if (ev.code === 'Enter') {
emit('enter');
emit('enter', ev);
}
};

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkFoldableSection :expanded="true">
@@ -143,25 +143,55 @@ async function search() {
if (query == null || query === '') return;
//#region AP lookup
if (query.startsWith('https://')) {
const promise = misskeyApi('ap/show', {
uri: query,
if (query.startsWith('https://') && !query.includes(' ')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.lookupConfirm,
});
if (!confirm.canceled) {
const promise = misskeyApi('ap/show', {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
const res = await promise;
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
}
return;
}
return;
}
//#endregion
if (query.length > 1 && !query.includes(' ')) {
if (query.startsWith('@')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.lookupConfirm,
});
if (!confirm.canceled) {
router.push(`/${query}`);
return;
}
}
if (query.startsWith('#')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.openTagPageConfirm,
});
if (!confirm.canceled) {
router.push(`/tags/${encodeURIComponent(query.substring(1))}`);
return;
}
}
}
notePagination.value = {
endpoint: 'notes/search',
limit: 10,

View File

@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div class="_gaps">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter="search">
<MkInput v-model="searchQuery" :large="true" :autofocus="true" type="search" @enter.prevent="search">
<template #prefix><i class="ti ti-search"></i></template>
</MkInput>
<MkRadios v-model="searchOrigin" @update:modelValue="search()">
@@ -39,8 +39,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { useRouter } from '@/router/supplier.js';
const props = withDefaults(defineProps<{
query?: string,
origin?: Endpoints['users/search']['req']['origin'],
query?: string,
origin?: Endpoints['users/search']['req']['origin'],
}>(), {
query: '',
origin: 'combined',
@@ -59,25 +59,55 @@ async function search() {
if (query == null || query === '') return;
//#region AP lookup
if (query.startsWith('https://')) {
const promise = misskeyApi('ap/show', {
uri: query,
if (query.startsWith('https://') && !query.includes(' ')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.lookupConfirm,
});
if (!confirm.canceled) {
const promise = misskeyApi('ap/show', {
uri: query,
});
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
const res = await promise;
const res = await promise;
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
if (res.type === 'User') {
router.push(`/@${res.object.username}@${res.object.host}`);
} else if (res.type === 'Note') {
router.push(`/notes/${res.object.id}`);
}
return;
}
return;
}
//#endregion
if (query.length > 1 && !query.includes(' ')) {
if (query.startsWith('@')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.lookupConfirm,
});
if (!confirm.canceled) {
router.push(`/${query}`);
return;
}
}
if (query.startsWith('#')) {
const confirm = await os.confirm({
type: 'info',
text: i18n.ts.openTagPageConfirm,
});
if (!confirm.canceled) {
router.push(`/user-tags/${encodeURIComponent(query.substring(1))}`);
return;
}
}
}
userPagination.value = {
endpoint: 'users/search',
limit: 10,

View File

@@ -16,7 +16,7 @@ export async function lookup(router?: Router) {
title: i18n.ts.lookup,
});
const query = temp ? temp.trim() : '';
if (canceled) return;
if (canceled || query.length <= 1) return;
if (query.startsWith('@') && !query.includes(' ')) {
_router.push(`/${query}`);

View File

@@ -2481,7 +2481,7 @@ type ModerationLog = {
});
// @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"];
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient"];
// @public (undocumented)
type MuteCreateRequest = operations['mute___create']['requestBody']['content']['application/json'];

View File

@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.7.0-rc.6",
"version": "2024.7.0-rc.7",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@@ -143,6 +143,12 @@ export const moderationLogTypes = [
'deleteAvatarDecoration',
'unsetUserAvatar',
'unsetUserBanner',
'createSystemWebhook',
'updateSystemWebhook',
'deleteSystemWebhook',
'createAbuseReportNotificationRecipient',
'updateAbuseReportNotificationRecipient',
'deleteAbuseReportNotificationRecipient',
] as const;
// See: packages/backend/src/core/ReversiService.ts@L410