Skip to content

Commit

Permalink
enhance(backend): ページ、ギャラリー、Playのモデレーション強化 (#13523)
Browse files Browse the repository at this point in the history
* enhance(backend): Page、ギャラリー、Playのモデレーション強化

* Update CHANGELOG.md

* fix: update misskey-js

* refactor(frontend): use `MkA`

* Update CHANGELOG.md

* fix(i18n): Page -> ページ
  • Loading branch information
zyoshoka authored Aug 17, 2024
1 parent 383c41b commit fd744f4
Show file tree
Hide file tree
Showing 16 changed files with 333 additions and 30 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
### General
- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように
- Enhance: アカウントの削除のモデレーションログを残すように
- Enhance: 不適切なページ、ギャラリー、Playを管理者権限で削除できるように
- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正

### Client
- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように
- Enhance: 不適切なページ、ギャラリー、Playを通報できるように
- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正
- Fix: ページ遷移に失敗することがある問題を修正
- Fix: iOSでユーザー名などがリンクとして誤検知される現象を抑制
Expand Down
14 changes: 13 additions & 1 deletion locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2829,7 +2829,7 @@ export interface Locale extends ILocale {
*/
"reportAbuseOf": ParameterizedString<"name">;
/**
* 通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください
* 通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください
*/
"fillAbuseReportDescription": string;
/**
Expand Down Expand Up @@ -9687,6 +9687,18 @@ export interface Locale extends ILocale {
* アカウントを削除
*/
"deleteAccount": string;
/**
* ページを削除
*/
"deletePage": string;
/**
* Playを削除
*/
"deleteFlash": string;
/**
* ギャラリーの投稿を削除
*/
"deleteGalleryPost": string;
};
"_fileViewer": {
/**
Expand Down
5 changes: 4 additions & 1 deletion locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ abuseReports: "通報"
reportAbuse: "通報"
reportAbuseRenote: "リノートを通報"
reportAbuseOf: "{name}を通報する"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートがある場合はそのURLも記入してください"
fillAbuseReportDescription: "通報理由の詳細を記入してください。対象のノートやページなどがある場合はそのURLも記入してください"
abuseReported: "内容が送信されました。ご報告ありがとうございました。"
reporter: "通報者"
reporteeOrigin: "通報先"
Expand Down Expand Up @@ -2569,6 +2569,9 @@ _moderationLogTypes:
updateAbuseReportNotificationRecipient: "通報の通知先を更新"
deleteAbuseReportNotificationRecipient: "通報の通知先を削除"
deleteAccount: "アカウントを削除"
deletePage: "ページを削除"
deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除"

_fileViewer:
title: "ファイルの詳細"
Expand Down
24 changes: 22 additions & 2 deletions packages/backend/src/server/api/endpoints/flash/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
*/

import { Inject, Injectable } from '@nestjs/common';
import type { FlashsRepository } from '@/models/_.js';
import type { FlashsRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';

export const meta = {
Expand Down Expand Up @@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const flash = await this.flashsRepository.findOneBy({ id: ps.flashId });

if (flash == null) {
throw new ApiError(meta.errors.noSuchFlash);
}
if (flash.userId !== me.id) {

if (!await this.roleService.isModerator(me) && flash.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}

await this.flashsRepository.delete(flash.id);

if (flash.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: flash.userId });
this.moderationLogService.log(me, 'deleteFlash', {
flashId: flash.id,
flashUserId: flash.userId,
flashUserUsername: user.username,
flash,
});
}
});
}
}
35 changes: 30 additions & 5 deletions packages/backend/src/server/api/endpoints/gallery/posts/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository } from '@/models/_.js';
import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';

export const meta = {
Expand All @@ -22,6 +24,12 @@ export const meta = {
code: 'NO_SUCH_POST',
id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5',
},

accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: 'c86e09de-1c48-43ac-a435-1c7e42ed4496',
},
},
} as const;

Expand All @@ -38,18 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.galleryPostsRepository)
private galleryPostsRepository: GalleryPostsRepository,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const post = await this.galleryPostsRepository.findOneBy({
id: ps.postId,
userId: me.id,
});
const post = await this.galleryPostsRepository.findOneBy({ id: ps.postId });

if (post == null) {
throw new ApiError(meta.errors.noSuchPost);
}

if (!await this.roleService.isModerator(me) && post.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}

await this.galleryPostsRepository.delete(post.id);

if (post.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: post.userId });
this.moderationLogService.log(me, 'deleteGalleryPost', {
postId: post.id,
postUserId: post.userId,
postUserUsername: user.username,
post,
});
}
});
}
}
24 changes: 22 additions & 2 deletions packages/backend/src/server/api/endpoints/pages/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
*/

import { Inject, Injectable } from '@nestjs/common';
import type { PagesRepository } from '@/models/_.js';
import type { PagesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';

export const meta = {
Expand Down Expand Up @@ -44,17 +46,35 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.pagesRepository)
private pagesRepository: PagesRepository,

@Inject(DI.usersRepository)
private usersRepository: UsersRepository,

private moderationLogService: ModerationLogService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const page = await this.pagesRepository.findOneBy({ id: ps.pageId });

if (page == null) {
throw new ApiError(meta.errors.noSuchPage);
}
if (page.userId !== me.id) {

if (!await this.roleService.isModerator(me) && page.userId !== me.id) {
throw new ApiError(meta.errors.accessDenied);
}

await this.pagesRepository.delete(page.id);

if (page.userId !== me.id) {
const user = await this.usersRepository.findOneByOrFail({ id: page.userId });
this.moderationLogService.log(me, 'deletePage', {
pageId: page.id,
pageUserId: page.userId,
pageUserUsername: user.username,
page,
});
}
});
}
}
21 changes: 21 additions & 0 deletions packages/backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ export const moderationLogTypes = [
'updateAbuseReportNotificationRecipient',
'deleteAbuseReportNotificationRecipient',
'deleteAccount',
'deletePage',
'deleteFlash',
'deleteGalleryPost',
] as const;

export type ModerationLogPayloads = {
Expand Down Expand Up @@ -320,6 +323,24 @@ export type ModerationLogPayloads = {
userUsername: string;
userHost: string | null;
};
deletePage: {
pageId: string;
pageUserId: string;
pageUserUsername: string;
page: any;
};
deleteFlash: {
flashId: string;
flashUserId: string;
flashUserUsername: string;
flash: any;
};
deleteGalleryPost: {
postId: string;
postUserId: string;
postUserUsername: string;
post: any;
};
};

export type Serialized<T> = {
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkAbuseReportWindow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';

const props = defineProps<{
user: Misskey.entities.UserDetailed;
user: Misskey.entities.UserLite;
initialComment?: string;
}>();

Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/global/MkAcct.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { host as hostRaw } from '@/config.js';
import { defaultStore } from '@/store.js';

defineProps<{
user: Misskey.entities.User;
user: Misskey.entities.UserLite;
detail?: boolean;
}>();

Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/src/pages/admin/modlog.ModLog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
'deleteSystemWebhook',
'deleteAbuseReportNotificationRecipient',
'deleteAccount',
'deletePage',
'deleteFlash',
'deleteGalleryPost',
].includes(log.type)
}"
>{{ i18n.ts._moderationLogTypes[log.type] }}</b>
Expand Down Expand Up @@ -74,6 +77,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="log.type === 'updateAbuseReportNotificationRecipient'">: {{ log.info.before.name }}</span>
<span v-else-if="log.type === 'deleteAbuseReportNotificationRecipient'">: {{ log.info.recipient.name }}</span>
<span v-else-if="log.type === 'deleteAccount'">: @{{ log.info.userUsername }}{{ log.info.userHost ? '@' + log.info.userHost : '' }}</span>
<span v-else-if="log.type === 'deletePage'">: @{{ log.info.pageUserUsername }}</span>
<span v-else-if="log.type === 'deleteFlash'">: @{{ log.info.flashUserUsername }}</span>
<span v-else-if="log.type === 'deleteGalleryPost'">: @{{ log.info.postUserUsername }}</span>
</template>
<template #icon>
<MkAvatar :user="log.user" :class="$style.avatar"/>
Expand Down
49 changes: 48 additions & 1 deletion packages/frontend/src/pages/flash/flash.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-else v-tooltip="i18n.ts.like" asLike class="button" rounded @click="like()"><i class="ti ti-heart"></i><span v-if="flash?.likedCount && flash.likedCount > 0" style="margin-left: 6px;">{{ flash.likedCount }}</span></MkButton>
<MkButton v-tooltip="i18n.ts.copyLink" class="button" rounded @click="copyLink"><i class="ti ti-link ti-fw"></i></MkButton>
<MkButton v-tooltip="i18n.ts.share" class="button" rounded @click="share"><i class="ti ti-share ti-fw"></i></MkButton>
<MkButton v-if="$i && $i.id !== flash.user.id" class="button" rounded @mousedown="showMenu"><i class="ti ti-dots ti-fw"></i></MkButton>
</div>
</div>
</div>
Expand Down Expand Up @@ -61,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>

<script lang="ts" setup>
import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef } from 'vue';
import { computed, onDeactivated, onUnmounted, Ref, ref, watch, shallowRef, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import { Interpreter, Parser, values } from '@syuilo/aiscript';
import MkButton from '@/components/MkButton.vue';
Expand All @@ -79,6 +80,7 @@ import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
import { isSupportShare } from '@/scripts/navigator.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { MenuItem } from '@/types/menu';
import { pleaseLogin } from '@/scripts/please-login.js';

const props = defineProps<{
Expand Down Expand Up @@ -229,6 +231,51 @@ async function run() {
}
}

function reportAbuse() {
if (!flash.value) return;

const pageUrl = `${url}/play/${flash.value.id}`;

os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: flash.value.user,
initialComment: `Play: ${pageUrl}\n-----\n`,
}, {}, 'closed');
}

function showMenu(ev: MouseEvent) {
if (!flash.value) return;

const menu: MenuItem[] = [
...($i && $i.id !== flash.value.userId ? [
{
icon: 'ti ti-exclamation-circle',
text: i18n.ts.reportAbuse,
action: reportAbuse,
},
...($i.isModerator || $i.isAdmin ? [
{
type: 'divider' as const,
},
{
icon: 'ti ti-trash',
text: i18n.ts.delete,
danger: true,
action: () => os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
}).then(({ canceled }) => {
if (canceled || !flash.value) return;

os.apiWithDialog('flash/delete', { flashId: flash.value.id });
}),
},
] : []),
] : []),
];

os.popupMenu(menu, ev.currentTarget ?? ev.target);
}

function reset() {
if (aiscript.value) aiscript.value.abort();
started.value = false;
Expand Down
Loading

1 comment on commit fd744f4

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chromatic detects changes. Please review the changes on Chromatic.

Please sign in to comment.