diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html index 075be8498..19b33a0f5 100644 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html @@ -23,7 +23,12 @@ - {{ serverBlock.blockedServer.host }} + + + {{ serverBlock.blockedServer.host }} + + + {{ serverBlock.createdAt }} diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss index 9d3bedd80..c6c71587f 100644 --- a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss @@ -1,6 +1,20 @@ @import '_variables'; @import '_mixins'; +a { + @include disable-default-a-behaviour; + display: inline-block; + + &, &:hover { + color: var(--mainForegroundColor); + } + + span { + font-size: 80%; + color: var(--inputPlaceholderColor); + } +} + .unblock-button { @include peertube-button; @include grey-button; diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index a015b6d85..9ceff1161 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss @@ -29,10 +29,6 @@ } } -.glyphicon-trash { - font-size: 80%; -} - .screenratio { position: relative; width: 100%; @@ -47,6 +43,7 @@ display: inline-flex; justify-content: center; align-items: center; + color: var(--inputPlaceholderColor); } ::ng-deep iframe { diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 204cb209e..2204bb371 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html @@ -41,9 +41,22 @@ - - - {{ videoAbuse.video.name }} + +
+
+ + Deleted +
+
+
+ {{ videoAbuse.video.name }} + + + +
+
by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }}
+
+
@@ -78,10 +91,10 @@
-
+
The video was {{ videoAbuse.video.deleted ? 'deleted' : 'blacklisted' }}
-
+
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss new file mode 100644 index 000000000..09402fda7 --- /dev/null +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss @@ -0,0 +1,57 @@ +@import 'mixins'; +@import 'miniature'; + +.video-abuse-video-link { + @include disable-outline; + position: relative; + top: 3px; +} + +.video-abuse-video { + display: inline-flex; + + .video-abuse-video-image { + @include miniature-thumbnail; + + $image-height: 45px; + + height: $image-height; + width: #{(16/9) * $image-height}; + margin-right: 0.5rem; + border-radius: 2px; + border: none; + background: transparent; + display: inline-flex; + justify-content: center; + align-items: center; + + img { + height: 100%; + width: 100%; + border-radius: 2px; + } + + span { + color: var(--inputPlaceholderColor); + } + } + + .video-abuse-video-text { + display: inline-flex; + flex-direction: column; + justify-content: center; + font-size: 90%; + color: var(--mainForegroundColor); + line-height: 1rem; + + div .glyphicon { + font-size: 80%; + color: gray; + margin-left: 0.1rem; + } + + div + div { + font-size: 80%; + } + } +} diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index 9858cbce2..cc5014ae8 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts @@ -20,14 +20,14 @@ import { VideoService } from '@app/shared/video/video.service' @Component({ selector: 'my-video-abuse-list', templateUrl: './video-abuse-list.component.html', - styleUrls: [ '../moderation.component.scss'] + styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ] }) export class VideoAbuseListComponent extends RestTable implements OnInit { @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent videoAbuses: (VideoAbuse & { moderationCommentHtml?: string, reasonHtml?: string })[] = [] totalRecords = 0 - rowsPerPageOptions = [ 20, 50, 100 ] + rowsPerPageOptions = [ 20, 50, 100 ] rowsPerPage = this.rowsPerPageOptions[0] sort: SortMeta = { field: 'createdAt', order: 1 } pagination: RestPagination = { count: this.rowsPerPage, start: 0 } @@ -86,7 +86,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { }, { label: this.i18n('Blacklist video'), - isDisplayed: videoAbuse => !videoAbuse.video.deleted, + isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted, handler: videoAbuse => { this.videoBlacklistService.blacklistVideo(videoAbuse.video.id, undefined, true) .subscribe( @@ -100,11 +100,30 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { ) } }, + { + label: this.i18n('Unblacklist video'), + isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted, + handler: videoAbuse => { + this.videoBlacklistService.removeVideoFromBlacklist(videoAbuse.video.id) + .subscribe( + () => { + this.notifier.success(this.i18n('Video unblacklisted.')) + + this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED) + }, + + err => this.notifier.error(err.message) + ) + } + }, { label: this.i18n('Delete video'), isDisplayed: videoAbuse => !videoAbuse.video.deleted, handler: async videoAbuse => { - const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this video?'), this.i18n('Delete')) + const res = await this.confirmService.confirm( + this.i18n('Do you really want to delete this video?'), + this.i18n('Delete') + ) if (res === false) return this.videoService.removeVideo(videoAbuse.video.id) @@ -126,18 +145,36 @@ export class VideoAbuseListComponent extends RestTable implements OnInit { isHeader: true }, { - label: this.i18n('Mute reporter'), + label: this.i18n('Mute reporter'), handler: async videoAbuse => { const account = videoAbuse.reporterAccount as Account this.blocklistService.blockAccountByInstance(account) .subscribe( () => { - this.notifier.success(this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost })) + this.notifier.success( + this.i18n('Account {{nameWithHost}} muted by the instance.', { nameWithHost: account.nameWithHost }) + ) account.mutedByInstance = true }, + err => this.notifier.error(err.message) + ) + } + }, + { + label: this.i18n('Mute server'), + isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId, + handler: async videoAbuse => { + this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host) + .subscribe( + () => { + this.notifier.success( + this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host }) + ) + }, + err => this.notifier.error(err.message) ) } diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index fbe3d6950..e0e1fbddf 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts @@ -85,7 +85,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { this.userService.updateUser(this.user.id, userUpdate).subscribe( () => { - this.notifier.success(this.i18n('User {{user.username}} updated.', { username: this.user.username })) + this.notifier.success(this.i18n('User {{username}} updated.', { username: this.user.username })) this.router.navigate([ '/admin/users/list' ]) }, diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index 2c0641f3a..5a99edc7f 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -292,7 +292,7 @@ class Emailer { const videoUrl = WEBSERVER.URL + videoAbuse.Video.getWatchStaticPath() const text = 'Hi,\n\n' + - `${WEBSERVER.HOST} received an abuse for the following video ${videoUrl}\n\n` + + `${WEBSERVER.HOST} received an abuse for the following video: ${videoUrl}\n\n` + 'Cheers,\n' + `${CONFIG.EMAIL.BODY.SIGNATURE}` diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts index ea9856213..ea943ffdf 100644 --- a/server/models/video/video-abuse.ts +++ b/server/models/video/video-abuse.ts @@ -1,4 +1,6 @@ -import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { + AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt, DefaultScope +} from 'sequelize-typescript' import { VideoAbuseObject } from '../../../shared/models/activitypub/objects' import { VideoAbuse } from '../../../shared/models/videos' import { @@ -14,7 +16,40 @@ import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/const import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models' import * as Bluebird from 'bluebird' import { literal, Op } from 'sequelize' +import { ThumbnailModel } from './thumbnail' +import { VideoChannelModel } from './video-channel' +import { ActorModel } from '../activitypub/actor' +import { VideoBlacklistModel } from './video-blacklist' +@DefaultScope(() => ({ + include: [ + { + model: AccountModel, + required: true + }, + { + model: VideoModel, + required: false, + include: [ + { + model: ThumbnailModel + }, + { + model: VideoChannelModel.unscoped(), + include: [ + { + model: ActorModel + } + ] + }, + { + attributes: [ 'id', 'reason', 'unfederated' ], + model: VideoBlacklistModel + } + ] + } + ] +})) @Table({ tableName: 'videoAbuse', indexes: [ @@ -114,16 +149,8 @@ export class VideoAbuseModel extends Model { [Op.notIn]: literal('(' + buildBlockedAccountSQL(serverAccountId, userAccountId) + ')') } }, - include: [ - { - model: AccountModel, - required: true - }, - { - model: VideoModel, - required: false - } - ] + col: 'VideoAbuseModel.id', + distinct: true } return VideoAbuseModel.findAndCountAll(query) @@ -151,7 +178,10 @@ export class VideoAbuseModel extends Model { uuid: video.uuid, name: video.name, nsfw: video.nsfw, - deleted: !this.Video + deleted: !this.Video, + blacklisted: this.Video && this.Video.isBlacklisted(), + thumbnailPath: this.Video?.getMiniatureStaticPath(), + channel: this.Video?.VideoChannel.toFormattedSummaryJSON() || this.deletedVideo?.channel }, createdAt: this.createdAt } diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 2636ebd8e..f32216e90 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -810,7 +810,7 @@ export class VideoModel extends Model { if (instance.VideoAbuses.length === 0) return undefined } - const details = instance.toFormattedJSON() + const details = instance.toFormattedDetailsJSON() for (const abuse of instance.VideoAbuses) { tasks.push((_ => { diff --git a/server/typings/models/video/video-abuse.ts b/server/typings/models/video/video-abuse.ts index 49bd1ff2e..54acccdf5 100644 --- a/server/typings/models/video/video-abuse.ts +++ b/server/typings/models/video/video-abuse.ts @@ -1,6 +1,6 @@ import { VideoAbuseModel } from '../../../models/video/video-abuse' import { PickWith } from '../../utils' -import { MVideo } from './video' +import { MVideoAccountLightBlacklistAllFiles } from './video' import { MAccountDefault, MAccountFormattable } from '../account' type Use = PickWith @@ -16,12 +16,12 @@ export type MVideoAbuseId = Pick export type MVideoAbuseVideo = MVideoAbuse & Pick & - Use<'Video', MVideo> + Use<'Video', MVideoAccountLightBlacklistAllFiles> export type MVideoAbuseAccountVideo = MVideoAbuse & Pick & - Use<'Video', MVideo> & + Use<'Video', MVideoAccountLightBlacklistAllFiles> & Use<'Account', MAccountDefault> // ############################################################################ @@ -31,4 +31,5 @@ export type MVideoAbuseAccountVideo = export type MVideoAbuseFormattable = MVideoAbuse & Use<'Account', MAccountFormattable> & - Use<'Video', Pick> + Use<'Video', Pick> diff --git a/shared/models/videos/abuse/video-abuse.model.ts b/shared/models/videos/abuse/video-abuse.model.ts index b47ee05a0..953193e5e 100644 --- a/shared/models/videos/abuse/video-abuse.model.ts +++ b/shared/models/videos/abuse/video-abuse.model.ts @@ -1,6 +1,7 @@ import { Account } from '../../actors/index' import { VideoConstant } from '../video-constant.model' import { VideoAbuseState } from './video-abuse-state.model' +import { VideoChannelSummary } from '../channel/video-channel.model' export interface VideoAbuse { id: number @@ -16,6 +17,9 @@ export interface VideoAbuse { uuid: string nsfw: boolean deleted: boolean + blacklisted: boolean + thumbnailPath?: string + channel?: VideoChannelSummary } createdAt: Date