aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2020-04-18 22:57:20 +0200
committerRigel Kent <par@rigelk.eu>2020-05-01 16:41:02 +0200
commit5fd4ca0051c7e7f3f8c47bcbde5cab0c56532e64 (patch)
tree1a62c391b834589532a074355bc218352b17b1e9
parent844db39ee56ff0dd59a96acfc68f10f9ac53000b (diff)
downloadPeerTube-5fd4ca0051c7e7f3f8c47bcbde5cab0c56532e64.tar.gz
PeerTube-5fd4ca0051c7e7f3f8c47bcbde5cab0c56532e64.tar.zst
PeerTube-5fd4ca0051c7e7f3f8c47bcbde5cab0c56532e64.zip
Add nth abuse count for a given video, add reporter/reportee reports stats
-rw-r--r--client/src/app/+admin/moderation/moderation.component.scss34
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html66
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss21
-rw-r--r--client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts4
-rw-r--r--client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html13
-rw-r--r--server/models/video/video-abuse.ts96
-rw-r--r--shared/models/videos/abuse/video-abuse.model.ts11
7 files changed, 206 insertions, 39 deletions
diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss
index 9ceff1161..ef6a39b5d 100644
--- a/client/src/app/+admin/moderation/moderation.component.scss
+++ b/client/src/app/+admin/moderation/moderation.component.scss
@@ -7,19 +7,23 @@
7 margin-right: 30px; 7 margin-right: 30px;
8} 8}
9 9
10.moderation-expanded-label { 10.moderation-expanded {
11 font-weight: $font-semibold; 11 font-size: 90%;
12 display: inline-block;
13 vertical-align: top;
14 text-align: right;
15}
16
17.moderation-expanded-text {
18 display: inline-block;
19 word-wrap: break-word;
20 12
21 ::ng-deep p:last-child { 13 .moderation-expanded-label {
22 margin-bottom: 0px !important; 14 font-weight: $font-semibold;
15 display: inline-block;
16 vertical-align: top;
17 text-align: right;
18 }
19
20 .moderation-expanded-text {
21 display: inline-flex;
22 word-wrap: break-word;
23
24 ::ng-deep p:last-child {
25 margin-bottom: 0px !important;
26 }
23 } 27 }
24} 28}
25 29
@@ -58,3 +62,9 @@
58.chip { 62.chip {
59 @include chip; 63 @include chip;
60} 64}
65
66my-action-dropdown.show {
67 ::ng-deep .dropdown-root {
68 display: block !important;
69 }
70}
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 c1ce093d7..67ef28408 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
@@ -54,7 +54,13 @@
54 <td *ngIf="!videoAbuse.video.deleted"> 54 <td *ngIf="!videoAbuse.video.deleted">
55 <a [href]="getVideoUrl(videoAbuse)" class="video-abuse-video-link" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer"> 55 <a [href]="getVideoUrl(videoAbuse)" class="video-abuse-video-link" i18n-title title="Open video in a new tab" target="_blank" rel="noopener noreferrer">
56 <div class="video-abuse-video"> 56 <div class="video-abuse-video">
57 <div class="video-abuse-video-image"><img [src]="videoAbuse.video.thumbnailPath"></div> 57 <div class="video-abuse-video-image">
58 <img [src]="videoAbuse.video.thumbnailPath">
59 <span
60 class="video-abuse-video-image-label" *ngIf="videoAbuse.count > 1"
61 i18n-title title="This video has been reported multiple times."
62 >{{ videoAbuse.nth }}/{{ videoAbuse.count }}</span>
63 </div>
58 <div class="video-abuse-video-text"> 64 <div class="video-abuse-video-text">
59 <div> 65 <div>
60 {{ videoAbuse.video.name }} 66 {{ videoAbuse.video.name }}
@@ -85,11 +91,14 @@
85 <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse"> 91 <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
86 <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> 92 <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
87 <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span> 93 <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
88 <span *ngIf="videoAbuse.moderationComment" [title]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span> 94 <span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
89 </td> 95 </td>
90 96
91 <td class="action-cell"> 97 <td class="action-cell">
92 <my-action-dropdown placement="bottom-right auto" container="body" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown> 98 <my-action-dropdown
99 [ngClass]="{ 'show': expanded }" placement="bottom-right auto" container="body"
100 i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
101 ></my-action-dropdown>
93 </td> 102 </td>
94 </tr> 103 </tr>
95 </ng-template> 104 </ng-template>
@@ -97,14 +106,59 @@
97 <ng-template pTemplate="rowexpansion" let-videoAbuse> 106 <ng-template pTemplate="rowexpansion" let-videoAbuse>
98 <tr> 107 <tr>
99 <td class="expand-cell" colspan="6"> 108 <td class="expand-cell" colspan="6">
100 <div class="d-flex"> 109 <div class="d-flex moderation-expanded">
110 <!-- report metadata -->
101 <div class="col-8"> 111 <div class="col-8">
102 <div class="d-flex"> 112 <div class="d-flex">
103 <span class="col-3 moderation-expanded-label" i18n>Reason:</span> 113 <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
114 <span class="col-9 moderation-expanded-text">
115 <div class="chip">
116 <img
117 class="avatar"
118 [src]="videoAbuse.reporterAccount.avatar.path"
119 (error)="switchToDefaultAvatar($event)"
120 alt="Avatar"
121 >
122 <div>
123 <span class="text-muted">{{ createByString(videoAbuse.reporterAccount) }}</span>
124 </div>
125 </div>
126 <a routerLink="/admin/moderation/video-abuses/list" class="ml-auto text-muted video-abuse-links" i18n>
127 {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
128 </a>
129 </span>
130 </div>
131 <div class="d-flex">
132 <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
133 <span class="col-9 moderation-expanded-text">
134 <div class="chip">
135 <img
136 class="avatar"
137 [src]="videoAbuse.video.channel.ownerAccount?.avatar.path"
138 (error)="switchToDefaultAvatar($event)"
139 alt="Avatar"
140 >
141 <div>
142 <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? createByString(videoAbuse.video.channel.ownerAccount) : '' }}</span>
143 </div>
144 </div>
145 <a routerLink="/admin/moderation/video-abuses/list" class="ml-auto text-muted video-abuse-links" *ngIf="!videoAbuse.video.deleted" i18n>
146 {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
147 </a>
148 </span>
149 </div>
150 <div class="d-flex">
151 <span class="col-3 moderation-expanded-label" i18n>Updated</span>
152 <time class="col-9 moderation-expanded-text video-abuse-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time>
153 </div>
154
155 <!-- report text -->
156 <div class="mt-3 d-flex">
157 <span class="col-3 moderation-expanded-label" i18n>Report</span>
104 <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span> 158 <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
105 </div> 159 </div>
106 <div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment"> 160 <div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
107 <span class="col-3 moderation-expanded-label" i18n>Note:</span> 161 <span class="col-3 moderation-expanded-label" i18n>Note</span>
108 <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span> 162 <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
109 </div> 163 </div>
110 </div> 164 </div>
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
index 9b60c39dc..b5dc53b3a 100644
--- 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
@@ -9,6 +9,15 @@
9 } 9 }
10} 10}
11 11
12.video-abuse-date-updated {
13 font-size: 90%;
14 margin-top: .1rem;
15}
16
17.video-abuse-links {
18 @include disable-default-a-behaviour;
19}
20
12.video-abuse-video-link { 21.video-abuse-video-link {
13 @include disable-outline; 22 @include disable-outline;
14 position: relative; 23 position: relative;
@@ -32,6 +41,7 @@
32 display: inline-flex; 41 display: inline-flex;
33 justify-content: center; 42 justify-content: center;
34 align-items: center; 43 align-items: center;
44 position: relative;
35 45
36 img { 46 img {
37 height: 100%; 47 height: 100%;
@@ -42,6 +52,17 @@
42 span { 52 span {
43 color: var(--inputPlaceholderColor); 53 color: var(--inputPlaceholderColor);
44 } 54 }
55
56 .video-abuse-video-image-label {
57 @include static-thumbnail-overlay;
58 position: absolute;
59 border-radius: 3px;
60 font-size: 10px;
61 padding: 0 3px;
62 line-height: 1.3;
63 bottom: 2px;
64 right: 2px;
65 }
45 } 66 }
46 67
47 .video-abuse-video-text { 68 .video-abuse-video-text {
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 6dcf96ccf..e4e78cdf7 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
@@ -46,7 +46,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
46 private i18n: I18n, 46 private i18n: I18n,
47 private markdownRenderer: MarkdownService, 47 private markdownRenderer: MarkdownService,
48 private sanitizer: DomSanitizer, 48 private sanitizer: DomSanitizer,
49 private route: ActivatedRoute, 49 private route: ActivatedRoute
50 ) { 50 ) {
51 super() 51 super()
52 52
@@ -223,7 +223,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
223 } 223 }
224 224
225 getVideoEmbed (videoAbuse: VideoAbuse) { 225 getVideoEmbed (videoAbuse: VideoAbuse) {
226 const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl() 226 const absoluteAPIUrl = 'http://localhost:9000' || getAbsoluteAPIUrl() // TODO
227 const embedUrl = buildVideoLink({ 227 const embedUrl = buildVideoLink({
228 baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid, 228 baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid,
229 warningTitle: false 229 warningTitle: false
diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
index 2f9fc8ba4..c5c0fdbbf 100644
--- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html
@@ -30,9 +30,16 @@
30 </a> 30 </a>
31 </td> 31 </td>
32 32
33 <td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td> 33 <ng-container *ngIf="videoBlacklist.reason">
34 <td>{{ booleanToText(videoBlacklist.unfederated) }}</td> 34 <td class="c-hand" [pRowToggler]="videoBlacklist">{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
35 <td>{{ videoBlacklist.createdAt }}</td> 35 <td class="c-hand" [pRowToggler]="videoBlacklist">{{ booleanToText(videoBlacklist.unfederated) }}</td>
36 <td class="c-hand" [pRowToggler]="videoBlacklist">{{ videoBlacklist.createdAt }}</td>
37 </ng-container>
38 <ng-container *ngIf="!videoBlacklist.reason">
39 <td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
40 <td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
41 <td>{{ videoBlacklist.createdAt }}</td>
42 </ng-container>
36 43
37 <td class="action-cell"> 44 <td class="action-cell">
38 <my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown> 45 <my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 5ead02eca..d68608ca6 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -11,14 +11,13 @@ import {
11import { AccountModel } from '../account/account' 11import { AccountModel } from '../account/account'
12import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils' 12import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
13import { VideoModel } from './video' 13import { VideoModel } from './video'
14import { VideoAbuseState, Video } from '../../../shared' 14import { VideoAbuseState, VideoDetails } from '../../../shared'
15import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' 15import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
16import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models' 16import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
17import * as Bluebird from 'bluebird' 17import * as Bluebird from 'bluebird'
18import { literal, Op } from 'sequelize' 18import { literal, Op } from 'sequelize'
19import { ThumbnailModel } from './thumbnail' 19import { ThumbnailModel } from './thumbnail'
20import { VideoChannelModel } from './video-channel' 20import { VideoChannelModel } from './video-channel'
21import { ActorModel } from '../activitypub/actor'
22import { VideoBlacklistModel } from './video-blacklist' 21import { VideoBlacklistModel } from './video-blacklist'
23 22
24export enum ScopeNames { 23export enum ScopeNames {
@@ -78,9 +77,73 @@ export enum ScopeNames {
78 }) 77 })
79 } 78 }
80 79
81 console.log(where)
82
83 return { 80 return {
81 attributes: {
82 include: [
83 [
84 literal(
85 '(' +
86 'SELECT t.count ' +
87 'FROM ( ' +
88 'SELECT id, ' +
89 'count(id) OVER (PARTITION BY "videoId") ' +
90 'FROM "videoAbuse" ' +
91 ') t ' +
92 'WHERE t.id = "VideoAbuseModel".id ' +
93 ')'
94 ),
95 'countReportsForVideo'
96 ],
97 [
98 literal(
99 '(' +
100 'SELECT t.nth ' +
101 'FROM ( ' +
102 'SELECT id, ' +
103 'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
104 'FROM "videoAbuse" ' +
105 ') t ' +
106 'WHERE t.id = "VideoAbuseModel".id ' +
107 ')'
108 ),
109 'nthReportForVideo'
110 ],
111 [
112 literal(
113 '(' +
114 'SELECT count("videoAbuse"."id") ' +
115 'FROM "videoAbuse" ' +
116 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
117 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
118 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
119 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
120 ')'
121 ),
122 'countReportsForReporter'
123 ],
124 [
125 literal(
126 '(' +
127 'WITH ' +
128 'ids AS ( ' +
129 'SELECT "account"."id" ' +
130 'FROM "account" ' +
131 'INNER JOIN "videoChannel" ON "videoChannel"."accountId" = "account"."id" ' +
132 'INNER JOIN "video" ON "video"."channelId" = "videoChannel"."id" ' +
133 'WHERE "video"."id" = "VideoAbuseModel"."videoId" ' +
134 ') ' +
135 'SELECT count("videoAbuse"."id") ' +
136 'FROM "videoAbuse" ' +
137 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
138 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
139 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
140 'INNER JOIN ids ON "account"."id" = ids.id ' +
141 ')'
142 ),
143 'countReportsForReportee'
144 ]
145 ]
146 },
84 include: [ 147 include: [
85 { 148 {
86 model: AccountModel, 149 model: AccountModel,
@@ -96,13 +159,8 @@ export enum ScopeNames {
96 model: ThumbnailModel 159 model: ThumbnailModel
97 }, 160 },
98 { 161 {
99 model: VideoChannelModel.unscoped(), 162 model: VideoChannelModel.scope([ 'WITH_ACTOR', 'WITH_ACCOUNT' ]),
100 where: { ...search(options.searchVideoChannel, 'name') }, 163 where: { ...search(options.searchVideoChannel, 'name') }
101 include: [
102 {
103 model: ActorModel
104 }
105 ]
106 }, 164 },
107 { 165 {
108 attributes: [ 'id', 'reason', 'unfederated' ], 166 attributes: [ 'id', 'reason', 'unfederated' ],
@@ -149,7 +207,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
149 @AllowNull(true) 207 @AllowNull(true)
150 @Default(null) 208 @Default(null)
151 @Column(DataType.JSONB) 209 @Column(DataType.JSONB)
152 deletedVideo: Video 210 deletedVideo: VideoDetails
153 211
154 @CreatedAt 212 @CreatedAt
155 createdAt: Date 213 createdAt: Date
@@ -229,6 +287,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
229 } 287 }
230 288
231 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { 289 toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
290 const countReportsForVideo = this.get('countReportsForVideo') as number
291 const nthReportForVideo = this.get('nthReportForVideo') as number
292 const countReportsForReporter = this.get('countReportsForReporter') as number
293 const countReportsForReportee = this.get('countReportsForReportee') as number
294
232 const video = this.Video 295 const video = this.Video
233 ? this.Video 296 ? this.Video
234 : this.deletedVideo 297 : this.deletedVideo
@@ -250,9 +313,14 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
250 deleted: !this.Video, 313 deleted: !this.Video,
251 blacklisted: this.Video && this.Video.isBlacklisted(), 314 blacklisted: this.Video && this.Video.isBlacklisted(),
252 thumbnailPath: this.Video?.getMiniatureStaticPath(), 315 thumbnailPath: this.Video?.getMiniatureStaticPath(),
253 channel: this.Video?.VideoChannel.toFormattedSummaryJSON() || this.deletedVideo?.channel 316 channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
254 }, 317 },
255 createdAt: this.createdAt 318 createdAt: this.createdAt,
319 updatedAt: this.updatedAt,
320 count: countReportsForVideo || 0,
321 nth: nthReportForVideo || 0,
322 countReportsForReporter: countReportsForReporter || 0,
323 countReportsForReportee: countReportsForReportee || 0
256 } 324 }
257 } 325 }
258 326
diff --git a/shared/models/videos/abuse/video-abuse.model.ts b/shared/models/videos/abuse/video-abuse.model.ts
index 953193e5e..f2c2cdc41 100644
--- a/shared/models/videos/abuse/video-abuse.model.ts
+++ b/shared/models/videos/abuse/video-abuse.model.ts
@@ -1,7 +1,7 @@
1import { Account } from '../../actors/index' 1import { Account } from '../../actors/index'
2import { VideoConstant } from '../video-constant.model' 2import { VideoConstant } from '../video-constant.model'
3import { VideoAbuseState } from './video-abuse-state.model' 3import { VideoAbuseState } from './video-abuse-state.model'
4import { VideoChannelSummary } from '../channel/video-channel.model' 4import { VideoChannel } from '../channel/video-channel.model'
5 5
6export interface VideoAbuse { 6export interface VideoAbuse {
7 id: number 7 id: number
@@ -19,8 +19,15 @@ export interface VideoAbuse {
19 deleted: boolean 19 deleted: boolean
20 blacklisted: boolean 20 blacklisted: boolean
21 thumbnailPath?: string 21 thumbnailPath?: string
22 channel?: VideoChannelSummary 22 channel?: VideoChannel
23 } 23 }
24 24
25 createdAt: Date 25 createdAt: Date
26 updatedAt: Date
27
28 count?: number
29 nth?: number
30
31 countReportsForReporter?: number
32 countReportsForReportee?: number
26} 33}