feat: emoji moderators
This commit is contained in:
parent
dce911613e
commit
52ebc2d8dc
31 changed files with 434 additions and 38 deletions
|
@ -2,6 +2,14 @@
|
|||
|
||||
Breaking changes are indicated by the :warning: icon.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Added `admin/set-emoji-moderator` endpoint, where moderators can give these permissions to regular users:
|
||||
- `add`: Add new custom emojis, set tag/category/license to newly added custom emojis
|
||||
- `mod`: `add` permission + edit the name/category/tag/license of the existing custom emojis
|
||||
- `full`: `mod` permission + delete existing custom emojis
|
||||
- Emoji moderators are able to access to the endpoints under `admin/emoji/`
|
||||
|
||||
## v20240217
|
||||
|
||||
- :warning: Since the auto NSFW media detection has been removed, these endpoints are affected:
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
Critical security updates are indicated by the :warning: icon.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Add the ability to give regular (non-moderator) users permission to manage custom emojis
|
||||
|
||||
## :warning: v20240217-1
|
||||
|
||||
- Fix a [security issue](https://github.com/misskey-dev/misskey/security/advisories/GHSA-qqrm-9grj-6v32)
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
BEGIN;
|
||||
|
||||
DELETE FROM "migrations" WHERE name IN (
|
||||
'EmojiModerator1692825433698',
|
||||
'RemoveNsfwDetection1705848938166',
|
||||
'FirefishUrlMove1707850084123',
|
||||
'RemoveNativeUtilsMigration1705877093218'
|
||||
);
|
||||
|
||||
-- emoji-moderator
|
||||
ALTER TABLE "user" DROP COLUMN "emojiModPerm";
|
||||
DROP TYPE "public"."user_emojimodperm_enum";
|
||||
|
||||
-- remove-nsfw-detection
|
||||
ALTER TABLE "user_profile" ADD "autoSensitive" boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE "meta" ADD "enableSensitiveMediaDetectionForVideos" boolean NOT NULL DEFAULT false;
|
||||
|
|
|
@ -1170,6 +1170,13 @@ replaceWidgetsButtonWithReloadButton: "Replace widgets button with reload button
|
|||
searchEngine: "Search engine used in search bar MFM"
|
||||
postSearch: "Post search on this server"
|
||||
showBigPostButton: "Show a huge post button on the posting form"
|
||||
emojiModPerm: "Custom emoji management permission"
|
||||
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:
|
||||
unauthorized: "None"
|
||||
add: "Add"
|
||||
mod: "Add and Edit"
|
||||
full: "Allow All"
|
||||
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing
|
||||
|
|
|
@ -2019,3 +2019,10 @@ replaceWidgetsButtonWithReloadButton: "ウィジェットのボタンを再読
|
|||
searchEngine: "検索のMFMで使用する検索エンジン"
|
||||
postSearch: "このサーバーの投稿検索"
|
||||
showBigPostButton: "投稿ボタンを巨大にする"
|
||||
emojiModPerm: "カスタム絵文字の管理権"
|
||||
emojiModPermDescription: "追加: カスタム絵文字の新規追加と新規追加されたカスタム絵文字(正確には、タグとカテゴリとライセンスが設定されていないカスタム絵文字)へのタグとカテゴリとライセンスの設定を許可します。\n追加と変更:「追加」の権限に加え、既存の絵文字の名前・カテゴリ・タグ・ライセンスの変更を許可します。\n全て許可:「追加と変更」の権限に加え、既存のカスタム絵文字の削除を許可します。\nこの設定にかかわらず、サーバーの管理者およびモデレーターには「全て許可」の権限が与えられます。"
|
||||
_emojiModPerm:
|
||||
unauthorized: "無し"
|
||||
add: "追加"
|
||||
mod: "追加と変更"
|
||||
full: "全て許可"
|
||||
|
|
|
@ -125,6 +125,22 @@ pub enum RelayStatusEnum {
|
|||
Requesting,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[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,
|
||||
#[sea_orm(string_value = "unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
|
||||
#[sea_orm(
|
||||
rs_type = "String",
|
||||
db_type = "Enum",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.10
|
||||
|
||||
use super::sea_orm_active_enums::UserEmojimodpermEnum;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
|
@ -38,6 +39,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")]
|
||||
|
|
17
packages/backend/migration/1692825433698-emoji-moderator.js
Normal file
17
packages/backend/migration/1692825433698-emoji-moderator.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
export class EmojiModerator1692825433698 {
|
||||
name = "EmojiModerator1692825433698";
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "public"."user_emojimodperm_enum" AS ENUM('unauthorized', 'add', 'mod', 'full')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user" ADD "emojiModPerm" "public"."user_emojimodperm_enum" NOT NULL DEFAULT 'unauthorized'`,
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "emojiModPerm"`);
|
||||
await queryRunner.query(`DROP TYPE "public"."user_emojimodperm_enum"`);
|
||||
}
|
||||
}
|
|
@ -178,6 +178,17 @@ export class User {
|
|||
})
|
||||
public isModerator: boolean;
|
||||
|
||||
// unauthorized: 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
|
||||
@Column({
|
||||
type: "enum",
|
||||
enum: ["unauthorized", "add", "mod", "full"],
|
||||
default: "unauthorized",
|
||||
})
|
||||
public emojiModPerm: "unauthorized" | "add" | "mod" | "full";
|
||||
|
||||
@Index()
|
||||
@Column("boolean", {
|
||||
default: true,
|
||||
|
|
|
@ -449,6 +449,7 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
avatarUrl: this.getAvatarUrlSync(user),
|
||||
avatarBlurhash: user.avatar?.blurhash || null,
|
||||
avatarColor: null, // 後方互換性のため
|
||||
emojiModPerm: user.emojiModPerm ?? "unauthorized",
|
||||
isAdmin: user.isAdmin || falsy,
|
||||
isModerator: user.isModerator || falsy,
|
||||
isBot: user.isBot || falsy,
|
||||
|
|
|
@ -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";
|
||||
|
@ -411,6 +412,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],
|
||||
|
|
|
@ -2,12 +2,21 @@ import define from "@/server/api/define.js";
|
|||
import { Emojis } from "@/models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { ApiError } from "@/server/api/error.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,11 +39,22 @@ 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 "add" permission
|
||||
if (!(me.isAdmin || me.isModerator || me.emojiModPerm === "unauthorized"))
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
const emojis = await Emojis.findBy({
|
||||
id: In(ps.ids),
|
||||
});
|
||||
|
||||
// require emoji "mod" permission if an alias has already been set
|
||||
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(),
|
||||
|
|
|
@ -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 !== "unauthorized"))
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
const file = await DriveFiles.findOneBy({ id: ps.fileId });
|
||||
|
||||
if (file == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -3,12 +3,21 @@ import { Emojis } from "@/models/index.js";
|
|||
import { In } from "typeorm";
|
||||
import { insertModerationLog } from "@/services/insert-moderation-log.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
|
||||
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 = {
|
||||
|
@ -26,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),
|
||||
});
|
||||
|
|
|
@ -5,10 +5,10 @@ import { ApiError } from "@/server/api/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);
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
import define from "@/server/api/define.js";
|
||||
import { createImportCustomEmojisJob } from "@/queue/index.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
|
||||
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 = {
|
||||
|
@ -16,5 +27,11 @@ export const paramDef = {
|
|||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
// require emoji "add" permission
|
||||
if (
|
||||
!(user.isAdmin || user.isModerator || user.emojiModPerm !== "unauthorized")
|
||||
)
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
createImportCustomEmojisJob(user, ps.fileId);
|
||||
});
|
||||
|
|
|
@ -3,12 +3,13 @@ import { Emojis } from "@/models/index.js";
|
|||
import { toPuny } from "@/misc/convert-host.js";
|
||||
import { makePaginationQuery } from "@/server/api/common/make-pagination-query.js";
|
||||
import { sqlLikeEscape } from "@/misc/sql-like-escape.js";
|
||||
import { ApiError } from "@/server/api/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 !== "unauthorized"))
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
const q = makePaginationQuery(
|
||||
Emojis.createQueryBuilder("emoji"),
|
||||
ps.sinceId,
|
||||
|
|
|
@ -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 "@/server/api/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 !== "unauthorized"))
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
const q = makePaginationQuery(
|
||||
Emojis.createQueryBuilder("emoji"),
|
||||
ps.sinceId,
|
||||
|
|
|
@ -2,12 +2,21 @@ import define from "@/server/api/define.js";
|
|||
import { Emojis } from "@/models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { ApiError } from "@/server/api/error.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 +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),
|
||||
});
|
||||
|
|
|
@ -2,12 +2,21 @@ import define from "@/server/api/define.js";
|
|||
import { Emojis } from "@/models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { ApiError } from "@/server/api/error.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 +39,22 @@ 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 "add" permission
|
||||
if (!(me.isAdmin || me.isModerator || me.emojiModPerm === "unauthorized"))
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
const emojis = await Emojis.findBy({
|
||||
id: In(ps.ids),
|
||||
});
|
||||
|
||||
// require emoji mod permission if an alias has already been set
|
||||
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),
|
||||
|
|
|
@ -2,12 +2,21 @@ import define from "@/server/api/define.js";
|
|||
import { Emojis } from "@/models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { ApiError } from "@/server/api/error.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 = {
|
||||
|
@ -29,7 +38,21 @@ export const paramDef = {
|
|||
required: ["ids"],
|
||||
} 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 === "unauthorized"))
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
const emojis = await Emojis.findBy({
|
||||
id: In(ps.ids),
|
||||
});
|
||||
|
||||
// require emoji "mod" permission if a category has already been set
|
||||
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),
|
||||
|
|
|
@ -2,12 +2,21 @@ import define from "@/server/api/define.js";
|
|||
import { Emojis } from "@/models/index.js";
|
||||
import { In } from "typeorm";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { ApiError } from "@/server/api/error.js";
|
||||
|
||||
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 = {
|
||||
|
@ -29,7 +38,21 @@ export const paramDef = {
|
|||
required: ["ids"],
|
||||
} 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 === "unauthorized"))
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
const emojis = await Emojis.findBy({
|
||||
id: In(ps.ids),
|
||||
});
|
||||
|
||||
// require emoji "mod" permission if a license has already been set
|
||||
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),
|
||||
|
|
|
@ -4,10 +4,10 @@ import { ApiError } from "@/server/api/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,11 +47,25 @@ 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 "add" permission
|
||||
if (!(me.isAdmin || me.isModerator || me.emojiModPerm !== "unauthorized"))
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
|
||||
const emoji = await Emojis.findOneBy({ id: ps.id });
|
||||
|
||||
if (emoji == null) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
|
||||
// require emoji "mod" permission if a category/alias/license has already been set
|
||||
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(),
|
||||
name: ps.name,
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import define from "@/server/api/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 (!["unauthorized", "add", "mod", "full"].includes(ps.emojiModPerm)) {
|
||||
throw new Error(
|
||||
"emojiModPerm must be 'unauthorized', 'add', 'mod', or 'full'",
|
||||
);
|
||||
}
|
||||
|
||||
const _emojiModPerm =
|
||||
(ps.emojiModPerm as "unauthorized" | "add" | "mod" | "full") ??
|
||||
"unauthorized";
|
||||
|
||||
await Users.update(user.id, {
|
||||
emojiModPerm: _emojiModPerm,
|
||||
});
|
||||
|
||||
publishInternalEvent("userChangeModeratorState", {
|
||||
id: user.id,
|
||||
isModerator: true,
|
||||
});
|
||||
});
|
|
@ -65,6 +65,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,
|
||||
|
|
|
@ -178,6 +178,29 @@
|
|||
</FormSection>
|
||||
</div>
|
||||
<div v-else-if="tab === 'moderation'" class="_formRoot">
|
||||
<FormSelect
|
||||
v-model="emojiModPerm"
|
||||
class="_formBlock"
|
||||
@update:modelValue="setEmojiMod"
|
||||
>
|
||||
<template #label>{{ i18n.ts.emojiModPerm }}</template>
|
||||
<option value="unauthorized">
|
||||
{{ i18n.ts._emojiModPerm.unauthorized }}
|
||||
</option>
|
||||
<option value="add">
|
||||
{{ i18n.ts._emojiModPerm.add }}
|
||||
</option>
|
||||
<option value="mod">
|
||||
{{ i18n.ts._emojiModPerm.mod }}
|
||||
</option>
|
||||
<option value="full">
|
||||
{{ i18n.ts._emojiModPerm.full }}
|
||||
</option>
|
||||
</FormSelect>
|
||||
<MkInfo class="_formBlock">{{
|
||||
i18n.ts.emojiModPermDescription
|
||||
}}</MkInfo>
|
||||
|
||||
<FormSwitch
|
||||
v-if="
|
||||
user.host == null &&
|
||||
|
@ -368,6 +391,7 @@ import FormSection from "@/components/form/section.vue";
|
|||
import FormButton from "@/components/MkButton.vue";
|
||||
import FormInput from "@/components/form/input.vue";
|
||||
import FormFolder from "@/components/form/folder.vue";
|
||||
import FormSelect from "@/components/form/select.vue";
|
||||
import MkKeyValue from "@/components/MkKeyValue.vue";
|
||||
import FormSuspense from "@/components/form/suspense.vue";
|
||||
import MkFileListForAdmin from "@/components/MkFileListForAdmin.vue";
|
||||
|
@ -393,6 +417,7 @@ const info = ref();
|
|||
const ips = ref(null);
|
||||
const ap = ref(null);
|
||||
const moderator = ref(false);
|
||||
const emojiModPerm = ref<"unauthorized" | "add" | "mod" | "full">();
|
||||
const silenced = ref(false);
|
||||
const suspended = ref(false);
|
||||
const driveCapacityOverrideMb = ref<number | null>(0);
|
||||
|
@ -425,6 +450,7 @@ function createFetcher() {
|
|||
info.value = _info;
|
||||
ips.value = _ips;
|
||||
moderator.value = info.value.isModerator;
|
||||
emojiModPerm.value = info.value.emojiModPerm;
|
||||
silenced.value = info.value.isSilenced;
|
||||
suspended.value = info.value.isSuspended;
|
||||
driveCapacityOverrideMb.value =
|
||||
|
@ -510,6 +536,14 @@ async function toggleModerator(v) {
|
|||
await refreshUser();
|
||||
}
|
||||
|
||||
async function setEmojiMod() {
|
||||
await os.api("admin/set-emoji-moderator", {
|
||||
userId: user.value.id,
|
||||
emojiModPerm: emojiModPerm.value,
|
||||
});
|
||||
await refreshUser();
|
||||
}
|
||||
|
||||
async function sendModMail() {
|
||||
const { canceled, result } = await os.inputParagraph({
|
||||
title: "Moderation Notice",
|
||||
|
|
|
@ -10,4 +10,5 @@ export const $i = accountData
|
|||
|
||||
export const isSignedIn = $i != null;
|
||||
export const isModerator = $i != null && ($i.isModerator || $i.isAdmin);
|
||||
export const isAdmin = $i != null && $i.isAdmin;
|
||||
export const isEmojiMod = isModerator || $i?.emojiModPerm !== "unauthorized";
|
||||
export const isAdmin = $i?.isAdmin;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { AsyncComponentLoader } from "vue";
|
||||
import { defineAsyncComponent, inject } from "vue";
|
||||
import { $i, isModerator } from "@/reactiveAccount";
|
||||
import { $i, isModerator, isEmojiMod } from "@/reactiveAccount";
|
||||
import { Router } from "@/nirax";
|
||||
import MkError from "@/pages/_error_.vue";
|
||||
import MkLoading from "@/pages/_loading_.vue";
|
||||
|
@ -422,6 +422,13 @@ export const routes = [
|
|||
? page(() => import("./pages/admin-file.vue"))
|
||||
: page(() => import("./pages/not-found.vue")),
|
||||
},
|
||||
{
|
||||
path: "/admin/emojis",
|
||||
name: "emojis",
|
||||
component: isEmojiMod
|
||||
? page(() => import("./pages/admin/emojis.vue"))
|
||||
: page(() => import("./pages/not-found.vue")),
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
component: isModerator
|
||||
|
@ -443,11 +450,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",
|
||||
|
|
|
@ -80,6 +80,17 @@
|
|||
<i :class="icon('ph-door icon ph-fw')"></i
|
||||
><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||
</MkA>
|
||||
<MkA
|
||||
v-else-if="$i.emojiModPerm !== 'unauthorized'"
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="i18n.ts.customEmojis"
|
||||
class="item _button"
|
||||
active-class="active"
|
||||
to="/admin/emojis"
|
||||
>
|
||||
<i class="icon ph-smiley ph-bold ph-fw ph-lg"></i
|
||||
><span class="text">{{ i18n.ts.customEmojis }}</span>
|
||||
</MkA>
|
||||
<button v-click-anime class="item _button" @click="more">
|
||||
<i :class="icon('ph-dots-three-outline icon ph-fw')"></i
|
||||
><span class="text">{{ i18n.ts.more }}</span>
|
||||
|
|
|
@ -92,6 +92,17 @@
|
|||
><i :class="icon('ph-door icon ph-fw')"></i
|
||||
><span class="text">{{ i18n.ts.controlPanel }}</span>
|
||||
</MkA>
|
||||
<MkA
|
||||
v-else-if="$i.emojiModPerm !== 'unauthorized'"
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="i18n.ts.customEmojis"
|
||||
class="item _button"
|
||||
active-class="active"
|
||||
to="/admin/emojis"
|
||||
>
|
||||
<i class="icon ph-smiley ph-bold ph-fw ph-lg"></i
|
||||
><span class="text">{{ i18n.ts.customEmojis }}</span>
|
||||
</MkA>
|
||||
<button
|
||||
v-click-anime
|
||||
v-tooltip.noDelay.right="i18n.ts.more"
|
||||
|
|
Loading…
Reference in a new issue