From dd426735a0a840691273e86362d6e1c5538c0b72 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 3 Jul 2022 00:15:03 +0900 Subject: [PATCH] feat: moderation note --- CHANGELOG.md | 1 + locales/ja-JP.yml | 1 + .../1656772790599-user-moderation-note.js | 11 ++ .../src/models/entities/user-profile.ts | 7 +- packages/backend/src/server/api/endpoints.ts | 2 + .../server/api/endpoints/admin/show-user.ts | 1 + .../api/endpoints/admin/update-user-note.ts | 31 ++++ packages/client/src/pages/user-info.vue | 136 ++++++++++++------ 8 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 packages/backend/migration/1656772790599-user-moderation-note.js create mode 100644 packages/backend/src/server/api/endpoints/admin/update-user-note.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ddc123994..5f185f63fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ You should also include the user name that made the change. - Client: Add rss-marquee widget @syuilo - Client: Removing entries from a clip @futchitwo - Client: Poll highlights in explore page @syuilo +- ユーザーにモデレーションメモを残せる機能 @syuilo - Make possible to delete an account by admin @syuilo - Improve player detection in URL preview @mei23 - Add Badge Image to Push Notification #8012 @tamaina diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index ec726c821a..d333ac29df 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -381,6 +381,7 @@ administrator: "管理者" token: "トークン" twoStepAuthentication: "二段階認証" moderator: "モデレーター" +moderation: "モデレーション" nUsersMentioned: "{n}人が投稿" securityKey: "セキュリティキー" securityKeyName: "キーの名前" diff --git a/packages/backend/migration/1656772790599-user-moderation-note.js b/packages/backend/migration/1656772790599-user-moderation-note.js new file mode 100644 index 0000000000..133bcffe1a --- /dev/null +++ b/packages/backend/migration/1656772790599-user-moderation-note.js @@ -0,0 +1,11 @@ +export class userModerationNote1656772790599 { + name = 'userModerationNote1656772790599' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "moderationNote"`); + } +} diff --git a/packages/backend/src/models/entities/user-profile.ts b/packages/backend/src/models/entities/user-profile.ts index 1778742ea2..7dfe13fe19 100644 --- a/packages/backend/src/models/entities/user-profile.ts +++ b/packages/backend/src/models/entities/user-profile.ts @@ -1,8 +1,8 @@ import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm'; +import { ffVisibility, notificationTypes } from '@/types.js'; import { id } from '../id.js'; import { User } from './user.js'; import { Page } from './page.js'; -import { ffVisibility, notificationTypes } from '@/types.js'; // TODO: このテーブルで管理している情報すべてレジストリで管理するようにしても良いかも // ただ、「emailVerified が true なユーザーを find する」のようなクエリは書けなくなるからウーン @@ -117,6 +117,11 @@ export class UserProfile { }) public password: string | null; + @Column('varchar', { + length: 8192, default: '', + }) + public moderationNote: string | null; + // TODO: そのうち消す @Column('jsonb', { default: {}, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index f458763923..4a2ecebd86 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -61,6 +61,7 @@ import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js'; import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js'; import * as ep___admin_vacuum from './endpoints/admin/vacuum.js'; import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js'; +import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js'; import * as ep___announcements from './endpoints/announcements.js'; import * as ep___antennas_create from './endpoints/antennas/create.js'; import * as ep___antennas_delete from './endpoints/antennas/delete.js'; @@ -376,6 +377,7 @@ const eps = [ ['admin/update-meta', ep___admin_updateMeta], ['admin/vacuum', ep___admin_vacuum], ['admin/delete-account', ep___admin_deleteAccount], + ['admin/update-user-note', ep___admin_updateUserNote], ['announcements', ep___announcements], ['antennas/create', ep___antennas_create], ['antennas/delete', ep___antennas_delete], 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 36384c2b3d..f04a7a67c5 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-user.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts @@ -69,6 +69,7 @@ export default define(meta, paramDef, async (ps, me) => { isSilenced: user.isSilenced, isSuspended: user.isSuspended, lastActiveDate: user.lastActiveDate, + moderationNote: profile.moderationNote, signins, }; }); diff --git a/packages/backend/src/server/api/endpoints/admin/update-user-note.ts b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts new file mode 100644 index 0000000000..fa21ab7833 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/update-user-note.ts @@ -0,0 +1,31 @@ +import { UserProfiles, Users } from '@/models/index.js'; +import define from '../../define.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, +} as const; + +export const paramDef = { + type: 'object', + properties: { + userId: { type: 'string', format: 'misskey:id' }, + text: { type: 'string' }, + }, + required: ['userId', 'text'], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, me) => { + const user = await Users.findOneBy({ id: ps.userId }); + + if (user == null) { + throw new Error('user not found'); + } + + await UserProfiles.update({ userId: user.id }, { + moderationNote: ps.text, + }); +}); diff --git a/packages/client/src/pages/user-info.vue b/packages/client/src/pages/user-info.vue index f9edd208ab..204ece7eb6 100644 --- a/packages/client/src/pages/user-info.vue +++ b/packages/client/src/pages/user-info.vue @@ -9,6 +9,11 @@
@{{ acct(user) }} + + Suspended + Silenced + Moderator +
@@ -41,20 +46,12 @@ + + + + - - - {{ $ts.moderator }} - {{ $ts.silence }} - {{ $ts.suspend }} - {{ $ts.reflectMayTakeTime }} -
- {{ $ts.resetPassword }} - {{ $ts.deleteAccount }} -
-
- @@ -78,8 +75,44 @@ {{ $ts.updateRemoteUser }} + + + + + + + +
+ {{ $ts.moderator }} + {{ $ts.silence }} + {{ $ts.suspend }} + {{ $ts.reflectMayTakeTime }} +
+ {{ $ts.resetPassword }} + {{ $ts.deleteAccount }} +
+ + + + + + {{ i18n.ts.requireAdminForView }} + The date is the IP address was first acknowledged. + + + + + + + +
@@ -95,23 +128,6 @@
-
- -
-
- {{ i18n.ts.requireAdminForView }} - The date is the IP address was first acknowledged. - -
-
- - -
@@ -134,6 +150,7 @@ import FormSwitch from '@/components/form/switch.vue'; import FormLink from '@/components/form/link.vue'; import FormSection from '@/components/form/section.vue'; import FormButton from '@/components/ui/button.vue'; +import FormFolder from '@/components/form/folder.vue'; import MkKeyValue from '@/components/key-value.vue'; import MkSelect from '@/components/form/select.vue'; import FormSuspense from '@/components/form/suspense.vue'; @@ -162,6 +179,7 @@ let ap = $ref(null); let moderator = $ref(false); let silenced = $ref(false); let suspended = $ref(false); +let moderationNote = $ref(''); const filesPagination = { endpoint: 'admin/drive/files' as const, limit: 10, @@ -185,6 +203,12 @@ function createFetcher() { moderator = info.isModerator; silenced = info.isSilenced; suspended = info.isSuspended; + moderationNote = info.moderationNote; + + watch($$(moderationNote), async () => { + await os.api('admin/update-user-note', { userId: user.id, text: moderationNote }); + await refreshUser(); + }); }); } else { return () => os.api('users/show', { @@ -309,23 +333,15 @@ const headerTabs = $computed(() => [{ key: 'overview', title: i18n.ts.overview, icon: 'fas fa-info-circle', -}, { +}, iAmModerator ? { + key: 'moderation', + title: i18n.ts.moderation, + icon: 'fas fa-shield-halved', +} : null, { key: 'chart', title: i18n.ts.charts, icon: 'fas fa-chart-simple', -}, iAmModerator ? { - key: 'files', - title: i18n.ts.files, - icon: 'fas fa-cloud', -} : null, { - key: 'ap', - title: 'AP', - icon: 'fas fa-share-alt', -}, iAmModerator ? { - key: 'ip', - title: 'IP', - icon: 'fas fa-bars-staggered', -} : null, { +}, { key: 'raw', title: 'Raw', icon: 'fas fa-code', @@ -370,6 +386,40 @@ definePageMetadata(computed(() => ({ overflow: hidden; text-overflow: ellipsis; } + + > .state { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 4px; + + &:empty { + display: none; + } + + > .suspended, > .silenced, > .moderator { + display: inline-block; + border: solid 1px; + border-radius: 6px; + padding: 2px 6px; + font-size: 85%; + } + + > .suspended { + color: var(--error); + border-color: var(--error); + } + + > .silenced { + color: var(--warn); + border-color: var(--warn); + } + + > .moderator { + color: var(--success); + border-color: var(--success); + } + } } }