diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ddc12399..5f185f63f 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 ec726c821..d333ac29d 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 000000000..133bcffe1 --- /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 1778742ea..7dfe13fe1 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 f45876392..4a2ecebd8 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 36384c2b3..f04a7a67c 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 000000000..fa21ab783 --- /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 f9edd208a..204ece7eb 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); + } + } } }