From d9927c027e9c616f05988f1f476fde7a51807c4d Mon Sep 17 00:00:00 2001 From: naskya Date: Fri, 25 Aug 2023 03:23:12 +0900 Subject: [PATCH] feat: emoji moderators (close #56) --- README.md | 7 +++ locales/en-US.yml | 7 +++ locales/ja-JP.yml | 7 +++ .../1692825433698-emoji-moderator.js | 17 ++++++++ .../src/model/entity/sea_orm_active_enums.rs | 17 ++++++++ .../native-utils/src/model/entity/user.rs | 3 ++ packages/backend/ormconfig.js | 2 +- packages/backend/src/models/entities/user.ts | 13 ++++++ .../backend/src/models/repositories/user.ts | 1 + packages/backend/src/server/api/endpoints.ts | 2 + .../endpoints/admin/emoji/add-aliases-bulk.ts | 24 +++++++++-- .../server/api/endpoints/admin/emoji/add.ts | 13 +++++- .../server/api/endpoints/admin/emoji/copy.ts | 15 ++++++- .../api/endpoints/admin/emoji/delete-bulk.ts | 16 ++++++- .../api/endpoints/admin/emoji/delete.ts | 13 +++++- .../api/endpoints/admin/emoji/import-zip.ts | 17 +++++++- .../api/endpoints/admin/emoji/list-remote.ts | 19 ++++++-- .../server/api/endpoints/admin/emoji/list.ts | 19 ++++++-- .../admin/emoji/remove-aliases-bulk.ts | 18 +++++++- .../endpoints/admin/emoji/set-aliases-bulk.ts | 28 ++++++++++-- .../admin/emoji/set-category-bulk.ts | 27 ++++++++++-- .../endpoints/admin/emoji/set-license-bulk.ts | 27 ++++++++++-- .../api/endpoints/admin/emoji/update.ts | 23 ++++++++-- .../endpoints/admin/set-emoji-moderator.ts | 43 +++++++++++++++++++ .../server/api/endpoints/admin/show-user.ts | 1 + packages/client/src/account.ts | 1 + packages/client/src/pages/user-info.vue | 34 +++++++++++++++ packages/client/src/router.ts | 14 +++--- .../src/ui/_common_/navbar-for-mobile.vue | 13 +++++- packages/client/src/ui/_common_/navbar.vue | 11 +++++ 30 files changed, 412 insertions(+), 40 deletions(-) create mode 100644 packages/backend/migration-neko/1692825433698-emoji-moderator.js create mode 100644 packages/backend/src/server/api/endpoints/admin/set-emoji-moderator.ts diff --git a/README.md b/README.md index b2311bb64..45ad509a9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ ## 主要な変更点 +- モデレーターでない一般ユーザーにもカスタム絵文字の管理権を与えられるように + - カスタム絵文字の管理が大変なサーバー管理者さんがたくさんいらっしゃったのでこの機能を追加するべきではないか他の開発者に訊いたところロール機能の実装を待つべきだと言われてしまったが、Firefish のロール機能は現状では仕様がまだ固まっておらず実装までに時間が掛かると考えられるため + - 以下の権限を与えられます + - 不許可: 絵文字の管理を許可しない + - 追加: 新しい絵文字の追加のみを許可する + - 追加と変更: 「追加」に加え、既存のカスタム絵文字の名前・カテゴリ・タグ・ライセンスの編集を許可する + - 全て許可:「追加と変更」に加え、既存のカスタム絵文字の削除を許可する - UI 用の言語とは別に、投稿翻訳に使用する言語を設定可能に - 投稿言語を自動検出して外国語の投稿に翻訳ボタンを表示する設定を追加 - モバイル表示の下部のウィジェットボタンを再読み込みボタンに変更可能に diff --git a/locales/en-US.yml b/locales/en-US.yml index d929c9fda..5035ec5af 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -2133,3 +2133,10 @@ _feeds: rss: "RSS" atom: "Atom" jsonFeed: "JSON feed" +emojiModPerm: "Custom emoji management" +emojiModPermDescription: "Add: Allow this user to add new custom emojis and to set tag/category/license to newly added custom emojis.\nAdd and Edit: \"Add\" Permission + Allow this user to edit the name/category/tag/license of the existing custom emojis.\nAllow All: \"Add and Edit\" Permission + Allow this user to delete existing custom emojis." +_emojiModPerm: + none: "Deny All" + add: "Add" + mod: "Add and Edit" + full: "Allow All" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index de600390b..90f1aa57d 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1989,3 +1989,10 @@ delete2faConfirm: これで、このアカウントの2要素認証は完全に inputNotMatch: 入力が一致しません deletePasskeysConfirm: これで、このアカウントのパスキーは完全に削除されます。続行しますか? confirm: "確認" +emojiModPerm: "カスタム絵文字の管理" +emojiModPermDescription: "追加: カスタム絵文字の新規追加と新規追加されたカスタム絵文字(正確には、タグとカテゴリとライセンスが設定されていないカスタム絵文字)へのタグとカテゴリとライセンスの設定を許可します。\n追加と変更:「追加」の権限に加え、既存の絵文字の名前・カテゴリ・タグ・ライセンスの変更を許可します。\n全て許可:「追加と変更」の権限に加え、既存のカスタム絵文字の削除を許可します。\nこの設定にかかわらず、サーバーの管理者およびモデレーターには「全て許可」の権限が与えられます。" +_emojiModPerm: + none: "不許可" + add: "追加" + mod: "追加と変更" + full: "全て許可" diff --git a/packages/backend/migration-neko/1692825433698-emoji-moderator.js b/packages/backend/migration-neko/1692825433698-emoji-moderator.js new file mode 100644 index 000000000..9ebcb72d5 --- /dev/null +++ b/packages/backend/migration-neko/1692825433698-emoji-moderator.js @@ -0,0 +1,17 @@ +export class EmojiModerator1692825433698 { + name = "EmojiModerator1692825433698"; + + async up(queryRunner) { + await queryRunner.query( + `CREATE TYPE "public"."user_emojimodperm_enum" AS ENUM('none', 'add', 'mod', 'full')`, + ); + await queryRunner.query( + `ALTER TABLE "user" ADD "emojiModPerm" "public"."user_emojimodperm_enum" NOT NULL DEFAULT 'none'`, + ); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "emojiModPerm"`); + await queryRunner.query(`DROP TYPE "public"."user_emojimodperm_enum"`); + } +} diff --git a/packages/backend/native-utils/src/model/entity/sea_orm_active_enums.rs b/packages/backend/native-utils/src/model/entity/sea_orm_active_enums.rs index f26995224..7f38837fd 100644 --- a/packages/backend/native-utils/src/model/entity/sea_orm_active_enums.rs +++ b/packages/backend/native-utils/src/model/entity/sea_orm_active_enums.rs @@ -168,6 +168,23 @@ pub enum RelayStatusEnum { Requesting, } #[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Default)] +#[sea_orm( + rs_type = "String", + db_type = "Enum", + enum_name = "user_emojimodperm_enum" +)] +pub enum UserEmojimodpermEnum { + #[sea_orm(string_value = "add")] + Add, + #[sea_orm(string_value = "full")] + Full, + #[sea_orm(string_value = "mod")] + Mod, + #[default] + #[sea_orm(string_value = "none")] + None, +} +#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum, Default)] #[sea_orm( rs_type = "String", db_type = "Enum", diff --git a/packages/backend/native-utils/src/model/entity/user.rs b/packages/backend/native-utils/src/model/entity/user.rs index e76ae08c7..6ffd2e335 100644 --- a/packages/backend/native-utils/src/model/entity/user.rs +++ b/packages/backend/native-utils/src/model/entity/user.rs @@ -1,5 +1,6 @@ //! `SeaORM` Entity. Generated by sea-orm-codegen 0.11.3 +use super::sea_orm_active_enums::UserEmojimodpermEnum; use sea_orm::entity::prelude::*; use super::newtype::StringVec; @@ -40,6 +41,8 @@ pub struct Model { pub is_bot: bool, #[sea_orm(column_name = "isCat")] pub is_cat: bool, + #[sea_orm(column_name = "emojiModPerm")] + pub emoji_mod_perm: UserEmojimodpermEnum, #[sea_orm(column_name = "isAdmin")] pub is_admin: bool, #[sea_orm(column_name = "isModerator")] diff --git a/packages/backend/ormconfig.js b/packages/backend/ormconfig.js index 5f85cead8..e35c5d147 100644 --- a/packages/backend/ormconfig.js +++ b/packages/backend/ormconfig.js @@ -11,5 +11,5 @@ export default new DataSource({ database: config.db.db, extra: config.db.extra, entities: entities, - migrations: ["migration/*.js"], + migrations: ["migration/*.js", "migration-neko/*.js"], }); diff --git a/packages/backend/src/models/entities/user.ts b/packages/backend/src/models/entities/user.ts index ddad9f3b2..7883ea6a9 100644 --- a/packages/backend/src/models/entities/user.ts +++ b/packages/backend/src/models/entities/user.ts @@ -9,6 +9,12 @@ import { import { id } from "../id.js"; import { DriveFile } from "./drive-file.js"; +// none: no permission +// add: add custom emojis to the server +// mod: add permission + modify {category, tags, license} of existing custom emojis +// full: mod permission + {rename, delete} existing custom emojis +export type EmojiModPerm = "none" | "add" | "mod" | "full"; + @Entity() @Index(["usernameLower", "host"], { unique: true }) export class User { @@ -178,6 +184,13 @@ export class User { }) public isModerator: boolean; + @Column({ + type: "enum", + enum: ["none", "add", "mod", "full"], + default: "none", + }) + public emojiModPerm: EmojiModPerm; + @Index() @Column("boolean", { default: true, diff --git a/packages/backend/src/models/repositories/user.ts b/packages/backend/src/models/repositories/user.ts index e7c4b6f00..caa631ab4 100644 --- a/packages/backend/src/models/repositories/user.ts +++ b/packages/backend/src/models/repositories/user.ts @@ -450,6 +450,7 @@ export const UserRepository = db.getRepository(User).extend({ avatarUrl: this.getAvatarUrlSync(user), avatarBlurhash: user.avatar?.blurhash || null, avatarColor: null, // 後方互換性のため + emojiModPerm: user.emojiModPerm ?? "none", isAdmin: user.isAdmin || falsy, isModerator: user.isModerator || falsy, isBot: user.isBot || falsy, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 8cc4fb2d7..58eff1ace 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -55,6 +55,7 @@ import * as ep___admin_search_indexAll from "./endpoints/admin/search/index-all. import * as ep___admin_sendEmail from "./endpoints/admin/send-email.js"; import * as ep___admin_sendModMail from "./endpoints/admin/send-mod-mail.js"; import * as ep___admin_serverInfo from "./endpoints/admin/server-info.js"; +import * as ep___admin_setEmojiModerator from "./endpoints/admin/set-emoji-moderator.js"; import * as ep___admin_showModerationLogs from "./endpoints/admin/show-moderation-logs.js"; import * as ep___admin_showUser from "./endpoints/admin/show-user.js"; import * as ep___admin_showUsers from "./endpoints/admin/show-users.js"; @@ -410,6 +411,7 @@ const eps = [ ["admin/send-email", ep___admin_sendEmail], ["admin/send-mod-mail", ep___admin_sendModMail], ["admin/server-info", ep___admin_serverInfo], + ["admin/set-emoji-moderator", ep___admin_setEmojiModerator], ["admin/show-moderation-logs", ep___admin_showModerationLogs], ["admin/show-user", ep___admin_showUser], ["admin/show-users", ep___admin_showUsers], diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts index 1ea457adf..2633c550b 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add-aliases-bulk.ts @@ -5,10 +5,18 @@ import { ApiError } from "../../../error.js"; import { db } from "@/db/postgre.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -31,11 +39,21 @@ export const paramDef = { required: ["ids", "aliases"], } as const; -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { + // require emoji mod permission (or add permission if there is no alias) + if (!(me.isAdmin || me.isModerator || me.emojiModPerm === "none")) + throw new ApiError(meta.errors.accessDenied); + const emojis = await Emojis.findBy({ id: In(ps.ids), }); + if (me.emojiModPerm === "add") { + for (const emoji of emojis) + if (emoji.aliases.length > 0) + throw new ApiError(meta.errors.accessDenied); + } + for (const emoji of emojis) { await Emojis.update(emoji.id, { updatedAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts index 4366406ec..7463e84aa 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/add.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/add.ts @@ -9,10 +9,10 @@ import { db } from "@/db/postgre.js"; import { getEmojiSize } from "@/misc/emoji-meta.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, errors: { noSuchFile: { @@ -20,6 +20,11 @@ export const meta = { code: "MO_SUCH_FILE", id: "fc46b5a4-6b92-4c33-ac66-b806659bb5cf", }, + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, }, } as const; @@ -32,6 +37,10 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { + // require emoji add permission + if (!(me.isAdmin || me.isModerator || me.emojiModPerm !== "none")) + throw new ApiError(meta.errors.accessDenied); + const file = await DriveFiles.findOneBy({ id: ps.fileId }); if (file == null) throw new ApiError(meta.errors.noSuchFile); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts index c90e60633..3143d70eb 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/copy.ts @@ -9,10 +9,10 @@ import { db } from "@/db/postgre.js"; import { getEmojiSize } from "@/misc/emoji-meta.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, errors: { noSuchEmoji: { @@ -20,6 +20,11 @@ export const meta = { code: "NO_SUCH_EMOJI", id: "e2785b66-dca3-4087-9cac-b93c541cc425", }, + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, }, res: { @@ -46,6 +51,12 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { + // require emoji mod permission + if ( + !(me.isAdmin || me.isModerator || ["mod", "full"].includes(me.emojiModPerm)) + ) + throw new ApiError(meta.errors.accessDenied); + const emoji = await Emojis.findOneBy({ id: ps.emojiId }); if (emoji == null) { diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts index 585af231f..d208cb730 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete-bulk.ts @@ -6,10 +6,18 @@ import { ApiError } from "../../../error.js"; import { db } from "@/db/postgre.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -27,6 +35,10 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { + // require emoji full permission + if (!(me.isAdmin || me.isModerator || me.emojiModPerm === "full")) + throw new ApiError(meta.errors.accessDenied); + const emojis = await Emojis.findBy({ id: In(ps.ids), }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts index 761c7c377..01807a755 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/delete.ts @@ -5,10 +5,10 @@ import { ApiError } from "../../../error.js"; import { db } from "@/db/postgre.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, errors: { noSuchEmoji: { @@ -16,6 +16,11 @@ export const meta = { code: "NO_SUCH_EMOJI", id: "be83669b-773a-44b7-b1f8-e5e5170ac3c2", }, + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, }, } as const; @@ -28,6 +33,10 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, me) => { + // require emoji full permission + if (!(me.isAdmin || me.isModerator || me.emojiModPerm === "full")) + throw new ApiError(meta.errors.accessDenied); + const emoji = await Emojis.findOneBy({ id: ps.id }); if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts index 6f49d6d18..7879d495d 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/import-zip.ts @@ -1,11 +1,22 @@ import define from "../../../define.js"; import { createImportCustomEmojisJob } from "@/queue/index.js"; +import { ApiError } from "../../../error.js"; import ms from "ms"; export const meta = { + tags: ["admin", "emoji"], + secure: true, requireCredential: true, - requireModerator: true, + requireModerator: false, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -17,5 +28,9 @@ export const paramDef = { } as const; export default define(meta, paramDef, async (ps, user) => { + // require emoji add permission + if (!(user.isAdmin || user.isModerator || user.emojiModPerm !== "none")) + throw new ApiError(meta.errors.accessDenied); + createImportCustomEmojisJob(user, ps.fileId); }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts index 6252e7e92..ab1eb9379 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list-remote.ts @@ -3,12 +3,13 @@ import { Emojis } from "@/models/index.js"; import { toPuny } from "@/misc/convert-host.js"; import { makePaginationQuery } from "../../../common/make-pagination-query.js"; import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { ApiError } from "../../../error.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, res: { type: "array", @@ -74,6 +75,14 @@ export const meta = { }, }, }, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -93,7 +102,11 @@ export const paramDef = { required: [], } as const; -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { + // require emoji add permission + if (!(me.isAdmin || me.isModerator || me.emojiModPerm !== "none")) + throw new ApiError(meta.errors.accessDenied); + const q = makePaginationQuery( Emojis.createQueryBuilder("emoji"), ps.sinceId, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts index f8269588c..1c36d78de 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/list.ts @@ -3,12 +3,13 @@ import { Emojis } from "@/models/index.js"; import { makePaginationQuery } from "../../../common/make-pagination-query.js"; import type { Emoji } from "@/models/entities/emoji.js"; //import { sqlLikeEscape } from "@/misc/sql-like-escape.js"; +import { ApiError } from "../../../error.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, res: { type: "array", @@ -74,6 +75,14 @@ export const meta = { }, }, }, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -87,7 +96,11 @@ export const paramDef = { required: [], } as const; -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { + // require emoji add permission + if (!(me.isAdmin || me.isModerator || me.emojiModPerm !== "none")) + throw new ApiError(meta.errors.accessDenied); + const q = makePaginationQuery( Emojis.createQueryBuilder("emoji"), ps.sinceId, diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts index 4e57fa3dd..d6cd62966 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/remove-aliases-bulk.ts @@ -8,7 +8,15 @@ export const meta = { tags: ["admin"], requireCredential: true, - requireModerator: true, + requireModerator: false, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -31,7 +39,13 @@ export const paramDef = { required: ["ids", "aliases"], } as const; -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { + // require emoji mod permission + if ( + !(me.isAdmin || me.isModerator || ["mod", "full"].includes(me.emojiModPerm)) + ) + throw new ApiError(meta.errors.accessDenied); + const emojis = await Emojis.findBy({ id: In(ps.ids), }); diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts index 1197f6077..f08e143b7 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-aliases-bulk.ts @@ -5,10 +5,18 @@ import { ApiError } from "../../../error.js"; import { db } from "@/db/postgre.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -31,7 +39,21 @@ export const paramDef = { required: ["ids", "aliases"], } as const; -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { + // require emoji mod permission (or add permission if there is no alias) + if (!(me.isAdmin || me.isModerator || me.emojiModPerm === "none")) + throw new ApiError(meta.errors.accessDenied); + + const emojis = await Emojis.findBy({ + id: In(ps.ids), + }); + + if (me.emojiModPerm === "add") { + for (const emoji of emojis) + if (emoji.aliases.length > 0) + throw new ApiError(meta.errors.accessDenied); + } + await Emojis.update( { id: In(ps.ids), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts index 17881a445..2831b7d8d 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-category-bulk.ts @@ -5,10 +5,18 @@ import { ApiError } from "../../../error.js"; import { db } from "@/db/postgre.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -30,7 +38,20 @@ export const paramDef = { required: ["ids"], } as const; -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { + // require emoji mod permission (or add permission if there is no category) + if (!(me.isAdmin || me.isModerator || me.emojiModPerm === "none")) + throw new ApiError(meta.errors.accessDenied); + + const emojis = await Emojis.findBy({ + id: In(ps.ids), + }); + + if (me.emojiModPerm === "add") { + for (const emoji of emojis) + if (emoji.category != null) throw new ApiError(meta.errors.accessDenied); + } + await Emojis.update( { id: In(ps.ids), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts index c98ca03fa..af5d02d26 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/set-license-bulk.ts @@ -5,10 +5,18 @@ import { ApiError } from "../../../error.js"; import { db } from "@/db/postgre.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, + + errors: { + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, + }, } as const; export const paramDef = { @@ -30,7 +38,20 @@ export const paramDef = { required: ["ids"], } as const; -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { + // require emoji mod permission (or add permission if there is no license) + if (!(me.isAdmin || me.isModerator || me.emojiModPerm === "none")) + throw new ApiError(meta.errors.accessDenied); + + const emojis = await Emojis.findBy({ + id: In(ps.ids), + }); + + if (me.emojiModPerm === "add") { + for (const emoji of emojis) + if (emoji.license != null) throw new ApiError(meta.errors.accessDenied); + } + await Emojis.update( { id: In(ps.ids), diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 9e2e85476..6c5b06e09 100644 --- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts +++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts @@ -4,10 +4,10 @@ import { ApiError } from "../../../error.js"; import { db } from "@/db/postgre.js"; export const meta = { - tags: ["admin"], + tags: ["admin", "emoji"], requireCredential: true, - requireModerator: true, + requireModerator: false, errors: { noSuchEmoji: { @@ -15,6 +15,11 @@ export const meta = { code: "NO_SUCH_EMOJI", id: "684dec9d-a8c2-4364-9aa8-456c49cb1dc8", }, + accessDenied: { + message: "Access denied.", + code: "ACCESS_DENIED", + id: "fe8d7103-0ea8-4ec3-814d-f8b401dc69e9", + }, }, } as const; @@ -42,10 +47,22 @@ export const paramDef = { required: ["id", "name", "aliases"], } as const; -export default define(meta, paramDef, async (ps) => { +export default define(meta, paramDef, async (ps, me) => { + // require emoji mod permission (or add permission if there is no category/alias/license) + if (!(me.isAdmin || me.isModerator || me.emojiModPerm !== "none")) + throw new ApiError(meta.errors.accessDenied); + const emoji = await Emojis.findOneBy({ id: ps.id }); if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji); + if (me.emojiModPerm === "add") { + if ( + emoji.category != null || + emoji.aliases.length > 0 || + emoji.license != null + ) + throw new ApiError(meta.errors.accessDenied); + } await Emojis.update(emoji.id, { updatedAt: new Date(), diff --git a/packages/backend/src/server/api/endpoints/admin/set-emoji-moderator.ts b/packages/backend/src/server/api/endpoints/admin/set-emoji-moderator.ts new file mode 100644 index 000000000..e963b5c6f --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/set-emoji-moderator.ts @@ -0,0 +1,43 @@ +import define from "../../define.js"; +import { Users } from "@/models/index.js"; +import { publishInternalEvent } from "@/services/stream.js"; + +export const meta = { + tags: ["admin"], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: "object", + properties: { + userId: { type: "string", format: "misskey:id" }, + emojiModPerm: { type: "string" }, + }, + required: ["userId", "emojiModPerm"], +} as const; + +export default define(meta, paramDef, async (ps) => { + const user = await Users.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error("user not found"); + } + + if (!["none", "add", "mod", "full"].includes(ps.emojiModPerm)) { + throw new Error("emojiModPerm must be 'none', 'add', 'mod', or 'full'"); + } + + const _emojiModPerm = + (ps.emojiModPerm as "none" | "add" | "mod" | "full") ?? "none"; + + await Users.update(user.id, { + emojiModPerm: _emojiModPerm, + }); + + publishInternalEvent("userChangeModeratorState", { + id: user.id, + isModerator: true, + }); +}); diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts index 7a2bf2365..b3ea3a649 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -73,6 +73,7 @@ export default define(meta, paramDef, async (ps, me) => { isModerator: user.isModerator, isSilenced: user.isSilenced, isSuspended: user.isSuspended, + emojiModPerm: user.emojiModPerm, lastActiveDate: user.lastActiveDate, moderationNote: profile.moderationNote, signins, diff --git a/packages/client/src/account.ts b/packages/client/src/account.ts index 9f48eff8c..b6260210f 100644 --- a/packages/client/src/account.ts +++ b/packages/client/src/account.ts @@ -19,6 +19,7 @@ export const $i = accountData export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator); export const iAmAdmin = $i?.isAdmin; +export const iAmEmojiMod = iAmModerator || $i?.emojiModPerm !== "none"; export async function signout() { waiting(); diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index 9e19996a7..0b0c78639 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -178,6 +178,29 @@
+ + + + + + + + {{ i18n.ts.emojiModPermDescription }} + import("./pages/admin-file.vue")) : page(() => import("./pages/not-found.vue")), }, + { + path: "/admin/emojis", + name: "emojis", + component: iAmEmojiMod + ? page(() => import("./pages/admin/emojis.vue")) + : page(() => import("./pages/not-found.vue")), + }, { path: "/admin", component: iAmModerator @@ -459,11 +466,6 @@ export const routes = [ name: "hashtags", component: page(() => import("./pages/admin/hashtags.vue")), }, - { - path: "/emojis", - name: "emojis", - component: page(() => import("./pages/admin/emojis.vue")), - }, { path: "/federation", name: "federation", diff --git a/packages/client/src/ui/_common_/navbar-for-mobile.vue b/packages/client/src/ui/_common_/navbar-for-mobile.vue index 9b616683b..75a258467 100644 --- a/packages/client/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/client/src/ui/_common_/navbar-for-mobile.vue @@ -80,6 +80,17 @@ {{ i18n.ts.controlPanel }} + + {{ i18n.ts.customEmojis }} +