predefined report reasons & improved reporter UI (#2842)
- added `startAt` and `endAt` optional timestamps to help pin down reported sections of a video - added predefined report reasons - added video player with report modal
This commit is contained in:
parent
07aea1a264
commit
1ebddadd07
|
@ -42,6 +42,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p-calendar {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
::ng-deep {
|
||||||
|
.ui-widget-content {
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@include peertube-input-text(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.screenratio {
|
.screenratio {
|
||||||
div {
|
div {
|
||||||
@include miniature-thumbnail;
|
@include miniature-thumbnail;
|
||||||
|
|
|
@ -57,6 +57,22 @@
|
||||||
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
|
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
|
||||||
|
<span class="col-3"></span>
|
||||||
|
<span class="col-9">
|
||||||
|
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()">
|
||||||
|
<div>{{ reason.label }}</div>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="videoAbuse.startAt" class="mt-2 d-flex">
|
||||||
|
<span class="col-3 moderation-expanded-label" i18n>Reported part</span>
|
||||||
|
<span class="col-9">
|
||||||
|
{{ startAt }}<ng-container *ngIf="videoAbuse.endAt"> - {{ endAt }}</ng-container>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
|
<div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
|
||||||
<span class="col-3 moderation-expanded-label" i18n>Note</span>
|
<span class="col-3 moderation-expanded-label" i18n>Note</span>
|
||||||
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
|
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { Account } from '@app/shared/account/account.model'
|
|
||||||
import { Actor } from '@app/shared/actor/actor.model'
|
import { Actor } from '@app/shared/actor/actor.model'
|
||||||
|
import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model'
|
||||||
import { ProcessedVideoAbuse } from './video-abuse-list.component'
|
import { ProcessedVideoAbuse } from './video-abuse-list.component'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { durationToString } from '@app/shared/misc/utils'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-abuse-details',
|
selector: 'my-video-abuse-details',
|
||||||
|
@ -11,6 +13,39 @@ import { ProcessedVideoAbuse } from './video-abuse-list.component'
|
||||||
export class VideoAbuseDetailsComponent {
|
export class VideoAbuseDetailsComponent {
|
||||||
@Input() videoAbuse: ProcessedVideoAbuse
|
@Input() videoAbuse: ProcessedVideoAbuse
|
||||||
|
|
||||||
|
private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string }
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
private i18n: I18n
|
||||||
|
) {
|
||||||
|
this.predefinedReasonsTranslations = {
|
||||||
|
violentOrRepulsive: this.i18n('Violent or Repulsive'),
|
||||||
|
hatefulOrAbusive: this.i18n('Hateful or Abusive'),
|
||||||
|
spamOrMisleading: this.i18n('Spam or Misleading'),
|
||||||
|
privacy: this.i18n('Privacy'),
|
||||||
|
rights: this.i18n('Rights'),
|
||||||
|
serverRules: this.i18n('Server rules'),
|
||||||
|
thumbnails: this.i18n('Thumbnails'),
|
||||||
|
captions: this.i18n('Captions')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get startAt () {
|
||||||
|
return durationToString(this.videoAbuse.startAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
get endAt () {
|
||||||
|
return durationToString(this.videoAbuse.endAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPredefinedReasons () {
|
||||||
|
if (!this.videoAbuse.predefinedReasons) return []
|
||||||
|
return this.videoAbuse.predefinedReasons.map(r => ({
|
||||||
|
id: r,
|
||||||
|
label: this.predefinedReasonsTranslations[r]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
switchToDefaultAvatar ($event: Event) {
|
switchToDefaultAvatar ($event: Event) {
|
||||||
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,13 +11,13 @@ import { ModerationCommentModalComponent } from './moderation-comment-modal.comp
|
||||||
import { Video } from '../../../shared/video/video.model'
|
import { Video } from '../../../shared/video/video.model'
|
||||||
import { MarkdownService } from '@app/shared/renderer'
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
import { Actor } from '@app/shared/actor/actor.model'
|
import { Actor } from '@app/shared/actor/actor.model'
|
||||||
import { buildVideoLink, buildVideoEmbed } from 'src/assets/player/utils'
|
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
|
||||||
import { getAbsoluteAPIUrl } from '@app/shared/misc/utils'
|
|
||||||
import { DomSanitizer } from '@angular/platform-browser'
|
import { DomSanitizer } from '@angular/platform-browser'
|
||||||
import { BlocklistService } from '@app/shared/blocklist'
|
import { BlocklistService } from '@app/shared/blocklist'
|
||||||
import { VideoService } from '@app/shared/video/video.service'
|
import { VideoService } from '@app/shared/video/video.service'
|
||||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||||
import { filter } from 'rxjs/operators'
|
import { filter } from 'rxjs/operators'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
export type ProcessedVideoAbuse = VideoAbuse & {
|
export type ProcessedVideoAbuse = VideoAbuse & {
|
||||||
moderationCommentHtml?: string,
|
moderationCommentHtml?: string,
|
||||||
|
@ -259,12 +259,15 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoEmbed (videoAbuse: VideoAbuse) {
|
getVideoEmbed (videoAbuse: VideoAbuse) {
|
||||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
return buildVideoEmbed(
|
||||||
const embedUrl = buildVideoLink({
|
buildVideoLink({
|
||||||
baseUrl: absoluteAPIUrl + '/videos/embed/' + videoAbuse.video.uuid,
|
baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`,
|
||||||
warningTitle: false
|
title: false,
|
||||||
})
|
warningTitle: false,
|
||||||
return buildVideoEmbed(embedUrl)
|
startTime: videoAbuse.startAt,
|
||||||
|
stopTime: videoAbuse.endAt
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToDefaultAvatar ($event: Event) {
|
switchToDefaultAvatar ($event: Event) {
|
||||||
|
|
|
@ -46,7 +46,7 @@ export class RestService {
|
||||||
addObjectParams (params: HttpParams, object: { [ name: string ]: any }) {
|
addObjectParams (params: HttpParams, object: { [ name: string ]: any }) {
|
||||||
for (const name of Object.keys(object)) {
|
for (const name of Object.keys(object)) {
|
||||||
const value = object[name]
|
const value = object[name]
|
||||||
if (!value) continue
|
if (value === undefined || value === null) continue
|
||||||
|
|
||||||
if (Array.isArray(value) && value.length !== 0) {
|
if (Array.isArray(value) && value.length !== 0) {
|
||||||
for (const v of value) params = params.append(name, v)
|
for (const v of value) params = params.append(name, v)
|
||||||
|
@ -93,7 +93,7 @@ export class RestService {
|
||||||
|
|
||||||
return t
|
return t
|
||||||
})
|
})
|
||||||
.filter(t => !!t)
|
.filter(t => !!t || t === 0)
|
||||||
|
|
||||||
if (matchedTokens.length === 0) continue
|
if (matchedTokens.length === 0) continue
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ export class RestService {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
search: searchTokens.join(' '),
|
search: searchTokens.join(' ') || undefined,
|
||||||
|
|
||||||
...additionalFilters
|
...additionalFilters
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { SortMeta } from 'primeng/api'
|
import { SortMeta } from 'primeng/api'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { ResultList, VideoAbuse, VideoAbuseUpdate, VideoAbuseState } from '../../../../../shared'
|
import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '../../../../../shared'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { RestExtractor, RestPagination, RestService } from '../rest'
|
import { RestExtractor, RestPagination, RestService } from '../rest'
|
||||||
|
import { omit } from 'lodash-es'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoAbuseService {
|
export class VideoAbuseService {
|
||||||
|
@ -51,7 +52,8 @@ export class VideoAbuseService {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
searchReporter: { prefix: 'reporter:' },
|
searchReporter: { prefix: 'reporter:' },
|
||||||
searchReportee: { prefix: 'reportee:' }
|
searchReportee: { prefix: 'reportee:' },
|
||||||
|
predefinedReason: { prefix: 'tag:' }
|
||||||
})
|
})
|
||||||
|
|
||||||
params = this.restService.addObjectParams(params, filters)
|
params = this.restService.addObjectParams(params, filters)
|
||||||
|
@ -63,9 +65,10 @@ export class VideoAbuseService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
reportVideo (id: number, reason: string) {
|
reportVideo (parameters: { id: number } & VideoAbuseCreate) {
|
||||||
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
|
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse'
|
||||||
const body = { reason }
|
|
||||||
|
const body = omit(parameters, [ 'id' ])
|
||||||
|
|
||||||
return this.authHttp.post(url, body)
|
return this.authHttp.post(url, body)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<ng-template #modal>
|
<ng-template #modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Blocklist video</h4>
|
<h4 i18n class="modal-title">Block video "{{ video.name }}"</h4>
|
||||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
<form novalidate [formGroup]="form" (ngSubmit)="block()">
|
<form novalidate [formGroup]="form" (ngSubmit)="block()">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<textarea
|
<textarea
|
||||||
i18n-placeholder placeholder="Reason..." formControlName="reason"
|
i18n-placeholder placeholder="Please describe the reason..." formControlName="reason"
|
||||||
[ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
|
[ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div *ngIf="formErrors.reason" class="form-error">
|
<div *ngIf="formErrors.reason" class="form-error">
|
||||||
|
|
|
@ -1,38 +1,97 @@
|
||||||
<ng-template #modal>
|
<ng-template #modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Report video</h4>
|
<h4 i18n class="modal-title">Report video "{{ video.name }}"</h4>
|
||||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
||||||
<div i18n class="information">
|
|
||||||
Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form novalidate [formGroup]="form" (ngSubmit)="report()">
|
<form novalidate [formGroup]="form" (ngSubmit)="report()">
|
||||||
<div class="form-group">
|
|
||||||
<textarea
|
<div class="row">
|
||||||
i18n-placeholder placeholder="Reason..." formControlName="reason"
|
<div class="col-5 form-group">
|
||||||
[ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
|
|
||||||
></textarea>
|
<label i18n for="reportPredefinedReasons">What is the issue?</label>
|
||||||
<div *ngIf="formErrors.reason" class="form-error">
|
|
||||||
{{ formErrors.reason }}
|
<div class="ml-2 mt-2 d-flex flex-column">
|
||||||
|
<ng-container formGroupName="predefinedReasons">
|
||||||
|
<div class="form-group" *ngFor="let reason of predefinedReasons">
|
||||||
|
<my-peertube-checkbox formControlName="{{reason.id}}" labelText="{{reason.label}}">
|
||||||
|
<ng-template *ngIf="reason.help" ptTemplate="help">
|
||||||
|
<div [innerHTML]="reason.help"></div>
|
||||||
|
</ng-template>
|
||||||
|
<ng-container *ngIf="reason.description" ngProjectAs="description">
|
||||||
|
<div [innerHTML]="reason.description"></div>
|
||||||
|
</ng-container>
|
||||||
|
</my-peertube-checkbox>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-7">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-12 col-lg-9 mb-2">
|
||||||
|
<div class="screenratio">
|
||||||
|
<div [innerHTML]="embedHtml"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-1 start-at" formGroupName="timestamp">
|
||||||
|
<my-peertube-checkbox
|
||||||
|
formControlName="hasStart"
|
||||||
|
i18n-labelText labelText="Start at"
|
||||||
|
></my-peertube-checkbox>
|
||||||
|
|
||||||
|
<my-timestamp-input
|
||||||
|
[timestamp]="timestamp.startAt"
|
||||||
|
[maxTimestamp]="video.duration"
|
||||||
|
formControlName="startAt"
|
||||||
|
inputName="startAt"
|
||||||
|
>
|
||||||
|
</my-timestamp-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 stop-at" formGroupName="timestamp" *ngIf="timestamp.hasStart">
|
||||||
|
<my-peertube-checkbox
|
||||||
|
formControlName="hasEnd"
|
||||||
|
i18n-labelText labelText="Stop at"
|
||||||
|
></my-peertube-checkbox>
|
||||||
|
|
||||||
|
<my-timestamp-input
|
||||||
|
[timestamp]="timestamp.endAt"
|
||||||
|
[maxTimestamp]="video.duration"
|
||||||
|
formControlName="endAt"
|
||||||
|
inputName="endAt"
|
||||||
|
>
|
||||||
|
</my-timestamp-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div i18n class="information">
|
||||||
|
Your report will be sent to moderators of {{ currentHost }}<ng-container *ngIf="isRemoteVideo()"> and will be forwarded to the video origin ({{ originHost }}) too</ng-container>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea
|
||||||
|
i18n-placeholder placeholder="Please describe the issue..." formControlName="reason" ngbAutofocus
|
||||||
|
[ngClass]="{ 'input-error': formErrors['reason'] }" class="form-control"
|
||||||
|
></textarea>
|
||||||
|
<div *ngIf="formErrors.reason" class="form-error">
|
||||||
|
{{ formErrors.reason }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group inputs">
|
<div class="form-group inputs">
|
||||||
<input
|
<input
|
||||||
type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
|
type="button" role="button" i18n-value value="Cancel" class="action-button action-button-cancel"
|
||||||
(click)="hide()" (key.enter)="hide()"
|
(click)="hide()" (key.enter)="hide()"
|
||||||
>
|
>
|
||||||
|
<input type="submit" i18n-value value="Submit" class="action-button-submit" [disabled]="!form.valid">
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
|
||||||
type="submit" i18n-value value="Submit" class="action-button-submit"
|
|
||||||
[disabled]="!form.valid"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -8,3 +8,20 @@
|
||||||
textarea {
|
textarea {
|
||||||
@include peertube-textarea(100%, 100px);
|
@include peertube-textarea(100%, 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.start-at,
|
||||||
|
.stop-at {
|
||||||
|
width: 300px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
my-timestamp-input {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.screenratio {
|
||||||
|
@include large-screen-ratio($selector: 'div, ::ng-deep iframe') {
|
||||||
|
left: 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,10 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||||
import { VideoAbuseService } from '@app/shared/video-abuse'
|
import { VideoAbuseService } from '@app/shared/video-abuse'
|
||||||
import { Video } from '@app/shared/video/video.model'
|
import { Video } from '@app/shared/video/video.model'
|
||||||
|
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
|
||||||
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||||
|
import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model'
|
||||||
|
import { mapValues, pickBy } from 'lodash-es'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-report',
|
selector: 'my-video-report',
|
||||||
|
@ -20,6 +24,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
@ViewChild('modal', { static: true }) modal: NgbModal
|
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||||
|
|
||||||
error: string = null
|
error: string = null
|
||||||
|
predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
|
||||||
|
embedHtml: SafeHtml
|
||||||
|
|
||||||
private openedModal: NgbModalRef
|
private openedModal: NgbModalRef
|
||||||
|
|
||||||
|
@ -29,6 +35,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
private videoAbuseValidatorsService: VideoAbuseValidatorsService,
|
private videoAbuseValidatorsService: VideoAbuseValidatorsService,
|
||||||
private videoAbuseService: VideoAbuseService,
|
private videoAbuseService: VideoAbuseService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -46,14 +53,82 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get timestamp () {
|
||||||
|
return this.form.get('timestamp').value
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoEmbed () {
|
||||||
|
return this.sanitizer.bypassSecurityTrustHtml(
|
||||||
|
buildVideoEmbed(
|
||||||
|
buildVideoLink({
|
||||||
|
baseUrl: this.video.embedUrl,
|
||||||
|
title: false,
|
||||||
|
warningTitle: false
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.buildForm({
|
this.buildForm({
|
||||||
reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON
|
reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON,
|
||||||
|
predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null),
|
||||||
|
timestamp: {
|
||||||
|
hasStart: null,
|
||||||
|
startAt: null,
|
||||||
|
hasEnd: null,
|
||||||
|
endAt: null
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.predefinedReasons = [
|
||||||
|
{
|
||||||
|
id: 'violentOrRepulsive',
|
||||||
|
label: this.i18n('Violent or repulsive'),
|
||||||
|
help: this.i18n('Contains offensive, violent, or coarse language or iconography.')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hatefulOrAbusive',
|
||||||
|
label: this.i18n('Hateful or abusive'),
|
||||||
|
help: this.i18n('Contains abusive, racist or sexist language or iconography.')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'spamOrMisleading',
|
||||||
|
label: this.i18n('Spam, ad or false news'),
|
||||||
|
help: this.i18n('Contains marketing, spam, purposefully deceitful news, or otherwise misleading thumbnail/text/tags. Please provide reputable sources to report hoaxes.')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'privacy',
|
||||||
|
label: this.i18n('Privacy breach or doxxing'),
|
||||||
|
help: this.i18n('Contains personal information that could be used to track, identify, contact or impersonate someone (e.g. name, address, phone number, email, or credit card details).')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rights',
|
||||||
|
label: this.i18n('Intellectual property violation'),
|
||||||
|
help: this.i18n('Infringes my intellectual property or copyright, wrt. the regional rules with which the server must comply.')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'serverRules',
|
||||||
|
label: this.i18n('Breaks server rules'),
|
||||||
|
description: this.i18n('Anything not included in the above that breaks the terms of service, code of conduct, or general rules in place on the server.')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'thumbnails',
|
||||||
|
label: this.i18n('Thumbnails'),
|
||||||
|
help: this.i18n('The above can only be seen in thumbnails.')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'captions',
|
||||||
|
label: this.i18n('Captions'),
|
||||||
|
help: this.i18n('The above can only be seen in captions (please describe which).')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
this.embedHtml = this.getVideoEmbed()
|
||||||
}
|
}
|
||||||
|
|
||||||
show () {
|
show () {
|
||||||
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false })
|
this.openedModal = this.modalService.open(this.modal, { centered: true, keyboard: false, size: 'lg' })
|
||||||
}
|
}
|
||||||
|
|
||||||
hide () {
|
hide () {
|
||||||
|
@ -62,17 +137,24 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
report () {
|
report () {
|
||||||
const reason = this.form.value['reason']
|
const reason = this.form.get('reason').value
|
||||||
|
const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[]
|
||||||
|
const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
|
||||||
|
|
||||||
this.videoAbuseService.reportVideo(this.video.id, reason)
|
this.videoAbuseService.reportVideo({
|
||||||
.subscribe(
|
id: this.video.id,
|
||||||
() => {
|
reason,
|
||||||
this.notifier.success(this.i18n('Video reported.'))
|
predefinedReasons,
|
||||||
this.hide()
|
startAt: hasStart && startAt ? startAt : undefined,
|
||||||
},
|
endAt: hasEnd && endAt ? endAt : undefined
|
||||||
|
}).subscribe(
|
||||||
|
() => {
|
||||||
|
this.notifier.success(this.i18n('Video reported.'))
|
||||||
|
this.hide()
|
||||||
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
isRemoteVideo () {
|
isRemoteVideo () {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { peertubeTranslate, ServerConfig } from '../../../../../shared/models'
|
||||||
import { Actor } from '@app/shared/actor/actor.model'
|
import { Actor } from '@app/shared/actor/actor.model'
|
||||||
import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
|
import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
|
||||||
import { AuthUser } from '@app/core'
|
import { AuthUser } from '@app/core'
|
||||||
|
import { environment } from '../../../environments/environment'
|
||||||
|
|
||||||
export class Video implements VideoServerModel {
|
export class Video implements VideoServerModel {
|
||||||
byVideoChannel: string
|
byVideoChannel: string
|
||||||
|
@ -111,7 +112,7 @@ export class Video implements VideoServerModel {
|
||||||
this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath)
|
this.previewUrl = hash.previewUrl || (absoluteAPIUrl + hash.previewPath)
|
||||||
|
|
||||||
this.embedPath = hash.embedPath
|
this.embedPath = hash.embedPath
|
||||||
this.embedUrl = hash.embedUrl || (absoluteAPIUrl + hash.embedPath)
|
this.embedUrl = hash.embedUrl || (environment.embedUrl + hash.embedPath)
|
||||||
|
|
||||||
this.url = hash.url
|
this.url = hash.url
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
hmr: false,
|
hmr: false,
|
||||||
apiUrl: 'http://localhost:9001'
|
apiUrl: 'http://localhost:9001',
|
||||||
|
embedUrl: 'http://localhost:9001/videos/embed'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
hmr: true,
|
hmr: true,
|
||||||
apiUrl: ''
|
apiUrl: '',
|
||||||
|
embedUrl: 'http://localhost:9000/videos/embed'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
hmr: false,
|
hmr: false,
|
||||||
apiUrl: ''
|
apiUrl: '',
|
||||||
|
embedUrl: '/videos/embed'
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,5 +11,6 @@ import 'core-js/features/reflect'
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: false,
|
production: false,
|
||||||
hmr: false,
|
hmr: false,
|
||||||
apiUrl: 'http://localhost:9000'
|
apiUrl: 'http://localhost:9000',
|
||||||
|
embedUrl: 'http://localhost:9000/videos/embed'
|
||||||
}
|
}
|
||||||
|
|
|
@ -804,10 +804,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin chip {
|
@mixin chip {
|
||||||
|
--chip-radius: 5rem;
|
||||||
|
--chip-padding: .2rem .4rem;
|
||||||
$avatar-height: 1.2rem;
|
$avatar-height: 1.2rem;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 5rem;
|
border-radius: var(--chip-radius);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
color: pvar(--mainForegroundColor);
|
color: pvar(--mainForegroundColor);
|
||||||
|
@ -816,12 +818,17 @@
|
||||||
margin: .1rem;
|
margin: .1rem;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: .2rem .4rem;
|
padding: var(--chip-padding);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&.rectangular {
|
||||||
|
--chip-radius: .2rem;
|
||||||
|
--chip-padding: .2rem .3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
margin-left: -.4rem;
|
margin-left: -.4rem;
|
||||||
margin-right: .2rem;
|
margin-right: .2rem;
|
||||||
|
|
|
@ -86,7 +86,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.focus-visible, &:hover {
|
&.focus-visible, &:hover {
|
||||||
background-color: var(--mainColor);
|
background-color: var(--mainColor, dimgray);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse } from '../../../../shared'
|
import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { getFormattedObjects } from '../../../helpers/utils'
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
import { sequelizeTypescript } from '../../../initializers/database'
|
||||||
|
@ -74,6 +74,7 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
id: req.query.id,
|
id: req.query.id,
|
||||||
|
predefinedReason: req.query.predefinedReason,
|
||||||
search: req.query.search,
|
search: req.query.search,
|
||||||
state: req.query.state,
|
state: req.query.state,
|
||||||
videoIs: req.query.videoIs,
|
videoIs: req.query.videoIs,
|
||||||
|
@ -123,12 +124,16 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
|
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
|
||||||
reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
|
reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
|
||||||
|
const predefinedReasons = body.predefinedReasons?.map(r => videoAbusePredefinedReasonsMap[r])
|
||||||
|
|
||||||
const abuseToCreate = {
|
const abuseToCreate = {
|
||||||
reporterAccountId: reporterAccount.id,
|
reporterAccountId: reporterAccount.id,
|
||||||
reason: body.reason,
|
reason: body.reason,
|
||||||
videoId: videoInstance.id,
|
videoId: videoInstance.id,
|
||||||
state: VideoAbuseState.PENDING
|
state: VideoAbuseState.PENDING,
|
||||||
|
predefinedReasons,
|
||||||
|
startAt: body.startAt,
|
||||||
|
endAt: body.endAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
|
const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
|
||||||
|
@ -152,7 +157,7 @@ async function reportVideoAbuse (req: express.Request, res: express.Response) {
|
||||||
reporter: reporterAccount.Actor.getIdentifier()
|
reporter: reporterAccount.Actor.getIdentifier()
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('Abuse report for video %s created.', videoInstance.name)
|
logger.info('Abuse report for video "%s" created.', videoInstance.name)
|
||||||
|
|
||||||
return res.json({ videoAbuse: videoAbuseJSON }).end()
|
return res.json({ videoAbuse: videoAbuseJSON }).end()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
|
|
||||||
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
|
||||||
import { exists } from './misc'
|
import { exists, isArray } from './misc'
|
||||||
import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
|
import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
|
||||||
|
import { VideoAbusePredefinedReasonsString, videoAbusePredefinedReasonsMap } from '@shared/models/videos/abuse/video-abuse-reason.model'
|
||||||
|
|
||||||
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
|
const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES
|
||||||
|
|
||||||
|
@ -10,6 +11,22 @@ function isVideoAbuseReasonValid (value: string) {
|
||||||
return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
|
return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isVideoAbusePredefinedReasonValid (value: VideoAbusePredefinedReasonsString) {
|
||||||
|
return exists(value) && value in videoAbusePredefinedReasonsMap
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVideoAbusePredefinedReasonsValid (value: VideoAbusePredefinedReasonsString[]) {
|
||||||
|
return exists(value) && isArray(value) && value.every(v => v in videoAbusePredefinedReasonsMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVideoAbuseTimestampValid (value: number) {
|
||||||
|
return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVideoAbuseTimestampCoherent (endAt: number, { req }) {
|
||||||
|
return exists(req.body.startAt) && endAt > req.body.startAt
|
||||||
|
}
|
||||||
|
|
||||||
function isVideoAbuseModerationCommentValid (value: string) {
|
function isVideoAbuseModerationCommentValid (value: string) {
|
||||||
return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
|
return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
|
||||||
}
|
}
|
||||||
|
@ -28,8 +45,12 @@ function isAbuseVideoIsValid (value: VideoAbuseVideoIs) {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
isVideoAbuseStateValid,
|
|
||||||
isVideoAbuseReasonValid,
|
isVideoAbuseReasonValid,
|
||||||
isAbuseVideoIsValid,
|
isVideoAbusePredefinedReasonValid,
|
||||||
isVideoAbuseModerationCommentValid
|
isVideoAbusePredefinedReasonsValid,
|
||||||
|
isVideoAbuseTimestampValid,
|
||||||
|
isVideoAbuseTimestampCoherent,
|
||||||
|
isVideoAbuseModerationCommentValid,
|
||||||
|
isVideoAbuseStateValid,
|
||||||
|
isAbuseVideoIsValid
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 510
|
const LAST_MIGRATION_VERSION = 515
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction
|
||||||
|
queryInterface: Sequelize.QueryInterface
|
||||||
|
sequelize: Sequelize.Sequelize
|
||||||
|
}): Promise<void> {
|
||||||
|
await utils.queryInterface.addColumn('videoAbuse', 'predefinedReasons', {
|
||||||
|
type: Sequelize.ARRAY(Sequelize.INTEGER),
|
||||||
|
allowNull: true
|
||||||
|
})
|
||||||
|
|
||||||
|
await utils.queryInterface.addColumn('videoAbuse', 'startAt', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
})
|
||||||
|
|
||||||
|
await utils.queryInterface.addColumn('videoAbuse', 'endAt', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -1,4 +1,9 @@
|
||||||
import { ActivityCreate, ActivityFlag, VideoAbuseState } from '../../../../shared'
|
import {
|
||||||
|
ActivityCreate,
|
||||||
|
ActivityFlag,
|
||||||
|
VideoAbuseState,
|
||||||
|
videoAbusePredefinedReasonsMap
|
||||||
|
} from '../../../../shared'
|
||||||
import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
|
import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
|
||||||
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
|
@ -38,13 +43,21 @@ async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag,
|
||||||
|
|
||||||
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
|
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
|
||||||
const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
|
const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
|
||||||
|
const tags = Array.isArray(flag.tag) ? flag.tag : []
|
||||||
|
const predefinedReasons = tags.map(tag => videoAbusePredefinedReasonsMap[tag.name])
|
||||||
|
.filter(v => !isNaN(v))
|
||||||
|
const startAt = flag.startAt
|
||||||
|
const endAt = flag.endAt
|
||||||
|
|
||||||
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
|
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
|
||||||
const videoAbuseData = {
|
const videoAbuseData = {
|
||||||
reporterAccountId: account.id,
|
reporterAccountId: account.id,
|
||||||
reason: flag.content,
|
reason: flag.content,
|
||||||
videoId: video.id,
|
videoId: video.id,
|
||||||
state: VideoAbuseState.PENDING
|
state: VideoAbuseState.PENDING,
|
||||||
|
predefinedReasons,
|
||||||
|
startAt,
|
||||||
|
endAt
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
|
const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
|
||||||
|
|
|
@ -1,19 +1,46 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { body, param, query } from 'express-validator'
|
import { body, param, query } from 'express-validator'
|
||||||
import { exists, isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
|
import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
|
||||||
import {
|
import {
|
||||||
isAbuseVideoIsValid,
|
isAbuseVideoIsValid,
|
||||||
isVideoAbuseModerationCommentValid,
|
isVideoAbuseModerationCommentValid,
|
||||||
isVideoAbuseReasonValid,
|
isVideoAbuseReasonValid,
|
||||||
isVideoAbuseStateValid
|
isVideoAbuseStateValid,
|
||||||
|
isVideoAbusePredefinedReasonsValid,
|
||||||
|
isVideoAbusePredefinedReasonValid,
|
||||||
|
isVideoAbuseTimestampValid,
|
||||||
|
isVideoAbuseTimestampCoherent
|
||||||
} from '../../../helpers/custom-validators/video-abuses'
|
} from '../../../helpers/custom-validators/video-abuses'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares'
|
import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares'
|
||||||
import { areValidationErrors } from '../utils'
|
import { areValidationErrors } from '../utils'
|
||||||
|
|
||||||
const videoAbuseReportValidator = [
|
const videoAbuseReportValidator = [
|
||||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
param('videoId')
|
||||||
body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'),
|
.custom(isIdOrUUIDValid)
|
||||||
|
.not()
|
||||||
|
.isEmpty()
|
||||||
|
.withMessage('Should have a valid videoId'),
|
||||||
|
body('reason')
|
||||||
|
.custom(isVideoAbuseReasonValid)
|
||||||
|
.withMessage('Should have a valid reason'),
|
||||||
|
body('predefinedReasons')
|
||||||
|
.optional()
|
||||||
|
.custom(isVideoAbusePredefinedReasonsValid)
|
||||||
|
.withMessage('Should have a valid list of predefined reasons'),
|
||||||
|
body('startAt')
|
||||||
|
.optional()
|
||||||
|
.customSanitizer(toIntOrNull)
|
||||||
|
.custom(isVideoAbuseTimestampValid)
|
||||||
|
.withMessage('Should have valid starting time value'),
|
||||||
|
body('endAt')
|
||||||
|
.optional()
|
||||||
|
.customSanitizer(toIntOrNull)
|
||||||
|
.custom(isVideoAbuseTimestampValid)
|
||||||
|
.withMessage('Should have valid ending time value')
|
||||||
|
.bail()
|
||||||
|
.custom(isVideoAbuseTimestampCoherent)
|
||||||
|
.withMessage('Should have a startAt timestamp beginning before endAt'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
|
logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
|
||||||
|
@ -63,6 +90,10 @@ const videoAbuseListValidator = [
|
||||||
query('id')
|
query('id')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(isIdValid).withMessage('Should have a valid id'),
|
.custom(isIdValid).withMessage('Should have a valid id'),
|
||||||
|
query('predefinedReason')
|
||||||
|
.optional()
|
||||||
|
.custom(isVideoAbusePredefinedReasonValid)
|
||||||
|
.withMessage('Should have a valid predefinedReason'),
|
||||||
query('search')
|
query('search')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(exists).withMessage('Should have a valid search'),
|
.custom(exists).withMessage('Should have a valid search'),
|
||||||
|
|
|
@ -15,7 +15,13 @@ import {
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
|
import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
|
||||||
import { VideoAbuseState, VideoDetails } from '../../../shared'
|
import {
|
||||||
|
VideoAbuseState,
|
||||||
|
VideoDetails,
|
||||||
|
VideoAbusePredefinedReasons,
|
||||||
|
VideoAbusePredefinedReasonsString,
|
||||||
|
videoAbusePredefinedReasonsMap
|
||||||
|
} from '../../../shared'
|
||||||
import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
|
import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
|
||||||
import { VideoAbuse } from '../../../shared/models/videos'
|
import { VideoAbuse } from '../../../shared/models/videos'
|
||||||
import {
|
import {
|
||||||
|
@ -31,6 +37,7 @@ import { ThumbnailModel } from './thumbnail'
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
import { VideoBlacklistModel } from './video-blacklist'
|
import { VideoBlacklistModel } from './video-blacklist'
|
||||||
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
|
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
|
||||||
|
import { invert } from 'lodash'
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
FOR_API = 'FOR_API'
|
FOR_API = 'FOR_API'
|
||||||
|
@ -47,6 +54,7 @@ export enum ScopeNames {
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
id?: number
|
id?: number
|
||||||
|
predefinedReasonId?: number
|
||||||
|
|
||||||
state?: VideoAbuseState
|
state?: VideoAbuseState
|
||||||
videoIs?: VideoAbuseVideoIs
|
videoIs?: VideoAbuseVideoIs
|
||||||
|
@ -104,6 +112,14 @@ export enum ScopeNames {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.predefinedReasonId) {
|
||||||
|
Object.assign(where, {
|
||||||
|
predefinedReasons: {
|
||||||
|
[Op.contains]: [ options.predefinedReasonId ]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const onlyBlacklisted = options.videoIs === 'blacklisted'
|
const onlyBlacklisted = options.videoIs === 'blacklisted'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -258,6 +274,21 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
@Column(DataType.JSONB)
|
@Column(DataType.JSONB)
|
||||||
deletedVideo: VideoDetails
|
deletedVideo: VideoDetails
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Default(null)
|
||||||
|
@Column(DataType.ARRAY(DataType.INTEGER))
|
||||||
|
predefinedReasons: VideoAbusePredefinedReasons[]
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Default(null)
|
||||||
|
@Column
|
||||||
|
startAt: number
|
||||||
|
|
||||||
|
@AllowNull(true)
|
||||||
|
@Default(null)
|
||||||
|
@Column
|
||||||
|
endAt: number
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
|
||||||
|
@ -311,6 +342,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
user?: MUserAccountId
|
user?: MUserAccountId
|
||||||
|
|
||||||
id?: number
|
id?: number
|
||||||
|
predefinedReason?: VideoAbusePredefinedReasonsString
|
||||||
state?: VideoAbuseState
|
state?: VideoAbuseState
|
||||||
videoIs?: VideoAbuseVideoIs
|
videoIs?: VideoAbuseVideoIs
|
||||||
|
|
||||||
|
@ -329,6 +361,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
serverAccountId,
|
serverAccountId,
|
||||||
state,
|
state,
|
||||||
videoIs,
|
videoIs,
|
||||||
|
predefinedReason,
|
||||||
searchReportee,
|
searchReportee,
|
||||||
searchVideo,
|
searchVideo,
|
||||||
searchVideoChannel,
|
searchVideoChannel,
|
||||||
|
@ -337,6 +370,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
} = parameters
|
} = parameters
|
||||||
|
|
||||||
const userAccountId = user ? user.Account.id : undefined
|
const userAccountId = user ? user.Account.id : undefined
|
||||||
|
const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
offset: start,
|
offset: start,
|
||||||
|
@ -348,6 +382,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
id,
|
id,
|
||||||
|
predefinedReasonId,
|
||||||
search,
|
search,
|
||||||
state,
|
state,
|
||||||
videoIs,
|
videoIs,
|
||||||
|
@ -360,7 +395,9 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoAbuseModel
|
return VideoAbuseModel
|
||||||
.scope({ method: [ ScopeNames.FOR_API, filters ] })
|
.scope([
|
||||||
|
{ method: [ ScopeNames.FOR_API, filters ] }
|
||||||
|
])
|
||||||
.findAndCountAll(query)
|
.findAndCountAll(query)
|
||||||
.then(({ rows, count }) => {
|
.then(({ rows, count }) => {
|
||||||
return { total: count, data: rows }
|
return { total: count, data: rows }
|
||||||
|
@ -368,6 +405,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
|
toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
|
||||||
|
const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
|
||||||
const countReportsForVideo = this.get('countReportsForVideo') as number
|
const countReportsForVideo = this.get('countReportsForVideo') as number
|
||||||
const nthReportForVideo = this.get('nthReportForVideo') as number
|
const nthReportForVideo = this.get('nthReportForVideo') as number
|
||||||
const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
|
const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
|
||||||
|
@ -382,6 +420,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
reason: this.reason,
|
reason: this.reason,
|
||||||
|
predefinedReasons,
|
||||||
reporterAccount: this.Account.toFormattedJSON(),
|
reporterAccount: this.Account.toFormattedJSON(),
|
||||||
state: {
|
state: {
|
||||||
id: this.state,
|
id: this.state,
|
||||||
|
@ -400,6 +439,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
},
|
},
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
|
startAt: this.startAt,
|
||||||
|
endAt: this.endAt,
|
||||||
count: countReportsForVideo || 0,
|
count: countReportsForVideo || 0,
|
||||||
nth: nthReportForVideo || 0,
|
nth: nthReportForVideo || 0,
|
||||||
countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
|
countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
|
||||||
|
@ -408,14 +449,31 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
|
toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
|
||||||
|
const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
|
||||||
|
|
||||||
|
const startAt = this.startAt
|
||||||
|
const endAt = this.endAt
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Flag' as 'Flag',
|
type: 'Flag' as 'Flag',
|
||||||
content: this.reason,
|
content: this.reason,
|
||||||
object: this.Video.url
|
object: this.Video.url,
|
||||||
|
tag: predefinedReasons.map(r => ({
|
||||||
|
type: 'Hashtag' as 'Hashtag',
|
||||||
|
name: r
|
||||||
|
})),
|
||||||
|
startAt,
|
||||||
|
endAt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getStateLabel (id: number) {
|
private static getStateLabel (id: number) {
|
||||||
return VIDEO_ABUSE_STATES[id] || 'Unknown'
|
return VIDEO_ABUSE_STATES[id] || 'Unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] {
|
||||||
|
return (predefinedReasons || [])
|
||||||
|
.filter(r => r in VideoAbusePredefinedReasons)
|
||||||
|
.map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import {
|
||||||
checkBadSortPagination,
|
checkBadSortPagination,
|
||||||
checkBadStartPagination
|
checkBadStartPagination
|
||||||
} from '../../../../shared/extra-utils/requests/check-api-params'
|
} from '../../../../shared/extra-utils/requests/check-api-params'
|
||||||
import { VideoAbuseState } from '../../../../shared/models/videos'
|
import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos'
|
||||||
|
|
||||||
describe('Test video abuses API validators', function () {
|
describe('Test video abuses API validators', function () {
|
||||||
let server: ServerInfo
|
let server: ServerInfo
|
||||||
|
@ -132,12 +132,36 @@ describe('Test video abuses API validators', function () {
|
||||||
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct parameters', async function () {
|
it('Should succeed with the correct parameters (basic)', async function () {
|
||||||
const fields = { reason: 'super reason' }
|
const fields = { reason: 'my super reason' }
|
||||||
|
|
||||||
const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
|
const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
|
||||||
videoAbuseId = res.body.videoAbuse.id
|
videoAbuseId = res.body.videoAbuse.id
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should fail with a wrong predefined reason', async function () {
|
||||||
|
const fields = { reason: 'my super reason', predefinedReasons: [ 'wrongPredefinedReason' ] }
|
||||||
|
|
||||||
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with negative timestamps', async function () {
|
||||||
|
const fields = { reason: 'my super reason', startAt: -1 }
|
||||||
|
|
||||||
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail mith misordered startAt/endAt', async function () {
|
||||||
|
const fields = { reason: 'my super reason', startAt: 5, endAt: 1 }
|
||||||
|
|
||||||
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the corret parameters (advanced)', async function () {
|
||||||
|
const fields: VideoAbuseCreate = { reason: 'my super reason', predefinedReasons: [ 'serverRules' ], startAt: 1, endAt: 5 }
|
||||||
|
|
||||||
|
await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When updating a video abuse', function () {
|
describe('When updating a video abuse', function () {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { VideoAbuse, VideoAbuseState } from '../../../../shared/models/videos'
|
import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
deleteVideoAbuse,
|
deleteVideoAbuse,
|
||||||
|
@ -291,6 +291,32 @@ describe('Test video abuses', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should list predefined reasons as well as timestamps for the reported video', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
|
||||||
|
const reason5 = 'my super bad reason 5'
|
||||||
|
const predefinedReasons5: VideoAbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
|
||||||
|
const createdAbuse = (await reportVideoAbuse(
|
||||||
|
servers[0].url,
|
||||||
|
servers[0].accessToken,
|
||||||
|
servers[0].video.id,
|
||||||
|
reason5,
|
||||||
|
predefinedReasons5,
|
||||||
|
1,
|
||||||
|
5
|
||||||
|
)).body.videoAbuse as VideoAbuse
|
||||||
|
|
||||||
|
const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
|
||||||
|
|
||||||
|
{
|
||||||
|
const abuse = (res.body.data as VideoAbuse[]).find(a => a.id === createdAbuse.id)
|
||||||
|
expect(abuse.reason).to.equals(reason5)
|
||||||
|
expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
|
||||||
|
expect(abuse.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
|
||||||
|
expect(abuse.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('Should delete the video abuse', async function () {
|
it('Should delete the video abuse', async function () {
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
|
|
||||||
|
@ -307,7 +333,7 @@ describe('Test video abuses', function () {
|
||||||
|
|
||||||
{
|
{
|
||||||
const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
|
const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
|
||||||
expect(res.body.total).to.equal(5)
|
expect(res.body.total).to.equal(6)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -328,25 +354,28 @@ describe('Test video abuses', function () {
|
||||||
expect(await list({ id: 56 })).to.have.lengthOf(0)
|
expect(await list({ id: 56 })).to.have.lengthOf(0)
|
||||||
expect(await list({ id: 1 })).to.have.lengthOf(1)
|
expect(await list({ id: 1 })).to.have.lengthOf(1)
|
||||||
|
|
||||||
expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(3)
|
expect(await list({ search: 'my super name for server 1' })).to.have.lengthOf(4)
|
||||||
expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
|
expect(await list({ search: 'aaaaaaaaaaaaaaaaaaaaaaaaaa' })).to.have.lengthOf(0)
|
||||||
|
|
||||||
expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
|
expect(await list({ searchVideo: 'my second super name for server 1' })).to.have.lengthOf(1)
|
||||||
|
|
||||||
expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(3)
|
expect(await list({ searchVideoChannel: 'root' })).to.have.lengthOf(4)
|
||||||
expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
|
expect(await list({ searchVideoChannel: 'aaaa' })).to.have.lengthOf(0)
|
||||||
|
|
||||||
expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
|
expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
|
||||||
expect(await list({ searchReporter: 'root' })).to.have.lengthOf(4)
|
expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
|
||||||
|
|
||||||
expect(await list({ searchReportee: 'root' })).to.have.lengthOf(3)
|
expect(await list({ searchReportee: 'root' })).to.have.lengthOf(4)
|
||||||
expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
|
expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
|
||||||
|
|
||||||
expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
|
expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
|
||||||
expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
|
expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
|
||||||
|
|
||||||
expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0)
|
expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0)
|
||||||
expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(5)
|
expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(6)
|
||||||
|
|
||||||
|
expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
|
||||||
|
expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
|
|
|
@ -1,17 +1,26 @@
|
||||||
import * as request from 'supertest'
|
import * as request from 'supertest'
|
||||||
import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
|
import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
|
||||||
import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests'
|
import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests'
|
||||||
import { VideoAbuseState } from '@shared/models'
|
import { VideoAbuseState, VideoAbusePredefinedReasonsString } from '@shared/models'
|
||||||
import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
|
import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
|
||||||
|
|
||||||
function reportVideoAbuse (url: string, token: string, videoId: number | string, reason: string, specialStatus = 200) {
|
function reportVideoAbuse (
|
||||||
|
url: string,
|
||||||
|
token: string,
|
||||||
|
videoId: number | string,
|
||||||
|
reason: string,
|
||||||
|
predefinedReasons?: VideoAbusePredefinedReasonsString[],
|
||||||
|
startAt?: number,
|
||||||
|
endAt?: number,
|
||||||
|
specialStatus = 200
|
||||||
|
) {
|
||||||
const path = '/api/v1/videos/' + videoId + '/abuse'
|
const path = '/api/v1/videos/' + videoId + '/abuse'
|
||||||
|
|
||||||
return request(url)
|
return request(url)
|
||||||
.post(path)
|
.post(path)
|
||||||
.set('Accept', 'application/json')
|
.set('Accept', 'application/json')
|
||||||
.set('Authorization', 'Bearer ' + token)
|
.set('Authorization', 'Bearer ' + token)
|
||||||
.send({ reason })
|
.send({ reason, predefinedReasons, startAt, endAt })
|
||||||
.expect(specialStatus)
|
.expect(specialStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +28,7 @@ function getVideoAbusesList (options: {
|
||||||
url: string
|
url: string
|
||||||
token: string
|
token: string
|
||||||
id?: number
|
id?: number
|
||||||
|
predefinedReason?: VideoAbusePredefinedReasonsString
|
||||||
search?: string
|
search?: string
|
||||||
state?: VideoAbuseState
|
state?: VideoAbuseState
|
||||||
videoIs?: VideoAbuseVideoIs
|
videoIs?: VideoAbuseVideoIs
|
||||||
|
@ -31,6 +41,7 @@ function getVideoAbusesList (options: {
|
||||||
url,
|
url,
|
||||||
token,
|
token,
|
||||||
id,
|
id,
|
||||||
|
predefinedReason,
|
||||||
search,
|
search,
|
||||||
state,
|
state,
|
||||||
videoIs,
|
videoIs,
|
||||||
|
@ -44,6 +55,7 @@ function getVideoAbusesList (options: {
|
||||||
const query = {
|
const query = {
|
||||||
sort: 'createdAt',
|
sort: 'createdAt',
|
||||||
id,
|
id,
|
||||||
|
predefinedReason,
|
||||||
search,
|
search,
|
||||||
state,
|
state,
|
||||||
videoIs,
|
videoIs,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ActivityPubActor } from './activitypub-actor'
|
import { ActivityPubActor } from './activitypub-actor'
|
||||||
import { ActivityPubSignature } from './activitypub-signature'
|
import { ActivityPubSignature } from './activitypub-signature'
|
||||||
import { CacheFileObject, VideoTorrentObject } from './objects'
|
import { CacheFileObject, VideoTorrentObject, ActivityFlagReasonObject } from './objects'
|
||||||
import { DislikeObject } from './objects/dislike-object'
|
import { DislikeObject } from './objects/dislike-object'
|
||||||
import { VideoAbuseObject } from './objects/video-abuse-object'
|
import { VideoAbuseObject } from './objects/video-abuse-object'
|
||||||
import { VideoCommentObject } from './objects/video-comment-object'
|
import { VideoCommentObject } from './objects/video-comment-object'
|
||||||
|
@ -113,4 +113,7 @@ export interface ActivityFlag extends BaseActivity {
|
||||||
type: 'Flag'
|
type: 'Flag'
|
||||||
content: string
|
content: string
|
||||||
object: APObject | APObject[]
|
object: APObject | APObject[]
|
||||||
|
tag?: ActivityFlagReasonObject[]
|
||||||
|
startAt?: number
|
||||||
|
endAt?: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { VideoAbusePredefinedReasonsString } from '@shared/models/videos'
|
||||||
|
|
||||||
export interface ActivityIdentifierObject {
|
export interface ActivityIdentifierObject {
|
||||||
identifier: string
|
identifier: string
|
||||||
name: string
|
name: string
|
||||||
|
@ -70,17 +72,22 @@ export type ActivityHtmlUrlObject = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActivityHashTagObject {
|
export interface ActivityHashTagObject {
|
||||||
type: 'Hashtag' | 'Mention'
|
type: 'Hashtag'
|
||||||
href?: string
|
href?: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActivityMentionObject {
|
export interface ActivityMentionObject {
|
||||||
type: 'Hashtag' | 'Mention'
|
type: 'Mention'
|
||||||
href?: string
|
href?: string
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ActivityFlagReasonObject {
|
||||||
|
type: 'Hashtag'
|
||||||
|
name: VideoAbusePredefinedReasonsString
|
||||||
|
}
|
||||||
|
|
||||||
export type ActivityTagObject =
|
export type ActivityTagObject =
|
||||||
ActivityPlaylistSegmentHashesObject
|
ActivityPlaylistSegmentHashesObject
|
||||||
| ActivityPlaylistInfohashesObject
|
| ActivityPlaylistInfohashesObject
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
|
import { ActivityFlagReasonObject } from './common-objects'
|
||||||
|
|
||||||
export interface VideoAbuseObject {
|
export interface VideoAbuseObject {
|
||||||
type: 'Flag'
|
type: 'Flag'
|
||||||
content: string
|
content: string
|
||||||
object: string | string[]
|
object: string | string[]
|
||||||
|
tag?: ActivityFlagReasonObject[]
|
||||||
|
startAt?: number
|
||||||
|
endAt?: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
|
||||||
|
|
||||||
export interface VideoAbuseCreate {
|
export interface VideoAbuseCreate {
|
||||||
reason: string
|
reason: string
|
||||||
|
predefinedReasons?: VideoAbusePredefinedReasonsString[]
|
||||||
|
startAt?: number
|
||||||
|
endAt?: number
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
export enum VideoAbusePredefinedReasons {
|
||||||
|
VIOLENT_OR_REPULSIVE = 1,
|
||||||
|
HATEFUL_OR_ABUSIVE,
|
||||||
|
SPAM_OR_MISLEADING,
|
||||||
|
PRIVACY,
|
||||||
|
RIGHTS,
|
||||||
|
SERVER_RULES,
|
||||||
|
THUMBNAILS,
|
||||||
|
CAPTIONS
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VideoAbusePredefinedReasonsString =
|
||||||
|
'violentOrRepulsive' |
|
||||||
|
'hatefulOrAbusive' |
|
||||||
|
'spamOrMisleading' |
|
||||||
|
'privacy' |
|
||||||
|
'rights' |
|
||||||
|
'serverRules' |
|
||||||
|
'thumbnails' |
|
||||||
|
'captions'
|
||||||
|
|
||||||
|
export const videoAbusePredefinedReasonsMap: {
|
||||||
|
[key in VideoAbusePredefinedReasonsString]: VideoAbusePredefinedReasons
|
||||||
|
} = {
|
||||||
|
violentOrRepulsive: VideoAbusePredefinedReasons.VIOLENT_OR_REPULSIVE,
|
||||||
|
hatefulOrAbusive: VideoAbusePredefinedReasons.HATEFUL_OR_ABUSIVE,
|
||||||
|
spamOrMisleading: VideoAbusePredefinedReasons.SPAM_OR_MISLEADING,
|
||||||
|
privacy: VideoAbusePredefinedReasons.PRIVACY,
|
||||||
|
rights: VideoAbusePredefinedReasons.RIGHTS,
|
||||||
|
serverRules: VideoAbusePredefinedReasons.SERVER_RULES,
|
||||||
|
thumbnails: VideoAbusePredefinedReasons.THUMBNAILS,
|
||||||
|
captions: VideoAbusePredefinedReasons.CAPTIONS
|
||||||
|
}
|
|
@ -2,10 +2,12 @@ import { Account } from '../../actors/index'
|
||||||
import { VideoConstant } from '../video-constant.model'
|
import { VideoConstant } from '../video-constant.model'
|
||||||
import { VideoAbuseState } from './video-abuse-state.model'
|
import { VideoAbuseState } from './video-abuse-state.model'
|
||||||
import { VideoChannel } from '../channel/video-channel.model'
|
import { VideoChannel } from '../channel/video-channel.model'
|
||||||
|
import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
|
||||||
|
|
||||||
export interface VideoAbuse {
|
export interface VideoAbuse {
|
||||||
id: number
|
id: number
|
||||||
reason: string
|
reason: string
|
||||||
|
predefinedReasons?: VideoAbusePredefinedReasonsString[]
|
||||||
reporterAccount: Account
|
reporterAccount: Account
|
||||||
|
|
||||||
state: VideoConstant<VideoAbuseState>
|
state: VideoConstant<VideoAbuseState>
|
||||||
|
@ -25,6 +27,9 @@ export interface VideoAbuse {
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
|
|
||||||
|
startAt: number
|
||||||
|
endAt: number
|
||||||
|
|
||||||
count?: number
|
count?: number
|
||||||
nth?: number
|
nth?: number
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ export * from './rate/account-video-rate.model'
|
||||||
export * from './rate/user-video-rate.type'
|
export * from './rate/user-video-rate.type'
|
||||||
export * from './abuse/video-abuse-state.model'
|
export * from './abuse/video-abuse-state.model'
|
||||||
export * from './abuse/video-abuse-create.model'
|
export * from './abuse/video-abuse-create.model'
|
||||||
|
export * from './abuse/video-abuse-reason.model'
|
||||||
export * from './abuse/video-abuse.model'
|
export * from './abuse/video-abuse.model'
|
||||||
export * from './abuse/video-abuse-update.model'
|
export * from './abuse/video-abuse-update.model'
|
||||||
export * from './blacklist/video-blacklist.model'
|
export * from './blacklist/video-blacklist.model'
|
||||||
|
|
|
@ -120,7 +120,7 @@ x-tagGroups:
|
||||||
- name: Moderation
|
- name: Moderation
|
||||||
tags:
|
tags:
|
||||||
- Video Abuses
|
- Video Abuses
|
||||||
- Video Blacklist
|
- Video Blocks
|
||||||
- name: Instance Configuration
|
- name: Instance Configuration
|
||||||
tags:
|
tags:
|
||||||
- Config
|
- Config
|
||||||
|
@ -1245,6 +1245,7 @@ paths:
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/idOrUUID'
|
- $ref: '#/components/parameters/idOrUUID'
|
||||||
requestBody:
|
requestBody:
|
||||||
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
@ -1253,6 +1254,28 @@ paths:
|
||||||
reason:
|
reason:
|
||||||
description: Reason why the user reports this video
|
description: Reason why the user reports this video
|
||||||
type: string
|
type: string
|
||||||
|
predefinedReasons:
|
||||||
|
description: Reason categories that help triage reports
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- violentOrAbusive
|
||||||
|
- hatefulOrAbusive
|
||||||
|
- spamOrMisleading
|
||||||
|
- privacy
|
||||||
|
- rights
|
||||||
|
- serverRules
|
||||||
|
- thumbnails
|
||||||
|
- captions
|
||||||
|
startAt:
|
||||||
|
type: number
|
||||||
|
description: Timestamp in the video that marks the beginning of the report
|
||||||
|
endAt:
|
||||||
|
type: number
|
||||||
|
description: Timestamp in the video that marks the ending of the report
|
||||||
|
required:
|
||||||
|
- reason
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: successful operation
|
description: successful operation
|
||||||
|
@ -2488,6 +2511,19 @@ components:
|
||||||
$ref: '#/components/schemas/VideoAbuseStateSet'
|
$ref: '#/components/schemas/VideoAbuseStateSet'
|
||||||
label:
|
label:
|
||||||
type: string
|
type: string
|
||||||
|
VideoAbusePredefinedReasons:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- violentOrAbusive
|
||||||
|
- hatefulOrAbusive
|
||||||
|
- spamOrMisleading
|
||||||
|
- privacy
|
||||||
|
- rights
|
||||||
|
- serverRules
|
||||||
|
- thumbnails
|
||||||
|
- captions
|
||||||
|
|
||||||
VideoResolutionConstant:
|
VideoResolutionConstant:
|
||||||
properties:
|
properties:
|
||||||
|
@ -2739,6 +2775,8 @@ components:
|
||||||
type: number
|
type: number
|
||||||
reason:
|
reason:
|
||||||
type: string
|
type: string
|
||||||
|
predefinedReasons:
|
||||||
|
$ref: '#/components/schemas/VideoAbusePredefinedReasons'
|
||||||
reporterAccount:
|
reporterAccount:
|
||||||
$ref: '#/components/schemas/Account'
|
$ref: '#/components/schemas/Account'
|
||||||
state:
|
state:
|
||||||
|
|
Loading…
Reference in New Issue