feat: emoji moderators (close #56)

This commit is contained in:
naskya 2023-08-25 03:23:12 +09:00
parent 1d29ca3609
commit d9927c027e
Signed by: naskya
GPG key ID: 164DFF24E2D40139
30 changed files with 412 additions and 40 deletions

View file

@ -10,6 +10,13 @@
## 主要な変更点
- モデレーターでない一般ユーザーにもカスタム絵文字の管理権を与えられるように
- カスタム絵文字の管理が大変なサーバー管理者さんがたくさんいらっしゃったのでこの機能を追加するべきではないか他の開発者に訊いたところロール機能の実装を待つべきだと言われてしまったが、Firefish のロール機能は現状では仕様がまだ固まっておらず実装までに時間が掛かると考えられるため
- 以下の権限を与えられます
- 不許可: 絵文字の管理を許可しない
- 追加: 新しい絵文字の追加のみを許可する
- 追加と変更: 「追加」に加え、既存のカスタム絵文字の名前・カテゴリ・タグ・ライセンスの編集を許可する
- 全て許可:「追加と変更」に加え、既存のカスタム絵文字の削除を許可する
- UI 用の言語とは別に、投稿翻訳に使用する言語を設定可能に
- 投稿言語を自動検出して外国語の投稿に翻訳ボタンを表示する設定を追加
- モバイル表示の下部のウィジェットボタンを再読み込みボタンに変更可能に

View file

@ -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"

View file

@ -1989,3 +1989,10 @@ delete2faConfirm: これで、このアカウントの2要素認証は完全に
inputNotMatch: 入力が一致しません
deletePasskeysConfirm: これで、このアカウントのパスキーは完全に削除されます。続行しますか?
confirm: "確認"
emojiModPerm: "カスタム絵文字の管理"
emojiModPermDescription: "追加: カスタム絵文字の新規追加と新規追加されたカスタム絵文字(正確には、タグとカテゴリとライセンスが設定されていないカスタム絵文字)へのタグとカテゴリとライセンスの設定を許可します。\n追加と変更:「追加」の権限に加え、既存の絵文字の名前・カテゴリ・タグ・ライセンスの変更を許可します。\n全て許可:「追加と変更」の権限に加え、既存のカスタム絵文字の削除を許可します。\nこの設定にかかわらず、サーバーの管理者およびモデレーターには「全て許可」の権限が与えられます。"
_emojiModPerm:
none: "不許可"
add: "追加"
mod: "追加と変更"
full: "全て許可"

View file

@ -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"`);
}
}

View file

@ -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",

View file

@ -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")]

View file

@ -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"],
});

View file

@ -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,

View file

@ -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,

View file

@ -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],

View file

@ -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(),

View file

@ -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);

View file

@ -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) {

View file

@ -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),
});

View file

@ -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);

View file

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

View file

@ -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,

View file

@ -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,

View file

@ -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),
});

View file

@ -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),

View file

@ -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),

View file

@ -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),

View file

@ -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(),

View file

@ -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,
});
});

View file

@ -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,

View file

@ -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();

View file

@ -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="none">
{{ i18n.ts._emojiModPerm.none }}
</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 &&
@ -369,6 +392,7 @@ import FormButton from "@/components/MkButton.vue";
import FormInput from "@/components/form/input.vue";
import FormSplit from "@/components/form/split.vue";
import FormFolder from "@/components/form/folder.vue";
import FormSelect from "@/components/form/select.vue";
import MkKeyValue from "@/components/MkKeyValue.vue";
import MkSelect from "@/components/form/select.vue";
import FormSuspense from "@/components/form/suspense.vue";
@ -398,6 +422,7 @@ let ap = $ref(null);
let moderator = $ref(false);
let silenced = $ref(false);
let suspended = $ref(false);
let emojiModPerm = $ref("");
let driveCapacityOverrideMb: number | null = $ref(0);
let moderationNote = $ref("");
const filesPagination = {
@ -430,6 +455,7 @@ function createFetcher() {
moderator = info.isModerator;
silenced = info.isSilenced;
suspended = info.isSuspended;
emojiModPerm = info.emojiModPerm;
driveCapacityOverrideMb = user.driveCapacityOverrideMb;
moderationNote = info.moderationNote;
@ -512,6 +538,14 @@ async function toggleModerator(v) {
await refreshUser();
}
async function setEmojiMod() {
await os.api("admin/set-emoji-moderator", {
userId: user.id,
emojiModPerm: emojiModPerm,
});
await refreshUser();
}
async function sendModMail() {
const { canceled, result } = await os.inputParagraph({
title: "Moderation Notice",

View file

@ -1,6 +1,6 @@
import { AsyncComponentLoader, defineAsyncComponent, inject } from "vue";
import { Router } from "@/nirax";
import { $i, iAmModerator } from "@/account";
import { $i, iAmModerator, iAmEmojiMod } from "@/account";
import MkLoading from "@/pages/_loading_.vue";
import MkError from "@/pages/_error_.vue";
import { api } from "@/os";
@ -438,6 +438,13 @@ export const routes = [
? page(() => 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",

View file

@ -80,6 +80,17 @@
<i class="icon ph-door ph-bold ph-lg ph-fw ph-lg"></i
><span class="text">{{ i18n.ts.controlPanel }}</span>
</MkA>
<MkA
v-else-if="$i.emojiModPerm !== 'none'"
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 ph-bold ph-lg ph-fw ph-lg"
@ -134,7 +145,7 @@ import {
} from "vue";
import * as os from "@/os";
import { navbarItemDef } from "@/navbar";
import { openAccountMenu as openAccountMenu_ } from "@/account";
import { $i, openAccountMenu as openAccountMenu_ } from "@/account";
import { openHelpMenu_ } from "@/scripts/helpMenu";
import { defaultStore } from "@/store";
import { i18n } from "@/i18n";

View file

@ -92,6 +92,17 @@
><i class="icon ph-door ph-bold ph-fw ph-lg"></i
><span class="text">{{ i18n.ts.controlPanel }}</span>
</MkA>
<MkA
v-else-if="$i.emojiModPerm !== 'none'"
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"