}
hasVideoAbusesRight () {
- return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
+ return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES)
}
hasVideoBlocklistRight () {
import { FollowingListComponent } from './follows/following-list/following-list.component'
import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
-import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlockListComponent } from './moderation'
+import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation'
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
import { ModerationComponent } from './moderation/moderation.component'
-import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component'
+import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component'
import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
ModerationComponent,
VideoBlockListComponent,
- VideoAbuseListComponent,
- VideoAbuseDetailsComponent,
+
+ AbuseListComponent,
+ AbuseDetailsComponent,
+
ModerationCommentModalComponent,
InstanceServerBlocklistComponent,
InstanceAccountBlocklistComponent,
--- /dev/null
+<div class="d-flex moderation-expanded">
+ <!-- report left part (report details) -->
+ <div class="col-8">
+
+ <!-- report metadata -->
+ <div class="d-flex">
+ <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
+ <span class="col-9 moderation-expanded-text">
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" class="chip">
+ <img
+ class="avatar"
+ [src]="abuse.reporterAccount.avatar?.path"
+ (error)="switchToDefaultAvatar($event)"
+ alt="Avatar"
+ >
+ <div>
+ <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
+ </div>
+ </a>
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
+ {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span>
+ </a>
+ </span>
+ </div>
+
+ <div class="d-flex">
+ <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
+ <span class="col-9 moderation-expanded-text">
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.video.channel.ownerAccount.displayName + '"' }" class="chip">
+ <img
+ class="avatar"
+ [src]="abuse.video.channel.ownerAccount?.avatar?.path"
+ (error)="switchToDefaultAvatar($event)"
+ alt="Avatar"
+ >
+ <div>
+ <span class="text-muted">{{ abuse.video.channel.ownerAccount ? abuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
+ </div>
+ </a>
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
+ {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span>
+ </a>
+ </span>
+ </div>
+
+ <div class="d-flex" *ngIf="abuse.updatedAt">
+ <span class="col-3 moderation-expanded-label" i18n>Updated</span>
+ <time class="col-9 moderation-expanded-text video-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
+ </div>
+
+ <!-- report text -->
+ <div class="mt-3 d-flex">
+ <span class="col-3 moderation-expanded-label">
+ <ng-container i18n>Report</ng-container>
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
+ </span>
+ <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
+ </div>
+
+ <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
+ <span class="col-3"></span>
+ <span class="col-9">
+ <a [routerLink]="[ '/admin/moderation/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="abuse.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="abuse.endAt"> - {{ endAt }}</ng-container>
+ </span>
+ </div>
+
+ <div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
+ <span class="col-3 moderation-expanded-label" i18n>Note</span>
+ <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.moderationCommentHtml"></span>
+ </div>
+
+ </div>
+
+ <!-- report right part (video details) -->
+ <div class="col-4">
+ <div class="screenratio">
+ <div *ngIf="abuse.video.deleted || abuse.video.blacklisted">
+ <span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
+ <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
+ </div>
+ <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
+ </div>
+ </div>
+</div>
import { Component, Input } from '@angular/core'
import { Actor } from '@app/shared/shared-main'
import { I18n } from '@ngx-translate/i18n-polyfill'
-import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model'
-import { ProcessedVideoAbuse } from './video-abuse-list.component'
+import { AbusePredefinedReasonsString } from '@shared/models'
+import { ProcessedAbuse } from './abuse-list.component'
import { durationToString } from '@app/helpers'
@Component({
- selector: 'my-video-abuse-details',
- templateUrl: './video-abuse-details.component.html',
+ selector: 'my-abuse-details',
+ templateUrl: './abuse-details.component.html',
styleUrls: [ '../moderation.component.scss' ]
})
-export class VideoAbuseDetailsComponent {
- @Input() videoAbuse: ProcessedVideoAbuse
+export class AbuseDetailsComponent {
+ @Input() abuse: ProcessedAbuse
- private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string }
+ private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
constructor (
private i18n: I18n
}
get startAt () {
- return durationToString(this.videoAbuse.startAt)
+ return durationToString(this.abuse.startAt)
}
get endAt () {
- return durationToString(this.videoAbuse.endAt)
+ return durationToString(this.abuse.endAt)
}
getPredefinedReasons () {
- if (!this.videoAbuse.predefinedReasons) return []
- return this.videoAbuse.predefinedReasons.map(r => ({
+ if (!this.abuse.predefinedReasons) return []
+ return this.abuse.predefinedReasons.map(r => ({
id: r,
label: this.predefinedReasonsTranslations[r]
}))
<p-table
- [value]="videoAbuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+ [value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
<div role="menu" ngbDropdownMenu>
<h6 class="dropdown-header" i18n>Advanced report filters</h6>
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
+ <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
</div>
</div>
<input
</tr>
</ng-template>
- <ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse>
+ <ng-template pTemplate="body" let-expanded="expanded" let-abuse>
<tr>
- <td class="c-hand" [pRowToggler]="videoAbuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
+ <td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
<span class="expander">
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
</span>
</td>
<td>
- <a [href]="videoAbuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
+ <a [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
<div class="chip two-lines">
<img
class="avatar"
- [src]="videoAbuse.reporterAccount.avatar?.path"
+ [src]="abuse.reporterAccount.avatar?.path"
(error)="switchToDefaultAvatar($event)"
alt="Avatar"
>
<div>
- {{ videoAbuse.reporterAccount.displayName }}
- <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
+ {{ abuse.reporterAccount.displayName }}
+ <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
</div>
</div>
</a>
</td>
- <td *ngIf="!videoAbuse.video.deleted">
- <a [href]="getVideoUrl(videoAbuse)" class="video-table-video-link" [title]="videoAbuse.video.name" target="_blank" rel="noopener noreferrer">
+ <td *ngIf="!abuse.video.deleted">
+ <a [href]="getVideoUrl(abuse)" class="video-table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
<div class="video-table-video">
<div class="video-table-video-image">
- <img [src]="videoAbuse.video.thumbnailPath">
+ <img [src]="abuse.video.thumbnailPath">
<span
- class="video-table-video-image-label" *ngIf="videoAbuse.count > 1"
+ class="video-table-video-image-label" *ngIf="abuse.count > 1"
i18n-title title="This video has been reported multiple times."
>
- {{ videoAbuse.nth }}/{{ videoAbuse.count }}
+ {{ abuse.nth }}/{{ abuse.count }}
</span>
</div>
<div class="video-table-video-text">
<div>
- <span *ngIf="!videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
- <span *ngIf="videoAbuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
- {{ videoAbuse.video.name }}
+ <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
+ <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
+ {{ abuse.video.name }}
</div>
- <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
+ <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
</div>
</div>
</a>
</td>
- <td *ngIf="videoAbuse.video.deleted" class="c-hand" [pRowToggler]="videoAbuse">
+ <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
<div class="video-table-video" i18n-title title="Video was deleted">
<div class="video-table-video-image">
<span i18n>Deleted</span>
</div>
<div class="video-table-video-text">
<div>
- {{ videoAbuse.video.name }}
+ {{ abuse.video.name }}
<span class="glyphicon glyphicon-trash"></span>
</div>
- <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
+ <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
</div>
</div>
</td>
- <td class="c-hand" [pRowToggler]="videoAbuse">{{ videoAbuse.createdAt | date: 'short' }}</td>
+ <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short' }}</td>
- <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
- <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
- <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
- <span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
+ <td class="c-hand video-abuse-states" [pRowToggler]="abuse">
+ <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
+ <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
+ <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
</td>
<td class="action-cell">
<my-action-dropdown
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
- i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
+ i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
></my-action-dropdown>
</td>
</tr>
</ng-template>
- <ng-template pTemplate="rowexpansion" let-videoAbuse>
+ <ng-template pTemplate="rowexpansion" let-abuse>
<tr>
<td class="expand-cell" colspan="6">
- <my-video-abuse-details [videoAbuse]="videoAbuse"></my-video-abuse-details>
+ <my-abuse-details [abuse]="abuse"></my-abuse-details>
</td>
</tr>
</ng-template>
import { SortMeta } from 'primeng/api'
-import { filter } from 'rxjs/operators'
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
import { environment } from 'src/environments/environment'
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
import { ActivatedRoute, Params, Router } from '@angular/router'
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
-import { BlocklistService, VideoAbuseService, VideoBlockService } from '@app/shared/shared-moderation'
+import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
import { I18n } from '@ngx-translate/i18n-polyfill'
-import { VideoAbuse, VideoAbuseState } from '@shared/models'
+import { Abuse, AbuseState } from '@shared/models'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
-export type ProcessedVideoAbuse = VideoAbuse & {
+export type ProcessedAbuse = Abuse & {
moderationCommentHtml?: string,
reasonHtml?: string
embedHtml?: string
updatedAt?: Date
+
// override bare server-side definitions with rich client-side definitions
reporterAccount: Account
- video: VideoAbuse['video'] & {
- channel: VideoAbuse['video']['channel'] & {
+
+ video: Abuse['video'] & {
+ channel: Abuse['video']['channel'] & {
ownerAccount: Account
}
}
}
@Component({
- selector: 'my-video-abuse-list',
- templateUrl: './video-abuse-list.component.html',
- styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ]
+ selector: 'my-abuse-list',
+ templateUrl: './abuse-list.component.html',
+ styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ]
})
-export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit {
+export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
@ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
- videoAbuses: ProcessedVideoAbuse[] = []
+ abuses: ProcessedAbuse[] = []
totalRecords = 0
sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
- videoAbuseActions: DropdownAction<VideoAbuse>[][] = []
+ abuseActions: DropdownAction<Abuse>[][] = []
constructor (
private notifier: Notifier,
- private videoAbuseService: VideoAbuseService,
+ private abuseService: AbuseService,
private blocklistService: BlocklistService,
private videoService: VideoService,
private videoBlocklistService: VideoBlockService,
) {
super()
- this.videoAbuseActions = [
+ this.abuseActions = [
[
{
label: this.i18n('Internal actions'),
},
{
label: this.i18n('Delete report'),
- handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
+ handler: abuse => this.removeAbuse(abuse)
},
{
label: this.i18n('Add note'),
- handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
- isDisplayed: videoAbuse => !videoAbuse.moderationComment
+ handler: abuse => this.openModerationCommentModal(abuse),
+ isDisplayed: abuse => !abuse.moderationComment
},
{
label: this.i18n('Update note'),
- handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
- isDisplayed: videoAbuse => !!videoAbuse.moderationComment
+ handler: abuse => this.openModerationCommentModal(abuse),
+ isDisplayed: abuse => !!abuse.moderationComment
},
{
label: this.i18n('Mark as accepted'),
- handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
- isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
+ handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
+ isDisplayed: abuse => !this.isAbuseAccepted(abuse)
},
{
label: this.i18n('Mark as rejected'),
- handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
- isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
+ handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
+ isDisplayed: abuse => !this.isAbuseRejected(abuse)
}
],
[
{
label: this.i18n('Actions for the video'),
isHeader: true,
- isDisplayed: videoAbuse => !videoAbuse.video.deleted
+ isDisplayed: abuse => !abuse.video.deleted
},
{
label: this.i18n('Block video'),
- isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted,
- handler: videoAbuse => {
- this.videoBlocklistService.blockVideo(videoAbuse.video.id, undefined, true)
+ isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted,
+ handler: abuse => {
+ this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
.subscribe(
() => {
this.notifier.success(this.i18n('Video blocked.'))
- this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
+ this.updateAbuseState(abuse, AbuseState.ACCEPTED)
},
err => this.notifier.error(err.message)
},
{
label: this.i18n('Unblock video'),
- isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted,
- handler: videoAbuse => {
- this.videoBlocklistService.unblockVideo(videoAbuse.video.id)
+ isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted,
+ handler: abuse => {
+ this.videoBlocklistService.unblockVideo(abuse.video.id)
.subscribe(
() => {
this.notifier.success(this.i18n('Video unblocked.'))
- this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
+ this.updateAbuseState(abuse, AbuseState.ACCEPTED)
},
err => this.notifier.error(err.message)
},
{
label: this.i18n('Delete video'),
- isDisplayed: videoAbuse => !videoAbuse.video.deleted,
- handler: async videoAbuse => {
+ isDisplayed: abuse => !abuse.video.deleted,
+ handler: async abuse => {
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)
+ this.videoService.removeVideo(abuse.video.id)
.subscribe(
() => {
this.notifier.success(this.i18n('Video deleted.'))
- this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
+ this.updateAbuseState(abuse, AbuseState.ACCEPTED)
},
err => this.notifier.error(err.message)
},
{
label: this.i18n('Mute reporter'),
- handler: async videoAbuse => {
- const account = videoAbuse.reporterAccount as Account
+ handler: async abuse => {
+ const account = abuse.reporterAccount as Account
this.blocklistService.blockAccountByInstance(account)
.subscribe(
},
{
label: this.i18n('Mute server'),
- isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId,
- handler: async videoAbuse => {
- this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host)
+ isDisplayed: abuse => !abuse.reporterAccount.userId,
+ handler: async abuse => {
+ this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
.subscribe(
() => {
this.notifier.success(
- this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host })
+ this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host })
)
},
}
getIdentifier () {
- return 'VideoAbuseListComponent'
+ return 'AbuseListComponent'
}
- openModerationCommentModal (videoAbuse: VideoAbuse) {
- this.moderationCommentModal.openModal(videoAbuse)
+ openModerationCommentModal (abuse: Abuse) {
+ this.moderationCommentModal.openModal(abuse)
}
onModerationCommentUpdated () {
}
/* END Table filter functions */
- isVideoAbuseAccepted (videoAbuse: VideoAbuse) {
- return videoAbuse.state.id === VideoAbuseState.ACCEPTED
+ isAbuseAccepted (abuse: Abuse) {
+ return abuse.state.id === AbuseState.ACCEPTED
}
- isVideoAbuseRejected (videoAbuse: VideoAbuse) {
- return videoAbuse.state.id === VideoAbuseState.REJECTED
+ isAbuseRejected (abuse: Abuse) {
+ return abuse.state.id === AbuseState.REJECTED
}
- getVideoUrl (videoAbuse: VideoAbuse) {
- return Video.buildClientUrl(videoAbuse.video.uuid)
+ getVideoUrl (abuse: Abuse) {
+ return Video.buildClientUrl(abuse.video.uuid)
}
- getVideoEmbed (videoAbuse: VideoAbuse) {
+ getVideoEmbed (abuse: Abuse) {
return buildVideoEmbed(
buildVideoLink({
- baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`,
+ baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
title: false,
warningTitle: false,
- startTime: videoAbuse.startAt,
- stopTime: videoAbuse.endAt
+ startTime: abuse.startAt,
+ stopTime: abuse.endAt
})
)
}
($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
}
- async removeVideoAbuse (videoAbuse: VideoAbuse) {
+ async removeAbuse (abuse: Abuse) {
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
if (res === false) return
- this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
+ this.abuseService.removeAbuse(abuse).subscribe(
() => {
this.notifier.success(this.i18n('Abuse deleted.'))
this.loadData()
)
}
- updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) {
- this.videoAbuseService.updateVideoAbuse(videoAbuse, { state })
+ updateAbuseState (abuse: Abuse, state: AbuseState) {
+ this.abuseService.updateAbuse(abuse, { state })
.subscribe(
() => this.loadData(),
}
protected loadData () {
- return this.videoAbuseService.getVideoAbuses({
+ return this.abuseService.getAbuses({
pagination: this.pagination,
sort: this.sort,
search: this.search
}).subscribe(
async resultList => {
this.totalRecords = resultList.total
- const videoAbuses = []
+ const abuses = []
for (const abuse of resultList.data) {
Object.assign(abuse, {
if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
- videoAbuses.push(abuse as ProcessedVideoAbuse)
+ abuses.push(abuse as ProcessedAbuse)
}
- this.videoAbuses = videoAbuses
+ this.abuses = abuses
},
err => this.notifier.error(err.message)
--- /dev/null
+export * from './abuse-details.component'
+export * from './abuse-list.component'
+export * from './moderation-comment-modal.component'
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { Notifier } from '@app/core'
-import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms'
-import { VideoAbuseService } from '@app/shared/shared-moderation'
+import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
+import { AbuseService } from '@app/shared/shared-moderation'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { I18n } from '@ngx-translate/i18n-polyfill'
-import { VideoAbuse } from '@shared/models'
+import { Abuse } from '@shared/models'
@Component({
selector: 'my-moderation-comment-modal',
@ViewChild('modal', { static: true }) modal: NgbModal
@Output() commentUpdated = new EventEmitter<string>()
- private abuseToComment: VideoAbuse
+ private abuseToComment: Abuse
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
private modalService: NgbModal,
private notifier: Notifier,
- private videoAbuseService: VideoAbuseService,
- private videoAbuseValidatorsService: VideoAbuseValidatorsService,
+ private abuseService: AbuseService,
+ private abuseValidatorsService: AbuseValidatorsService,
private i18n: I18n
) {
super()
ngOnInit () {
this.buildForm({
- moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_MODERATION_COMMENT
+ moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT
})
}
- openModal (abuseToComment: VideoAbuse) {
+ openModal (abuseToComment: Abuse) {
this.abuseToComment = abuseToComment
this.openedModal = this.modalService.open(this.modal, { centered: true })
async banUser () {
const moderationComment: string = this.form.value[ 'moderationComment' ]
- this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment })
+ this.abuseService.updateAbuse(this.abuseToComment, { moderationComment })
.subscribe(
() => {
this.notifier.success(this.i18n('Comment updated.'))
+export * from './abuse-list'
export * from './instance-blocklist'
-export * from './video-abuse-list'
export * from './video-block-list'
export * from './moderation.component'
export * from './moderation.routes'
import { Routes } from '@angular/router'
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
-import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
+import { AbuseListComponent } from '@app/+admin/moderation/abuse-list'
import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
import { UserRightGuard } from '@app/core'
import { UserRight } from '@shared/models'
children: [
{
path: '',
- redirectTo: 'video-abuses/list',
+ redirectTo: 'abuses/list',
pathMatch: 'full'
},
{
path: 'video-abuses',
- redirectTo: 'video-abuses/list',
+ redirectTo: 'abuses/list',
pathMatch: 'full'
},
{
path: 'video-abuses/list',
- component: VideoAbuseListComponent,
+ redirectTo: 'abuses/list',
+ pathMatch: 'full'
+ },
+ {
+ path: 'abuses/list',
+ component: AbuseListComponent,
canActivate: [ UserRightGuard ],
data: {
- userRight: UserRight.MANAGE_VIDEO_ABUSES,
+ userRight: UserRight.MANAGE_ABUSES,
meta: {
title: 'Video reports'
}
+++ /dev/null
-export * from './video-abuse-list.component'
-export * from './moderation-comment-modal.component'
+++ /dev/null
-<div class="d-flex moderation-expanded">
- <!-- report left part (report details) -->
- <div class="col-8">
-
- <!-- report metadata -->
- <div class="d-flex">
- <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
- <span class="col-9 moderation-expanded-text">
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + videoAbuse.reporterAccount.displayName + '"' }" class="chip">
- <img
- class="avatar"
- [src]="videoAbuse.reporterAccount.avatar?.path"
- (error)="switchToDefaultAvatar($event)"
- alt="Avatar"
- >
- <div>
- <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
- </div>
- </a>
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + videoAbuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
- {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span>
- </a>
- </span>
- </div>
-
- <div class="d-flex">
- <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
- <span class="col-9 moderation-expanded-text">
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +videoAbuse.video.channel.ownerAccount.displayName + '"' }" class="chip">
- <img
- class="avatar"
- [src]="videoAbuse.video.channel.ownerAccount?.avatar?.path"
- (error)="switchToDefaultAvatar($event)"
- alt="Avatar"
- >
- <div>
- <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? videoAbuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
- </div>
- </a>
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +videoAbuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
- {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1Â glyphicon glyphicon-flag"></span>
- </a>
- </span>
- </div>
-
- <div class="d-flex" *ngIf="videoAbuse.updatedAt">
- <span class="col-3 moderation-expanded-label" i18n>Updated</span>
- <time class="col-9 moderation-expanded-text video-details-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time>
- </div>
-
- <!-- report text -->
- <div class="mt-3 d-flex">
- <span class="col-3 moderation-expanded-label">
- <ng-container i18n>Report</ng-container>
- <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': '#' + videoAbuse.id }" class="ml-1 text-muted">#{{ videoAbuse.id }}</a>
- </span>
- <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
- </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">
- <span class="col-3 moderation-expanded-label" i18n>Note</span>
- <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
- </div>
-
- </div>
-
- <!-- report right part (video details) -->
- <div class="col-4">
- <div class="screenratio">
- <div *ngIf="videoAbuse.video.deleted || videoAbuse.video.blacklisted">
- <span i18n *ngIf="videoAbuse.video.deleted">The video was deleted</span>
- <span i18n *ngIf="!videoAbuse.video.deleted">The video was blocked</span>
- </div>
- <div *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" [innerHTML]="videoAbuse.embedHtml"></div>
- </div>
- </div>
-</div>
this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
this.rightNotifications = {
- videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
+ videoAbuseAsModerator: UserRight.MANAGE_ABUSES,
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
newUserRegistration: UserRight.MANAGE_USERS,
newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
private routesPerRight: { [ role in UserRight ]?: string } = {
[UserRight.MANAGE_USERS]: '/admin/users',
[UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends',
- [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/moderation/video-abuses',
+ [UserRight.MANAGE_ABUSES]: '/admin/moderation/abuses',
[UserRight.MANAGE_VIDEO_BLACKLIST]: '/admin/moderation/video-blocks',
[UserRight.MANAGE_JOBS]: '/admin/jobs',
[UserRight.MANAGE_CONFIGURATION]: '/admin/config'
const adminRights = [
UserRight.MANAGE_USERS,
UserRight.MANAGE_SERVER_FOLLOW,
- UserRight.MANAGE_VIDEO_ABUSES,
+ UserRight.MANAGE_ABUSES,
UserRight.MANAGE_VIDEO_BLACKLIST,
UserRight.MANAGE_JOBS,
UserRight.MANAGE_CONFIGURATION
import { BuildFormValidator } from './form-validator.service'
@Injectable()
-export class VideoAbuseValidatorsService {
- readonly VIDEO_ABUSE_REASON: BuildFormValidator
- readonly VIDEO_ABUSE_MODERATION_COMMENT: BuildFormValidator
+export class AbuseValidatorsService {
+ readonly ABUSE_REASON: BuildFormValidator
+ readonly ABUSE_MODERATION_COMMENT: BuildFormValidator
constructor (private i18n: I18n) {
- this.VIDEO_ABUSE_REASON = {
+ this.ABUSE_REASON = {
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
MESSAGES: {
'required': this.i18n('Report reason is required.'),
}
}
- this.VIDEO_ABUSE_MODERATION_COMMENT = {
+ this.ABUSE_MODERATION_COMMENT = {
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
MESSAGES: {
'required': this.i18n('Moderation comment is required.'),
+export * from './abuse-validators.service'
export * from './batch-domains-validators.service'
export * from './custom-config-validators.service'
export * from './form-validator.service'
export * from './login-validators.service'
export * from './reset-password-validators.service'
export * from './user-validators.service'
-export * from './video-abuse-validators.service'
export * from './video-accept-ownership-validators.service'
export * from './video-block-validators.service'
export * from './video-captions-validators.service'
LoginValidatorsService,
ResetPasswordValidatorsService,
UserValidatorsService,
- VideoAbuseValidatorsService,
+ AbuseValidatorsService,
VideoAcceptOwnershipValidatorsService,
VideoBlockValidatorsService,
VideoCaptionsValidatorsService,
LoginValidatorsService,
ResetPasswordValidatorsService,
UserValidatorsService,
- VideoAbuseValidatorsService,
+ AbuseValidatorsService,
VideoAcceptOwnershipValidatorsService,
VideoBlockValidatorsService,
VideoCaptionsValidatorsService,
avatarUrl: string
- static GET_ACTOR_AVATAR_URL (actor: { avatar?: Avatar }) {
+ static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
if (actor?.avatar?.url) return actor.avatar.url
if (actor && actor.avatar) {
video: VideoInfo
}
- videoAbuse?: {
+ abuse?: {
id: number
- video: VideoInfo
+
+ video?: VideoInfo
+
+ comment?: {
+ threadId: number
+
+ video: {
+ uuid: string
+ }
+ }
+
+ account?: ActorInfo
}
videoBlacklist?: {
// Additional fields
videoUrl?: string
commentUrl?: any[]
- videoAbuseUrl?: string
+ abuseUrl?: string
videoAutoBlacklistUrl?: string
accountUrl?: string
videoImportIdentifier?: string
this.comment = hash.comment
if (this.comment) this.setAvatarUrl(this.comment.account)
- this.videoAbuse = hash.videoAbuse
+ this.abuse = hash.abuse
this.videoBlacklist = hash.videoBlacklist
break
case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS:
- this.videoAbuseUrl = '/admin/moderation/video-abuses/list'
- this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
+ this.abuseUrl = '/admin/moderation/abuses/list'
+
+ if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
break
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
}
- private setAvatarUrl (actor: { avatarUrl?: string, avatar?: Avatar }) {
+ private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
}
}
<ng-template #noVideo>
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
-
+
<div class="message" i18n>
The notification concerns a video now unavailable
</div>
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
<div class="message" i18n>
- <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a>
+ <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a>
</div>
</ng-container>
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
</a>
-
+
<div class="message" i18n>
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a>
</div>
<ng-template #noComment>
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
-
+
<div class="message" i18n>
The notification concerns a comment now unavailable
</div>
import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { RestExtractor, RestPagination, RestService } from '@app/core'
-import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '@shared/models'
+import { AbuseUpdate, ResultList, Abuse, AbuseCreate, AbuseState } from '@shared/models'
import { environment } from '../../../environments/environment'
@Injectable()
-export class VideoAbuseService {
- private static BASE_VIDEO_ABUSE_URL = environment.apiUrl + '/api/v1/videos/'
+export class AbuseService {
+ private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses'
constructor (
private authHttp: HttpClient,
private restExtractor: RestExtractor
) {}
- getVideoAbuses (options: {
+ getAbuses (options: {
pagination: RestPagination,
sort: SortMeta,
search?: string
- }): Observable<ResultList<VideoAbuse>> {
+ }): Observable<ResultList<Abuse>> {
const { pagination, sort, search } = options
- const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse'
+ const url = AbuseService.BASE_ABUSE_URL + 'abuse'
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
state: {
prefix: 'state:',
handler: v => {
- if (v === 'accepted') return VideoAbuseState.ACCEPTED
- if (v === 'pending') return VideoAbuseState.PENDING
- if (v === 'rejected') return VideoAbuseState.REJECTED
+ if (v === 'accepted') return AbuseState.ACCEPTED
+ if (v === 'pending') return AbuseState.PENDING
+ if (v === 'rejected') return AbuseState.REJECTED
return undefined
}
params = this.restService.addObjectParams(params, filters)
}
- return this.authHttp.get<ResultList<VideoAbuse>>(url, { params })
+ return this.authHttp.get<ResultList<Abuse>>(url, { params })
.pipe(
catchError(res => this.restExtractor.handleError(res))
)
}
- reportVideo (parameters: { id: number } & VideoAbuseCreate) {
- const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse'
+ reportVideo (parameters: AbuseCreate) {
+ const url = AbuseService.BASE_ABUSE_URL
const body = omit(parameters, [ 'id' ])
)
}
- updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) {
- const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
+ updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
+ const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
return this.authHttp.put(url, abuseUpdate)
.pipe(
)
}
- removeVideoAbuse (videoAbuse: VideoAbuse) {
- const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
+ removeAbuse (abuse: Abuse) {
+ const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
return this.authHttp.delete(url)
.pipe(
+export * from './abuse.service'
export * from './account-block.model'
export * from './account-blocklist.component'
export * from './batch-domains-modal.component'
export * from './server-blocklist.component'
export * from './user-ban-modal.component'
export * from './user-moderation-dropdown.component'
-export * from './video-abuse.service'
export * from './video-block.component'
export * from './video-block.service'
export * from './video-report.component'
import { BulkService } from './bulk.service'
import { UserBanModalComponent } from './user-ban-modal.component'
import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
-import { VideoAbuseService } from './video-abuse.service'
+import { AbuseService } from './abuse.service'
import { VideoBlockComponent } from './video-block.component'
import { VideoBlockService } from './video-block.service'
import { VideoReportComponent } from './video-report.component'
providers: [
BlocklistService,
BulkService,
- VideoAbuseService,
+ AbuseService,
VideoBlockService
]
})
import { Component, Input, OnInit, ViewChild } from '@angular/core'
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
import { Notifier } from '@app/core'
-import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms'
+import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { I18n } from '@ngx-translate/i18n-polyfill'
-import { videoAbusePredefinedReasonsMap, VideoAbusePredefinedReasonsString } from '@shared/models/videos/abuse/video-abuse-reason.model'
+import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
import { Video } from '../shared-main'
-import { VideoAbuseService } from './video-abuse.service'
+import { AbuseService } from './abuse.service'
@Component({
selector: 'my-video-report',
@ViewChild('modal', { static: true }) modal: NgbModal
error: string = null
- predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
+ predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
embedHtml: SafeHtml
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
private modalService: NgbModal,
- private videoAbuseValidatorsService: VideoAbuseValidatorsService,
- private videoAbuseService: VideoAbuseService,
+ private abuseValidatorsService: AbuseValidatorsService,
+ private abuseService: AbuseService,
private notifier: Notifier,
private sanitizer: DomSanitizer,
private i18n: I18n
ngOnInit () {
this.buildForm({
- reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON,
- predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null),
+ reason: this.abuseValidatorsService.ABUSE_REASON,
+ predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null),
timestamp: {
hasStart: null,
startAt: null,
report () {
const reason = this.form.get('reason').value
- const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[]
+ const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[]
const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
- this.videoAbuseService.reportVideo({
- id: this.video.id,
+ this.abuseService.reportVideo({
+ accountId: this.video.account.id,
reason,
predefinedReasons,
- startAt: hasStart && startAt ? startAt : undefined,
- endAt: hasEnd && endAt ? endAt : undefined
+ video: {
+ id: this.video.id,
+ startAt: hasStart && startAt ? startAt : undefined,
+ endAt: hasEnd && endAt ? endAt : undefined
+ }
}).subscribe(
() => {
this.notifier.success(this.i18n('Video reported.'))
--- /dev/null
+import * as express from 'express'
+import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
+import { AbuseModel } from '@server/models/abuse/abuse'
+import { getServerActor } from '@server/models/application/application'
+import { AbuseCreate, abusePredefinedReasonsMap, AbuseState, UserRight } from '../../../shared'
+import { getFormattedObjects } from '../../helpers/utils'
+import { sequelizeTypescript } from '../../initializers/database'
+import {
+ abuseGetValidator,
+ abuseListValidator,
+ abuseReportValidator,
+ abusesSortValidator,
+ abuseUpdateValidator,
+ asyncMiddleware,
+ asyncRetryTransactionMiddleware,
+ authenticate,
+ ensureUserHasRight,
+ paginationValidator,
+ setDefaultPagination,
+ setDefaultSort
+} from '../../middlewares'
+import { AccountModel } from '../../models/account/account'
+
+const abuseRouter = express.Router()
+
+abuseRouter.get('/abuse',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_ABUSES),
+ paginationValidator,
+ abusesSortValidator,
+ setDefaultSort,
+ setDefaultPagination,
+ abuseListValidator,
+ asyncMiddleware(listAbuses)
+)
+abuseRouter.put('/:videoId/abuse/:id',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_ABUSES),
+ asyncMiddleware(abuseUpdateValidator),
+ asyncRetryTransactionMiddleware(updateAbuse)
+)
+abuseRouter.post('/:videoId/abuse',
+ authenticate,
+ asyncMiddleware(abuseReportValidator),
+ asyncRetryTransactionMiddleware(reportAbuse)
+)
+abuseRouter.delete('/:videoId/abuse/:id',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_ABUSES),
+ asyncMiddleware(abuseGetValidator),
+ asyncRetryTransactionMiddleware(deleteAbuse)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ abuseRouter,
+
+ // FIXME: deprecated in 2.3. Remove these exports
+ listAbuses,
+ updateAbuse,
+ deleteAbuse,
+ reportAbuse
+}
+
+// ---------------------------------------------------------------------------
+
+async function listAbuses (req: express.Request, res: express.Response) {
+ const user = res.locals.oauth.token.user
+ const serverActor = await getServerActor()
+
+ const resultList = await AbuseModel.listForApi({
+ start: req.query.start,
+ count: req.query.count,
+ sort: req.query.sort,
+ id: req.query.id,
+ filter: 'video',
+ predefinedReason: req.query.predefinedReason,
+ search: req.query.search,
+ state: req.query.state,
+ videoIs: req.query.videoIs,
+ searchReporter: req.query.searchReporter,
+ searchReportee: req.query.searchReportee,
+ searchVideo: req.query.searchVideo,
+ searchVideoChannel: req.query.searchVideoChannel,
+ serverAccountId: serverActor.Account.id,
+ user
+ })
+
+ return res.json(getFormattedObjects(resultList.data, resultList.total))
+}
+
+async function updateAbuse (req: express.Request, res: express.Response) {
+ const abuse = res.locals.abuse
+
+ if (req.body.moderationComment !== undefined) abuse.moderationComment = req.body.moderationComment
+ if (req.body.state !== undefined) abuse.state = req.body.state
+
+ await sequelizeTypescript.transaction(t => {
+ return abuse.save({ transaction: t })
+ })
+
+ // Do not send the delete to other instances, we updated OUR copy of this video abuse
+
+ return res.type('json').status(204).end()
+}
+
+async function deleteAbuse (req: express.Request, res: express.Response) {
+ const abuse = res.locals.abuse
+
+ await sequelizeTypescript.transaction(t => {
+ return abuse.destroy({ transaction: t })
+ })
+
+ // Do not send the delete to other instances, we delete OUR copy of this video abuse
+
+ return res.type('json').status(204).end()
+}
+
+async function reportAbuse (req: express.Request, res: express.Response) {
+ const videoInstance = res.locals.videoAll
+ const commentInstance = res.locals.videoCommentFull
+ const accountInstance = res.locals.account
+
+ const body: AbuseCreate = req.body
+
+ const { id } = await sequelizeTypescript.transaction(async t => {
+ const reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
+ const predefinedReasons = body.predefinedReasons?.map(r => abusePredefinedReasonsMap[r])
+
+ const baseAbuse = {
+ reporterAccountId: reporterAccount.id,
+ reason: body.reason,
+ state: AbuseState.PENDING,
+ predefinedReasons
+ }
+
+ if (body.video) {
+ return createVideoAbuse({
+ baseAbuse,
+ videoInstance,
+ reporterAccount,
+ transaction: t,
+ startAt: body.video.startAt,
+ endAt: body.video.endAt
+ })
+ }
+
+ if (body.comment) {
+ return createVideoCommentAbuse({
+ baseAbuse,
+ commentInstance,
+ reporterAccount,
+ transaction: t
+ })
+ }
+
+ // Account report
+ return createAccountAbuse({
+ baseAbuse,
+ accountInstance,
+ reporterAccount,
+ transaction: t
+ })
+ })
+
+ return res.json({ abuse: { id } })
+}
import * as RateLimit from 'express-rate-limit'
import { badRequest } from '../../helpers/express-utils'
import { CONFIG } from '../../initializers/config'
+import { abuseRouter } from './abuse'
import { accountsRouter } from './accounts'
import { bulkRouter } from './bulk'
import { configRouter } from './config'
apiRouter.use(apiRateLimiter)
apiRouter.use('/server', serverRouter)
+apiRouter.use('/abuses', abuseRouter)
apiRouter.use('/bulk', bulkRouter)
apiRouter.use('/oauth-clients', oauthClientsRouter)
apiRouter.use('/config', configRouter)
import * as express from 'express'
-import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared'
-import { logger } from '../../../helpers/logger'
+import { AbuseModel } from '@server/models/abuse/abuse'
+import { getServerActor } from '@server/models/application/application'
+import { AbuseCreate, UserRight, VideoAbuseCreate } from '../../../../shared'
import { getFormattedObjects } from '../../../helpers/utils'
-import { sequelizeTypescript } from '../../../initializers/database'
import {
+ abusesSortValidator,
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
setDefaultPagination,
setDefaultSort,
videoAbuseGetValidator,
+ videoAbuseListValidator,
videoAbuseReportValidator,
- videoAbusesSortValidator,
- videoAbuseUpdateValidator,
- videoAbuseListValidator
+ videoAbuseUpdateValidator
} from '../../../middlewares'
-import { AccountModel } from '../../../models/account/account'
-import { VideoAbuseModel } from '../../../models/video/video-abuse'
-import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
-import { Notifier } from '../../../lib/notifier'
-import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
-import { MVideoAbuseAccountVideo } from '../../../types/models/video'
-import { getServerActor } from '@server/models/application/application'
-import { MAccountDefault } from '@server/types/models'
+import { deleteAbuse, reportAbuse, updateAbuse } from '../abuse'
+
+// FIXME: deprecated in 2.3. Remove this controller
-const auditLogger = auditLoggerFactory('abuse')
const abuseVideoRouter = express.Router()
abuseVideoRouter.get('/abuse',
authenticate,
- ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
+ ensureUserHasRight(UserRight.MANAGE_ABUSES),
paginationValidator,
- videoAbusesSortValidator,
+ abusesSortValidator,
setDefaultSort,
setDefaultPagination,
videoAbuseListValidator,
)
abuseVideoRouter.put('/:videoId/abuse/:id',
authenticate,
- ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
+ ensureUserHasRight(UserRight.MANAGE_ABUSES),
asyncMiddleware(videoAbuseUpdateValidator),
asyncRetryTransactionMiddleware(updateVideoAbuse)
)
)
abuseVideoRouter.delete('/:videoId/abuse/:id',
authenticate,
- ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
+ ensureUserHasRight(UserRight.MANAGE_ABUSES),
asyncMiddleware(videoAbuseGetValidator),
asyncRetryTransactionMiddleware(deleteVideoAbuse)
)
const user = res.locals.oauth.token.user
const serverActor = await getServerActor()
- const resultList = await VideoAbuseModel.listForApi({
+ const resultList = await AbuseModel.listForApi({
start: req.query.start,
count: req.query.count,
sort: req.query.sort,
id: req.query.id,
+ filter: 'video',
predefinedReason: req.query.predefinedReason,
search: req.query.search,
state: req.query.state,
}
async function updateVideoAbuse (req: express.Request, res: express.Response) {
- const videoAbuse = res.locals.videoAbuse
-
- if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment
- if (req.body.state !== undefined) videoAbuse.state = req.body.state
-
- await sequelizeTypescript.transaction(t => {
- return videoAbuse.save({ transaction: t })
- })
-
- // Do not send the delete to other instances, we updated OUR copy of this video abuse
-
- return res.type('json').status(204).end()
+ return updateAbuse(req, res)
}
async function deleteVideoAbuse (req: express.Request, res: express.Response) {
- const videoAbuse = res.locals.videoAbuse
-
- await sequelizeTypescript.transaction(t => {
- return videoAbuse.destroy({ transaction: t })
- })
-
- // Do not send the delete to other instances, we delete OUR copy of this video abuse
-
- return res.type('json').status(204).end()
+ return deleteAbuse(req, res)
}
async function reportVideoAbuse (req: express.Request, res: express.Response) {
- const videoInstance = res.locals.videoAll
- const body: VideoAbuseCreate = req.body
- let reporterAccount: MAccountDefault
- let videoAbuseJSON: VideoAbuse
-
- const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
- reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
- const predefinedReasons = body.predefinedReasons?.map(r => videoAbusePredefinedReasonsMap[r])
-
- const abuseToCreate = {
- reporterAccountId: reporterAccount.id,
- reason: body.reason,
- videoId: videoInstance.id,
- state: VideoAbuseState.PENDING,
- predefinedReasons,
- startAt: body.startAt,
- endAt: body.endAt
- }
-
- const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
- videoAbuseInstance.Video = videoInstance
- videoAbuseInstance.Account = reporterAccount
-
- // We send the video abuse to the origin server
- if (videoInstance.isOwned() === false) {
- await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t)
- }
+ const oldBody = req.body as VideoAbuseCreate
- videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
- auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON))
+ req.body = {
+ accountId: res.locals.videoAll.VideoChannel.accountId,
- return videoAbuseInstance
- })
+ reason: oldBody.reason,
+ predefinedReasons: oldBody.predefinedReasons,
- Notifier.Instance.notifyOnNewVideoAbuse({
- videoAbuse: videoAbuseJSON,
- videoAbuseInstance,
- reporter: reporterAccount.Actor.getIdentifier()
- })
-
- logger.info('Abuse report for video "%s" created.', videoInstance.name)
+ video: {
+ id: res.locals.videoAll.id,
+ startAt: oldBody.startAt,
+ endAt: oldBody.endAt
+ }
+ } as AbuseCreate
- return res.json({ videoAbuse: videoAbuseJSON }).end()
+ return reportAbuse(req, res)
}
-import * as path from 'path'
-import * as express from 'express'
import { diff } from 'deep-object-diff'
-import { chain } from 'lodash'
+import * as express from 'express'
import * as flatten from 'flat'
+import { chain } from 'lodash'
+import * as path from 'path'
import * as winston from 'winston'
-import { jsonLoggerFormat, labelFormatter } from './logger'
-import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared'
-import { VideoComment } from '../../shared/models/videos/video-comment.model'
+import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
+import { Abuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
import { CustomConfig } from '../../shared/models/server/custom-config.model'
+import { VideoComment } from '../../shared/models/videos/video-comment.model'
import { CONFIG } from '../initializers/config'
-import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
+import { jsonLoggerFormat, labelFormatter } from './logger'
function getAuditIdFromRes (res: express.Response) {
return res.locals.oauth.token.User.username
}
}
-const videoAbuseKeysToKeep = [
+const abuseKeysToKeep = [
'id',
'reason',
'reporterAccount',
- 'video-id',
- 'video-name',
- 'video-uuid',
'createdAt'
]
-class VideoAbuseAuditView extends EntityAuditView {
- constructor (private readonly videoAbuse: VideoAbuse) {
- super(videoAbuseKeysToKeep, 'abuse', videoAbuse)
+class AbuseAuditView extends EntityAuditView {
+ constructor (private readonly abuse: Abuse) {
+ super(abuseKeysToKeep, 'abuse', abuse)
}
}
CommentAuditView,
UserAuditView,
VideoAuditView,
- VideoAbuseAuditView,
+ AbuseAuditView,
CustomConfigAuditView
}
--- /dev/null
+import validator from 'validator'
+import { abusePredefinedReasonsMap, AbusePredefinedReasonsString, AbuseVideoIs } from '@shared/models'
+import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
+import { exists, isArray } from './misc'
+
+const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.ABUSES
+
+function isAbuseReasonValid (value: string) {
+ return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON)
+}
+
+function isAbusePredefinedReasonValid (value: AbusePredefinedReasonsString) {
+ return exists(value) && value in abusePredefinedReasonsMap
+}
+
+function isAbusePredefinedReasonsValid (value: AbusePredefinedReasonsString[]) {
+ return exists(value) && isArray(value) && value.every(v => v in abusePredefinedReasonsMap)
+}
+
+function isAbuseTimestampValid (value: number) {
+ return value === null || (exists(value) && validator.isInt('' + value, { min: 0 }))
+}
+
+function isAbuseTimestampCoherent (endAt: number, { req }) {
+ return exists(req.body.startAt) && endAt > req.body.startAt
+}
+
+function isAbuseModerationCommentValid (value: string) {
+ return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
+}
+
+function isAbuseStateValid (value: string) {
+ return exists(value) && ABUSE_STATES[value] !== undefined
+}
+
+function isAbuseVideoIsValid (value: AbuseVideoIs) {
+ return exists(value) && (
+ value === 'deleted' ||
+ value === 'blacklisted'
+ )
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ isAbuseReasonValid,
+ isAbusePredefinedReasonValid,
+ isAbusePredefinedReasonsValid,
+ isAbuseTimestampValid,
+ isAbuseTimestampCoherent,
+ isAbuseModerationCommentValid,
+ isAbuseStateValid,
+ isAbuseVideoIsValid
+}
import { isActivityPubUrlValid } from './misc'
-import { isVideoAbuseReasonValid } from '../video-abuses'
+import { isAbuseReasonValid } from '../abuses'
function isFlagActivityValid (activity: any) {
return activity.type === 'Flag' &&
- isVideoAbuseReasonValid(activity.content) &&
+ isAbuseReasonValid(activity.content) &&
isActivityPubUrlValid(activity.object)
}
+++ /dev/null
-import validator from 'validator'
-
-import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
-import { exists, isArray } from './misc'
-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
-
-function isVideoAbuseReasonValid (value: string) {
- 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) {
- return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.MODERATION_COMMENT)
-}
-
-function isVideoAbuseStateValid (value: string) {
- return exists(value) && VIDEO_ABUSE_STATES[value] !== undefined
-}
-
-function isAbuseVideoIsValid (value: VideoAbuseVideoIs) {
- return exists(value) && (
- value === 'deleted' ||
- value === 'blacklisted'
- )
-}
-
-// ---------------------------------------------------------------------------
-
-export {
- isVideoAbuseReasonValid,
- isVideoAbusePredefinedReasonValid,
- isVideoAbusePredefinedReasonsValid,
- isVideoAbuseTimestampValid,
- isVideoAbuseTimestampCoherent,
- isVideoAbuseModerationCommentValid,
- isVideoAbuseStateValid,
- isAbuseVideoIsValid
-}
import { Response } from 'express'
-import { VideoAbuseModel } from '../../models/video/video-abuse'
+import { AbuseModel } from '../../models/abuse/abuse'
import { fetchVideo } from '../video'
+// FIXME: deprecated in 2.3. Remove this function
async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
const abuseId = parseInt(abuseIdArg + '', 10)
- let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
+ let abuse = await AbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
- if (!videoAbuse) {
+ if (!abuse) {
const userId = res.locals.oauth?.token.User.id
const video = await fetchVideo(videoUUID, 'all', userId)
- if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id)
+ if (video) abuse = await AbuseModel.loadByIdAndVideoId(abuseId, video.id)
}
- if (videoAbuse === null) {
+ if (abuse === null) {
res.status(404)
.json({ error: 'Video abuse not found' })
.end()
return false
}
- res.locals.videoAbuse = videoAbuse
+ res.locals.abuse = abuse
return true
}
+async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
+
+}
+
// ---------------------------------------------------------------------------
export {
+ doesAbuseExist,
doesVideoAbuseExist
}
+export * from './abuses'
export * from './accounts'
-export * from './video-abuses'
export * from './video-blacklists'
export * from './video-captions'
export * from './video-channels'
import { join } from 'path'
import { randomBytes } from 'crypto'
-import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models'
import { ActivityPubActorType } from '../../shared/models/activitypub'
import { FollowState } from '../../shared/models/actors'
-import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
+import {
+ AbuseState,
+ VideoImportState,
+ VideoPrivacy,
+ VideoTranscodingFPS,
+ JobType,
+ VideoRateType,
+ VideoResolution,
+ VideoState
+} from '../../shared/models'
// Do not use barrels, remain constants as independent as possible
import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
ACCOUNTS: [ 'createdAt' ],
JOBS: [ 'createdAt' ],
- VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],
VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
VIDEO_IMPORTS: [ 'createdAt' ],
VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ],
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
+ ABUSES: [ 'id', 'createdAt', 'state' ],
+
ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
SERVERS_BLOCKLIST: [ 'createdAt' ],
VIDEO_LANGUAGES: { max: 500 }, // Array length
BLOCKED_REASON: { min: 3, max: 250 } // Length
},
- VIDEO_ABUSES: {
+ ABUSES: {
REASON: { min: 2, max: 3000 }, // Length
MODERATION_COMMENT: { min: 2, max: 3000 } // Length
},
[VideoImportState.REJECTED]: 'Rejected'
}
-const VIDEO_ABUSE_STATES = {
- [VideoAbuseState.PENDING]: 'Pending',
- [VideoAbuseState.REJECTED]: 'Rejected',
- [VideoAbuseState.ACCEPTED]: 'Accepted'
+const ABUSE_STATES = {
+ [AbuseState.PENDING]: 'Pending',
+ [AbuseState.REJECTED]: 'Rejected',
+ [AbuseState.ACCEPTED]: 'Accepted'
}
const VIDEO_PLAYLIST_PRIVACIES = {
VIDEO_RATE_TYPES,
VIDEO_TRANSCODING_FPS,
FFMPEG_NICE,
- VIDEO_ABUSE_STATES,
+ ABUSE_STATES,
VIDEO_CHANNELS,
LRU_CACHE,
JOB_REQUEST_TIMEOUT,
+import { QueryTypes, Transaction } from 'sequelize'
import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
+import { AbuseModel } from '@server/models/abuse/abuse'
+import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
+import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
import { isTestInstance } from '../helpers/core-utils'
import { logger } from '../helpers/logger'
-
import { AccountModel } from '../models/account/account'
+import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { AccountVideoRateModel } from '../models/account/account-video-rate'
import { UserModel } from '../models/account/user'
+import { UserNotificationModel } from '../models/account/user-notification'
+import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
+import { UserVideoHistoryModel } from '../models/account/user-video-history'
import { ActorModel } from '../models/activitypub/actor'
import { ActorFollowModel } from '../models/activitypub/actor-follow'
import { ApplicationModel } from '../models/application/application'
import { AvatarModel } from '../models/avatar/avatar'
import { OAuthClientModel } from '../models/oauth/oauth-client'
import { OAuthTokenModel } from '../models/oauth/oauth-token'
+import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
+import { PluginModel } from '../models/server/plugin'
import { ServerModel } from '../models/server/server'
+import { ServerBlocklistModel } from '../models/server/server-blocklist'
+import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
import { TagModel } from '../models/video/tag'
+import { ThumbnailModel } from '../models/video/thumbnail'
import { VideoModel } from '../models/video/video'
-import { VideoAbuseModel } from '../models/video/video-abuse'
import { VideoBlacklistModel } from '../models/video/video-blacklist'
+import { VideoCaptionModel } from '../models/video/video-caption'
+import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
import { VideoChannelModel } from '../models/video/video-channel'
import { VideoCommentModel } from '../models/video/video-comment'
import { VideoFileModel } from '../models/video/video-file'
-import { VideoShareModel } from '../models/video/video-share'
-import { VideoTagModel } from '../models/video/video-tag'
-import { CONFIG } from './config'
-import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
-import { VideoCaptionModel } from '../models/video/video-caption'
import { VideoImportModel } from '../models/video/video-import'
-import { VideoViewModel } from '../models/video/video-view'
-import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
-import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
-import { UserVideoHistoryModel } from '../models/account/user-video-history'
-import { AccountBlocklistModel } from '../models/account/account-blocklist'
-import { ServerBlocklistModel } from '../models/server/server-blocklist'
-import { UserNotificationModel } from '../models/account/user-notification'
-import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
-import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
import { VideoPlaylistModel } from '../models/video/video-playlist'
import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
-import { ThumbnailModel } from '../models/video/thumbnail'
-import { PluginModel } from '../models/server/plugin'
-import { QueryTypes, Transaction } from 'sequelize'
+import { VideoShareModel } from '../models/video/video-share'
+import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
+import { VideoTagModel } from '../models/video/video-tag'
+import { VideoViewModel } from '../models/video/video-view'
+import { CONFIG } from './config'
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
TagModel,
AccountVideoRateModel,
UserModel,
+ AbuseModel,
+ VideoCommentAbuseModel,
VideoAbuseModel,
VideoModel,
VideoChangeOwnershipModel,
import * as Sequelize from 'sequelize'
-import { VideoAbuseState } from '../../../shared/models/videos'
+import { AbuseState } from '../../../shared/models'
async function up (utils: {
transaction: Sequelize.Transaction
}
{
- const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING
+ const query = 'UPDATE "videoAbuse" SET "state" = ' + AbuseState.PENDING
await utils.sequelize.query(query)
}
-import {
- ActivityCreate,
- ActivityFlag,
- VideoAbuseState,
- videoAbusePredefinedReasonsMap
-} from '../../../../shared'
-import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
+import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
+import { AccountModel } from '@server/models/account/account'
+import { VideoModel } from '@server/models/video/video'
+import { VideoCommentModel } from '@server/models/video/video-comment'
+import { AbuseObject, abusePredefinedReasonsMap, AbuseState, ActivityCreate, ActivityFlag } from '../../../../shared'
+import { getAPId } from '../../../helpers/activitypub'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
import { sequelizeTypescript } from '../../../initializers/database'
-import { VideoAbuseModel } from '../../../models/video/video-abuse'
-import { getOrCreateVideoAndAccountAndChannel } from '../videos'
-import { Notifier } from '../../notifier'
-import { getAPId } from '../../../helpers/activitypub'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
-import { MActorSignature, MVideoAbuseAccountVideo } from '../../../types/models'
-import { AccountModel } from '@server/models/account/account'
+import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models'
async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
const { activity, byActor } = options
- return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor)
+
+ return retryTransactionWrapper(processCreateAbuse, activity, byActor)
}
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
-async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
- const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject)
+async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
+ const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject)
const account = byActor.Account
- if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url)
+ if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url)
+
+ const reporterAccount = await AccountModel.load(account.id)
const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ]
+ const tags = Array.isArray(flag.tag) ? flag.tag : []
+ const predefinedReasons = tags.map(tag => abusePredefinedReasonsMap[tag.name])
+ .filter(v => !isNaN(v))
+
+ const startAt = flag.startAt
+ const endAt = flag.endAt
+
for (const object of objects) {
try {
- logger.debug('Reporting remote abuse for video %s.', getAPId(object))
-
- const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
- 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 videoAbuseData = {
- reporterAccountId: account.id,
- reason: flag.content,
- videoId: video.id,
- state: VideoAbuseState.PENDING,
- predefinedReasons,
- startAt,
- endAt
- }
+ const uri = getAPId(object)
- const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
- videoAbuseInstance.Video = video
- videoAbuseInstance.Account = reporterAccount
+ logger.debug('Reporting remote abuse for object %s.', uri)
- logger.info('Remote abuse for video uuid %s created', flag.object)
+ await sequelizeTypescript.transaction(async t => {
- return videoAbuseInstance
- })
+ const video = await VideoModel.loadByUrlAndPopulateAccount(uri)
+ let videoComment: MCommentOwnerVideo
+ let flaggedAccount: MAccountDefault
+
+ if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri)
+ if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri)
+
+ if (!video && !videoComment && !flaggedAccount) {
+ logger.warn('Cannot flag unknown entity %s.', object)
+ return
+ }
+
+ const baseAbuse = {
+ reporterAccountId: reporterAccount.id,
+ reason: flag.content,
+ state: AbuseState.PENDING,
+ predefinedReasons
+ }
- const videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
+ if (video) {
+ return createVideoAbuse({
+ baseAbuse,
+ startAt,
+ endAt,
+ reporterAccount,
+ transaction: t,
+ videoInstance: video
+ })
+ }
+
+ if (videoComment) {
+ return createVideoCommentAbuse({
+ baseAbuse,
+ reporterAccount,
+ transaction: t,
+ commentInstance: videoComment
+ })
+ }
- Notifier.Instance.notifyOnNewVideoAbuse({
- videoAbuse: videoAbuseJSON,
- videoAbuseInstance,
- reporter: reporterAccount.Actor.getIdentifier()
+ return await createAccountAbuse({
+ baseAbuse,
+ reporterAccount,
+ transaction: t,
+ accountInstance: flaggedAccount
+ })
})
} catch (err) {
- logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err })
+ logger.debug('Cannot process report of %s', getAPId(object), { err })
}
}
}
-import { getVideoAbuseActivityPubUrl } from '../url'
-import { unicastTo } from './utils'
-import { logger } from '../../../helpers/logger'
+import { Transaction } from 'sequelize'
import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
+import { logger } from '../../../helpers/logger'
+import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
import { audiencify, getAudience } from '../audience'
-import { Transaction } from 'sequelize'
-import { MActor, MVideoFullLight } from '../../../types/models'
-import { MVideoAbuseVideo } from '../../../types/models/video'
+import { getAbuseActivityPubUrl } from '../url'
+import { unicastTo } from './utils'
-function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) {
- if (!video.VideoChannel.Account.Actor.serverId) return // Local user
+function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
+ if (!flaggedAccount.Actor.serverId) return // Local user
- const url = getVideoAbuseActivityPubUrl(videoAbuse)
+ const url = getAbuseActivityPubUrl(abuse)
- logger.info('Creating job to send video abuse %s.', url)
+ logger.info('Creating job to send abuse %s.', url)
// Custom audience, we only send the abuse to the origin instance
- const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
- const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience)
+ const audience = { to: [ flaggedAccount.Actor.url ], cc: [] }
+ const flagActivity = buildFlagActivity(url, byActor, abuse, audience)
- t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.getSharedInbox()))
+ t.afterCommit(() => unicastTo(flagActivity, byActor, flaggedAccount.Actor.getSharedInbox()))
}
-function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag {
+function buildFlagActivity (url: string, byActor: MActor, abuse: MAbuseAP, audience: ActivityAudience): ActivityFlag {
if (!audience) audience = getAudience(byActor)
const activity = Object.assign(
{ id: url, actor: byActor.url },
- videoAbuse.toActivityPubObject()
+ abuse.toActivityPubObject()
)
return audiencify(activity, audience)
// ---------------------------------------------------------------------------
export {
- sendVideoAbuse
+ sendAbuse
}
MActorId,
MActorUrl,
MCommentId,
- MVideoAbuseId,
MVideoId,
MVideoUrl,
- MVideoUUID
+ MVideoUUID,
+ MAbuseId
} from '../../types/models'
import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist'
import { MVideoFileVideoUUID } from '../../types/models/video/video-file'
return WEBSERVER.URL + '/accounts/' + accountName
}
-function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) {
- return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
+function getAbuseActivityPubUrl (abuse: MAbuseId) {
+ return WEBSERVER.URL + '/admin/abuses/' + abuse.id
}
function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
getVideoCacheStreamingPlaylistActivityPubUrl,
getVideoChannelActivityPubUrl,
getAccountActivityPubUrl,
- getVideoAbuseActivityPubUrl,
+ getAbuseActivityPubUrl,
getActorFollowActivityPubUrl,
getActorFollowAcceptActivityPubUrl,
getVideoAnnounceActivityPubUrl,
+import { readFileSync } from 'fs-extra'
+import { merge } from 'lodash'
import { createTransport, Transporter } from 'nodemailer'
+import { join } from 'path'
+import { VideoChannelModel } from '@server/models/video/video-channel'
+import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
+import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
+import { Abuse, EmailPayload } from '@shared/models'
+import { SendEmailOptions } from '../../shared/models/server/emailer.model'
import { isTestInstance, root } from '../helpers/core-utils'
import { bunyanLogger, logger } from '../helpers/logger'
import { CONFIG, isEmailEnabled } from '../initializers/config'
-import { JobQueue } from './job-queue'
-import { readFileSync } from 'fs-extra'
import { WEBSERVER } from '../initializers/constants'
-import {
- MCommentOwnerVideo,
- MVideo,
- MVideoAbuseVideo,
- MVideoAccountLight,
- MVideoBlacklistLightVideo,
- MVideoBlacklistVideo
-} from '../types/models/video'
-import { MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
-import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
-import { EmailPayload } from '@shared/models'
-import { join } from 'path'
-import { VideoAbuse } from '../../shared/models/videos'
-import { SendEmailOptions } from '../../shared/models/server/emailer.model'
-import { merge } from 'lodash'
-import { VideoChannelModel } from '@server/models/video/video-channel'
+import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
+import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
+import { JobQueue } from './job-queue'
+
const Email = require('email-templates')
class Emailer {
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
}
- addVideoAbuseModeratorsNotification (to: string[], parameters: {
- videoAbuse: VideoAbuse
- videoAbuseInstance: MVideoAbuseVideo
+ addAbuseModeratorsNotification (to: string[], parameters: {
+ abuse: Abuse
+ abuseInstance: MAbuseFull
reporter: string
}) {
- const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id
- const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
+ const { abuse, abuseInstance, reporter } = parameters
- const emailPayload: EmailPayload = {
- template: 'video-abuse-new',
- to,
- subject: `New video abuse report from ${parameters.reporter}`,
- locals: {
- videoUrl,
- videoAbuseUrl,
- videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(),
- videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(),
- videoAbuse: parameters.videoAbuse,
- reporter: parameters.reporter,
- action: {
- text: 'View report #' + parameters.videoAbuse.id,
- url: videoAbuseUrl
+ const action = {
+ text: 'View report #' + abuse.id,
+ url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
+ }
+
+ let emailPayload: EmailPayload
+
+ if (abuseInstance.VideoAbuse) {
+ const video = abuseInstance.VideoAbuse.Video
+ const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
+
+ emailPayload = {
+ template: 'video-abuse-new',
+ to,
+ subject: `New video abuse report from ${reporter}`,
+ locals: {
+ videoUrl,
+ isLocal: video.remote === false,
+ videoCreatedAt: new Date(video.createdAt).toLocaleString(),
+ videoPublishedAt: new Date(video.publishedAt).toLocaleString(),
+ videoName: video.name,
+ reason: abuse.reason,
+ videoChannel: video.VideoChannel,
+ action
+ }
+ }
+ } else if (abuseInstance.VideoCommentAbuse) {
+ const comment = abuseInstance.VideoCommentAbuse.VideoComment
+ const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
+
+ emailPayload = {
+ template: 'comment-abuse-new',
+ to,
+ subject: `New comment abuse report from ${reporter}`,
+ locals: {
+ commentUrl,
+ isLocal: comment.isOwned(),
+ commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
+ reason: abuse.reason,
+ flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
+ action
+ }
+ }
+ } else {
+ const account = abuseInstance.FlaggedAccount
+ const accountUrl = account.getClientUrl()
+
+ emailPayload = {
+ template: 'account-abuse-new',
+ to,
+ subject: `New account abuse report from ${reporter}`,
+ locals: {
+ accountUrl,
+ accountDisplayName: account.getDisplayName(),
+ isLocal: account.isOwned(),
+ reason: abuse.reason,
+ action
}
}
}
--- /dev/null
+extends ../common/greetings
+include ../common/mixins.pug
+
+block title
+ | An account is pending moderation
+
+block content
+ p
+ | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}account "
+ a(href=accountUrl) #{accountDisplayName}
+
+ p The reporter, #{reporter}, cited the following reason(s):
+ blockquote #{reason}
+ br(style="display: none;")
mixin channel(channel)
- var handle = `${channel.name}@${channel.host}`
- | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
\ No newline at end of file
+ | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
+
+mixin account(account)
+ - var handle = `${account.name}@${account.host}`
+ | #[a(href=`${WEBSERVER.URL}/accounts/${handle}` title=handle) #{account.displayName}]
block content
p
- | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video "
- a(href=videoUrl) #{videoAbuse.video.name}
- | " by #[+channel(videoAbuse.video.channel)]
+ | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}video "
+ a(href=videoUrl) #{videoName}
+ | " by #[+channel(videoChannel)]
if videoPublishedAt
| , published the #{videoPublishedAt}.
else
| , uploaded the #{videoCreatedAt} but not yet published.
p The reporter, #{reporter}, cited the following reason(s):
- blockquote #{videoAbuse.reason}
+ blockquote #{reason}
br(style="display: none;")
--- /dev/null
+extends ../common/greetings
+include ../common/mixins.pug
+
+block title
+ | A comment is pending moderation
+
+block content
+ p
+ | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}comment "
+ a(href=commentUrl) of #{flaggedAccount}
+ | created on #{commentCreatedAt}
+
+ p The reporter, #{reporter}, cited the following reason(s):
+ blockquote #{reason}
+ br(style="display: none;")
-import { VideoModel } from '../models/video/video'
-import { VideoCommentModel } from '../models/video/video-comment'
-import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
+import { PathLike } from 'fs-extra'
+import { Transaction } from 'sequelize/types'
+import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger'
+import { logger } from '@server/helpers/logger'
+import { AbuseModel } from '@server/models/abuse/abuse'
+import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
+import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
+import { VideoFileModel } from '@server/models/video/video-file'
+import { FilteredModelAttributes } from '@server/types'
+import {
+ MAbuseFull,
+ MAccountDefault,
+ MAccountLight,
+ MCommentAbuseAccountVideo,
+ MCommentOwnerVideo,
+ MUser,
+ MVideoAbuseVideoFull,
+ MVideoAccountLightBlacklistAllFiles
+} from '@server/types/models'
+import { ActivityCreate } from '../../shared/models/activitypub'
+import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
+import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
import { VideoCreate, VideoImportCreate } from '../../shared/models/videos'
+import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
import { UserModel } from '../models/account/user'
-import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
-import { ActivityCreate } from '../../shared/models/activitypub'
import { ActorModel } from '../models/activitypub/actor'
-import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
-import { VideoFileModel } from '@server/models/video/video-file'
-import { PathLike } from 'fs-extra'
-import { MUser } from '@server/types/models'
+import { VideoModel } from '../models/video/video'
+import { VideoCommentModel } from '../models/video/video-comment'
+import { sendAbuse } from './activitypub/send/send-flag'
+import { Notifier } from './notifier'
export type AcceptResult = {
accepted: boolean
return { accepted: true }
}
+async function createVideoAbuse (options: {
+ baseAbuse: FilteredModelAttributes<AbuseModel>
+ videoInstance: MVideoAccountLightBlacklistAllFiles
+ startAt: number
+ endAt: number
+ transaction: Transaction
+ reporterAccount: MAccountDefault
+}) {
+ const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount } = options
+
+ const associateFun = async (abuseInstance: MAbuseFull) => {
+ const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({
+ abuseId: abuseInstance.id,
+ videoId: videoInstance.id,
+ startAt: startAt,
+ endAt: endAt
+ }, { transaction })
+
+ videoAbuseInstance.Video = videoInstance
+ abuseInstance.VideoAbuse = videoAbuseInstance
+
+ return { isOwned: videoInstance.isOwned() }
+ }
+
+ return createAbuse({
+ base: baseAbuse,
+ reporterAccount,
+ flaggedAccount: videoInstance.VideoChannel.Account,
+ transaction,
+ associateFun
+ })
+}
+
+function createVideoCommentAbuse (options: {
+ baseAbuse: FilteredModelAttributes<AbuseModel>
+ commentInstance: MCommentOwnerVideo
+ transaction: Transaction
+ reporterAccount: MAccountDefault
+}) {
+ const { baseAbuse, commentInstance, transaction, reporterAccount } = options
+
+ const associateFun = async (abuseInstance: MAbuseFull) => {
+ const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({
+ abuseId: abuseInstance.id,
+ videoCommentId: commentInstance.id
+ }, { transaction })
+
+ commentAbuseInstance.VideoComment = commentInstance
+ abuseInstance.VideoCommentAbuse = commentAbuseInstance
+
+ return { isOwned: commentInstance.isOwned() }
+ }
+
+ return createAbuse({
+ base: baseAbuse,
+ reporterAccount,
+ flaggedAccount: commentInstance.Account,
+ transaction,
+ associateFun
+ })
+}
+
+function createAccountAbuse (options: {
+ baseAbuse: FilteredModelAttributes<AbuseModel>
+ accountInstance: MAccountDefault
+ transaction: Transaction
+ reporterAccount: MAccountDefault
+}) {
+ const { baseAbuse, accountInstance, transaction, reporterAccount } = options
+
+ const associateFun = async () => {
+ return { isOwned: accountInstance.isOwned() }
+ }
+
+ return createAbuse({
+ base: baseAbuse,
+ reporterAccount,
+ flaggedAccount: accountInstance,
+ transaction,
+ associateFun
+ })
+}
+
export {
isLocalVideoAccepted,
isLocalVideoThreadAccepted,
isRemoteVideoCommentAccepted,
isLocalVideoCommentReplyAccepted,
isPreImportVideoAccepted,
- isPostImportVideoAccepted
+ isPostImportVideoAccepted,
+
+ createAbuse,
+ createVideoAbuse,
+ createVideoCommentAbuse,
+ createAccountAbuse
+}
+
+// ---------------------------------------------------------------------------
+
+async function createAbuse (options: {
+ base: FilteredModelAttributes<AbuseModel>
+ reporterAccount: MAccountDefault
+ flaggedAccount: MAccountLight
+ associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean} >
+ transaction: Transaction
+}) {
+ const { base, reporterAccount, flaggedAccount, associateFun, transaction } = options
+ const auditLogger = auditLoggerFactory('abuse')
+
+ const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id })
+ const abuseInstance: MAbuseFull = await AbuseModel.create(abuseAttributes, { transaction })
+
+ abuseInstance.ReporterAccount = reporterAccount
+ abuseInstance.FlaggedAccount = flaggedAccount
+
+ const { isOwned } = await associateFun(abuseInstance)
+
+ if (isOwned === false) {
+ await sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
+ }
+
+ const abuseJSON = abuseInstance.toFormattedJSON()
+ auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON))
+
+ Notifier.Instance.notifyOnNewAbuse({
+ abuse: abuseJSON,
+ abuseInstance,
+ reporter: reporterAccount.Actor.getIdentifier()
+ })
+
+ logger.info('Abuse report %d created.', abuseInstance.id)
+
+ return abuseJSON
}
MUserWithNotificationSetting,
UserNotificationModelForApi
} from '@server/types/models/user'
+import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
import { MVideoImportVideo } from '@server/types/models/video/video-import'
+import { Abuse } from '@shared/models'
import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
-import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos'
+import { VideoPrivacy, VideoState } from '../../shared/models/videos'
import { logger } from '../helpers/logger'
import { CONFIG } from '../initializers/config'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { UserModel } from '../models/account/user'
import { UserNotificationModel } from '../models/account/user-notification'
-import { MAccountServer, MActorFollowFull } from '../types/models'
-import {
- MCommentOwnerVideo,
- MVideoAbuseVideo,
- MVideoAccountLight,
- MVideoBlacklistLightVideo,
- MVideoBlacklistVideo,
- MVideoFullLight
-} from '../types/models/video'
+import { MAbuseFull, MAbuseVideo, MAccountServer, MActorFollowFull } from '../types/models'
+import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
import { isBlockedByServerOrAccount } from './blocklist'
import { Emailer } from './emailer'
import { PeerTubeSocket } from './peertube-socket'
.catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
}
- notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void {
- this.notifyModeratorsOfNewVideoAbuse(parameters)
- .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err }))
+ notifyOnNewAbuse (parameters: { abuse: Abuse, abuseInstance: MAbuseFull, reporter: string }): void {
+ this.notifyModeratorsOfNewAbuse(parameters)
+ .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err }))
}
notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
}
- private async notifyModeratorsOfNewVideoAbuse (parameters: {
- videoAbuse: VideoAbuse
- videoAbuseInstance: MVideoAbuseVideo
+ private async notifyModeratorsOfNewAbuse (parameters: {
+ abuse: Abuse
+ abuseInstance: MAbuseFull
reporter: string
}) {
- const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
+ const { abuse, abuseInstance } = parameters
+
+ const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
if (moderators.length === 0) return
- logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url)
+ const url = abuseInstance.VideoAbuse?.Video?.url || abuseInstance.VideoCommentAbuse?.VideoComment?.url
+
+ logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
function settingGetter (user: MUserWithNotificationSetting) {
return user.NotificationSetting.videoAbuseAsModerator
}
async function notificationCreator (user: MUserWithNotificationSetting) {
- const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
+ const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
userId: user.id,
- videoAbuseId: parameters.videoAbuse.id
+ abuseId: abuse.id
})
- notification.VideoAbuse = parameters.videoAbuseInstance
+ notification.Abuse = abuseInstance
return notification
}
function emailSender (emails: string[]) {
- return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters)
+ return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters)
}
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
--- /dev/null
+import * as express from 'express'
+import { body, param, query } from 'express-validator'
+import {
+ isAbuseModerationCommentValid,
+ isAbusePredefinedReasonsValid,
+ isAbusePredefinedReasonValid,
+ isAbuseReasonValid,
+ isAbuseStateValid,
+ isAbuseTimestampCoherent,
+ isAbuseTimestampValid,
+ isAbuseVideoIsValid
+} from '@server/helpers/custom-validators/abuses'
+import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '@server/helpers/custom-validators/misc'
+import { logger } from '@server/helpers/logger'
+import { doesAbuseExist, doesVideoAbuseExist, doesVideoExist } from '@server/helpers/middlewares'
+import { areValidationErrors } from './utils'
+
+const abuseReportValidator = [
+ param('videoId')
+ .custom(isIdOrUUIDValid)
+ .not()
+ .isEmpty()
+ .withMessage('Should have a valid videoId'),
+ body('reason')
+ .custom(isAbuseReasonValid)
+ .withMessage('Should have a valid reason'),
+ body('predefinedReasons')
+ .optional()
+ .custom(isAbusePredefinedReasonsValid)
+ .withMessage('Should have a valid list of predefined reasons'),
+ body('startAt')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isAbuseTimestampValid)
+ .withMessage('Should have valid starting time value'),
+ body('endAt')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isAbuseTimestampValid)
+ .withMessage('Should have valid ending time value')
+ .bail()
+ .custom(isAbuseTimestampCoherent)
+ .withMessage('Should have a startAt timestamp beginning before endAt'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking abuseReport parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+ if (!await doesVideoExist(req.params.videoId, res)) return
+
+ // TODO: check comment or video (exlusive)
+
+ return next()
+ }
+]
+
+const abuseGetValidator = [
+ param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+ param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking abuseGetValidator parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+ // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return
+
+ return next()
+ }
+]
+
+const abuseUpdateValidator = [
+ param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+ param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
+ body('state')
+ .optional()
+ .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
+ body('moderationComment')
+ .optional()
+ .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking abuseUpdateValidator parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+ // if (!await doesAbuseExist(req.params.id, req.params.videoId, res)) return
+
+ return next()
+ }
+]
+
+const abuseListValidator = [
+ query('id')
+ .optional()
+ .custom(isIdValid).withMessage('Should have a valid id'),
+ query('predefinedReason')
+ .optional()
+ .custom(isAbusePredefinedReasonValid)
+ .withMessage('Should have a valid predefinedReason'),
+ query('search')
+ .optional()
+ .custom(exists).withMessage('Should have a valid search'),
+ query('state')
+ .optional()
+ .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
+ query('videoIs')
+ .optional()
+ .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
+ query('searchReporter')
+ .optional()
+ .custom(exists).withMessage('Should have a valid reporter search'),
+ query('searchReportee')
+ .optional()
+ .custom(exists).withMessage('Should have a valid reportee search'),
+ query('searchVideo')
+ .optional()
+ .custom(exists).withMessage('Should have a valid video search'),
+ query('searchVideoChannel')
+ .optional()
+ .custom(exists).withMessage('Should have a valid video channel search'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking abuseListValidator parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+
+ return next()
+ }
+]
+
+// FIXME: deprecated in 2.3. Remove these validators
+
+const videoAbuseReportValidator = [
+ param('videoId')
+ .custom(isIdOrUUIDValid)
+ .not()
+ .isEmpty()
+ .withMessage('Should have a valid videoId'),
+ body('reason')
+ .custom(isAbuseReasonValid)
+ .withMessage('Should have a valid reason'),
+ body('predefinedReasons')
+ .optional()
+ .custom(isAbusePredefinedReasonsValid)
+ .withMessage('Should have a valid list of predefined reasons'),
+ body('startAt')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isAbuseTimestampValid)
+ .withMessage('Should have valid starting time value'),
+ body('endAt')
+ .optional()
+ .customSanitizer(toIntOrNull)
+ .custom(isAbuseTimestampValid)
+ .withMessage('Should have valid ending time value')
+ .bail()
+ .custom(isAbuseTimestampCoherent)
+ .withMessage('Should have a startAt timestamp beginning before endAt'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+ if (!await doesVideoExist(req.params.videoId, res)) return
+
+ return next()
+ }
+]
+
+const videoAbuseGetValidator = [
+ param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+ param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+ if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
+
+ return next()
+ }
+]
+
+const videoAbuseUpdateValidator = [
+ param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
+ param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
+ body('state')
+ .optional()
+ .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
+ body('moderationComment')
+ .optional()
+ .custom(isAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+ if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
+
+ return next()
+ }
+]
+
+const videoAbuseListValidator = [
+ query('id')
+ .optional()
+ .custom(isIdValid).withMessage('Should have a valid id'),
+ query('predefinedReason')
+ .optional()
+ .custom(isAbusePredefinedReasonValid)
+ .withMessage('Should have a valid predefinedReason'),
+ query('search')
+ .optional()
+ .custom(exists).withMessage('Should have a valid search'),
+ query('state')
+ .optional()
+ .custom(isAbuseStateValid).withMessage('Should have a valid video abuse state'),
+ query('videoIs')
+ .optional()
+ .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
+ query('searchReporter')
+ .optional()
+ .custom(exists).withMessage('Should have a valid reporter search'),
+ query('searchReportee')
+ .optional()
+ .custom(exists).withMessage('Should have a valid reportee search'),
+ query('searchVideo')
+ .optional()
+ .custom(exists).withMessage('Should have a valid video search'),
+ query('searchVideoChannel')
+ .optional()
+ .custom(exists).withMessage('Should have a valid video channel search'),
+
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body })
+
+ if (areValidationErrors(req, res)) return
+
+ return next()
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+ abuseListValidator,
+ abuseReportValidator,
+ abuseGetValidator,
+ abuseUpdateValidator,
+ videoAbuseReportValidator,
+ videoAbuseGetValidator,
+ videoAbuseUpdateValidator,
+ videoAbuseListValidator
+}
+export * from './abuse'
export * from './account'
export * from './blocklist'
export * from './oembed'
const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS)
const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
-const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
+const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES)
const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
-const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
+const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS)
const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
export {
usersSortValidator,
- videoAbusesSortValidator,
+ abusesSortValidator,
videoChannelsSortValidator,
videoImportsSortValidator,
videosSearchSortValidator,
-export * from './video-abuses'
export * from './video-blacklist'
export * from './video-captions'
export * from './video-channels'
+++ /dev/null
-import * as express from 'express'
-import { body, param, query } from 'express-validator'
-import { exists, isIdOrUUIDValid, isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc'
-import {
- isAbuseVideoIsValid,
- isVideoAbuseModerationCommentValid,
- isVideoAbuseReasonValid,
- isVideoAbuseStateValid,
- isVideoAbusePredefinedReasonsValid,
- isVideoAbusePredefinedReasonValid,
- isVideoAbuseTimestampValid,
- isVideoAbuseTimestampCoherent
-} from '../../../helpers/custom-validators/video-abuses'
-import { logger } from '../../../helpers/logger'
-import { doesVideoAbuseExist, doesVideoExist } from '../../../helpers/middlewares'
-import { areValidationErrors } from '../utils'
-
-const videoAbuseReportValidator = [
- param('videoId')
- .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) => {
- logger.debug('Checking videoAbuseReport parameters', { parameters: req.body })
-
- if (areValidationErrors(req, res)) return
- if (!await doesVideoExist(req.params.videoId, res)) return
-
- return next()
- }
-]
-
-const videoAbuseGetValidator = [
- param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
- param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
-
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
-
- if (areValidationErrors(req, res)) return
- if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
-
- return next()
- }
-]
-
-const videoAbuseUpdateValidator = [
- param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
- param('id').custom(isIdValid).not().isEmpty().withMessage('Should have a valid id'),
- body('state')
- .optional()
- .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'),
- body('moderationComment')
- .optional()
- .custom(isVideoAbuseModerationCommentValid).withMessage('Should have a valid video moderation comment'),
-
- async (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
-
- if (areValidationErrors(req, res)) return
- if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
-
- return next()
- }
-]
-
-const videoAbuseListValidator = [
- query('id')
- .optional()
- .custom(isIdValid).withMessage('Should have a valid id'),
- query('predefinedReason')
- .optional()
- .custom(isVideoAbusePredefinedReasonValid)
- .withMessage('Should have a valid predefinedReason'),
- query('search')
- .optional()
- .custom(exists).withMessage('Should have a valid search'),
- query('state')
- .optional()
- .custom(isVideoAbuseStateValid).withMessage('Should have a valid video abuse state'),
- query('videoIs')
- .optional()
- .custom(isAbuseVideoIsValid).withMessage('Should have a valid "video is" attribute'),
- query('searchReporter')
- .optional()
- .custom(exists).withMessage('Should have a valid reporter search'),
- query('searchReportee')
- .optional()
- .custom(exists).withMessage('Should have a valid reportee search'),
- query('searchVideo')
- .optional()
- .custom(exists).withMessage('Should have a valid video search'),
- query('searchVideoChannel')
- .optional()
- .custom(exists).withMessage('Should have a valid video channel search'),
-
- (req: express.Request, res: express.Response, next: express.NextFunction) => {
- logger.debug('Checking videoAbuseListValidator parameters', { parameters: req.body })
-
- if (areValidationErrors(req, res)) return
-
- return next()
- }
-]
-
-// ---------------------------------------------------------------------------
-
-export {
- videoAbuseListValidator,
- videoAbuseReportValidator,
- videoAbuseGetValidator,
- videoAbuseUpdateValidator
-}
import * as Bluebird from 'bluebird'
-import { literal, Op } from 'sequelize'
+import { invert } from 'lodash'
+import { literal, Op, WhereOptions } from 'sequelize'
import {
AllowNull,
BelongsTo,
DataType,
Default,
ForeignKey,
+ HasOne,
Is,
Model,
Scopes,
Table,
UpdatedAt
} from 'sequelize-typescript'
-import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
-import {
- VideoAbuseState,
- VideoDetails,
- VideoAbusePredefinedReasons,
- VideoAbusePredefinedReasonsString,
- videoAbusePredefinedReasonsMap
-} from '../../../shared'
-import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
-import { VideoAbuse } from '../../../shared/models/videos'
+import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
import {
- isVideoAbuseModerationCommentValid,
- isVideoAbuseReasonValid,
- isVideoAbuseStateValid
-} from '../../helpers/custom-validators/video-abuses'
-import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
-import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models'
-import { AccountModel } from '../account/account'
+ Abuse,
+ AbuseObject,
+ AbusePredefinedReasons,
+ abusePredefinedReasonsMap,
+ AbusePredefinedReasonsString,
+ AbuseState,
+ AbuseVideoIs,
+ VideoAbuse
+} from '@shared/models'
+import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter'
+import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
+import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
+import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
-import { ThumbnailModel } from './thumbnail'
-import { VideoModel } from './video'
-import { VideoBlacklistModel } from './video-blacklist'
-import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
-import { invert } from 'lodash'
+import { ThumbnailModel } from '../video/thumbnail'
+import { VideoModel } from '../video/video'
+import { VideoBlacklistModel } from '../video/video-blacklist'
+import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
+import { VideoAbuseModel } from './video-abuse'
+import { VideoCommentAbuseModel } from './video-comment-abuse'
export enum ScopeNames {
FOR_API = 'FOR_API'
search?: string
searchReporter?: string
searchReportee?: string
+
+ // video releated
searchVideo?: string
searchVideoChannel?: string
+ videoIs?: AbuseVideoIs
// filters
id?: number
predefinedReasonId?: number
+ filter?: AbuseFilter
- state?: VideoAbuseState
- videoIs?: VideoAbuseVideoIs
+ state?: AbuseState
// accountIds
serverAccountId: number
userAccountId: number
}) => {
+ const onlyBlacklisted = options.videoIs === 'blacklisted'
+ const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel)
+
const where = {
reporterAccountId: {
[Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
}
if (options.search) {
+ const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%')
+
Object.assign(where, {
[Op.or]: [
{
[Op.and]: [
- { videoId: { [Op.not]: null } },
- searchAttribute(options.search, '$Video.name$')
+ { '$VideoAbuse.videoId$': { [Op.not]: null } },
+ searchAttribute(options.search, '$VideoAbuse.Video.name$')
]
},
{
[Op.and]: [
- { videoId: { [Op.not]: null } },
- searchAttribute(options.search, '$Video.VideoChannel.name$')
+ { '$VideoAbuse.videoId$': { [Op.not]: null } },
+ searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$')
]
},
{
[Op.and]: [
- { deletedVideo: { [Op.not]: null } },
- { deletedVideo: searchAttribute(options.search, 'name') }
+ { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
+ literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`)
]
},
{
[Op.and]: [
- { deletedVideo: { [Op.not]: null } },
- { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
+ { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
+ literal(`"VideoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE ${escapedSearch}`)
]
},
- searchAttribute(options.search, '$Account.name$')
+ searchAttribute(options.search, '$ReporterAccount.name$'),
+ searchAttribute(options.search, '$FlaggedAccount.name$')
]
})
}
if (options.videoIs === 'deleted') {
Object.assign(where, {
- deletedVideo: {
+ '$VideoAbuse.deletedVideo$': {
[Op.not]: null
}
})
})
}
- const onlyBlacklisted = options.videoIs === 'blacklisted'
-
return {
attributes: {
include: [
'(' +
'SELECT count(*) ' +
'FROM "videoAbuse" ' +
- 'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
+ 'WHERE "videoId" = "VideoAbuse"."videoId" ' +
')'
),
'countReportsForVideo'
'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
'FROM "videoAbuse" ' +
') t ' +
- 'WHERE t.id = "VideoAbuseModel".id ' +
+ 'WHERE t.id = "VideoAbuse".id' +
')'
),
'nthReportForVideo'
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
- 'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
+ 'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
')'
),
'countReportsForReporter__video'
'(' +
'SELECT count(DISTINCT "videoAbuse"."id") ' +
'FROM "videoAbuse" ' +
- `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
+ `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` +
')'
),
'countReportsForReporter__deletedVideo'
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON ' +
- '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' +
- `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
+ '"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' +
+ `OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
')'
),
'countReportsForReportee__video'
'(' +
'SELECT count(DISTINCT "videoAbuse"."id") ' +
'FROM "videoAbuse" ' +
- `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` +
+ `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuse->Video->VideoChannel"."accountId" ` +
`OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
- `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
+ `CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
')'
),
'countReportsForReportee__deletedVideo'
},
include: [
{
- model: AccountModel,
+ model: AccountModel.scope(AccountScopeNames.SUMMARY),
+ as: 'ReporterAccount',
required: true,
where: searchAttribute(options.searchReporter, 'name')
},
{
- model: VideoModel,
- required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
- where: searchAttribute(options.searchVideo, 'name'),
+ model: AccountModel.scope(AccountScopeNames.SUMMARY),
+ as: 'FlaggedAccount',
+ required: true,
+ where: searchAttribute(options.searchReportee, 'name')
+ },
+ {
+ model: VideoAbuseModel,
+ required: options.filter === 'video' || !!options.videoIs || videoRequired,
include: [
{
- model: ThumbnailModel
- },
- {
- model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
- where: searchAttribute(options.searchVideoChannel, 'name'),
+ model: VideoModel,
+ required: videoRequired,
+ where: searchAttribute(options.searchVideo, 'name'),
include: [
{
- model: AccountModel,
- where: searchAttribute(options.searchReportee, 'name')
+ model: ThumbnailModel
+ },
+ {
+ model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: false } as SummaryOptions ] }),
+ where: searchAttribute(options.searchVideoChannel, 'name'),
+ required: true,
+ include: [
+ {
+ model: AccountModel.scope(AccountScopeNames.SUMMARY),
+ required: true,
+ where: searchAttribute(options.searchReportee, 'name')
+ }
+ ]
+ },
+ {
+ attributes: [ 'id', 'reason', 'unfederated' ],
+ model: VideoBlacklistModel,
+ required: onlyBlacklisted
}
]
- },
- {
- attributes: [ 'id', 'reason', 'unfederated' ],
- model: VideoBlacklistModel,
- required: onlyBlacklisted
}
]
}
}
}))
@Table({
- tableName: 'videoAbuse',
+ tableName: 'abuse',
indexes: [
{
- fields: [ 'videoId' ]
+ fields: [ 'reporterAccountId' ]
},
{
- fields: [ 'reporterAccountId' ]
+ fields: [ 'flaggedAccountId' ]
}
]
})
-export class VideoAbuseModel extends Model<VideoAbuseModel> {
+export class AbuseModel extends Model<AbuseModel> {
@AllowNull(false)
@Default(null)
- @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
+ @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
reason: string
@AllowNull(false)
@Default(null)
- @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
+ @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
@Column
- state: VideoAbuseState
+ state: AbuseState
@AllowNull(true)
@Default(null)
- @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
- @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
+ @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
+ @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
moderationComment: string
- @AllowNull(true)
- @Default(null)
- @Column(DataType.JSONB)
- 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
+ predefinedReasons: AbusePredefinedReasons[]
@CreatedAt
createdAt: Date
@BelongsTo(() => AccountModel, {
foreignKey: {
+ name: 'reporterAccountId',
allowNull: true
},
+ as: 'ReporterAccount',
onDelete: 'set null'
})
- Account: AccountModel
+ ReporterAccount: AccountModel
- @ForeignKey(() => VideoModel)
+ @ForeignKey(() => AccountModel)
@Column
- videoId: number
+ flaggedAccountId: number
- @BelongsTo(() => VideoModel, {
+ @BelongsTo(() => AccountModel, {
foreignKey: {
+ name: 'flaggedAccountId',
allowNull: true
},
+ as: 'FlaggedAccount',
onDelete: 'set null'
})
- Video: VideoModel
+ FlaggedAccount: AccountModel
+
+ @HasOne(() => VideoCommentAbuseModel, {
+ foreignKey: {
+ name: 'abuseId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoCommentAbuse: VideoCommentAbuseModel
- static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
- const videoAttributes = {}
- if (videoId) videoAttributes['videoId'] = videoId
- if (uuid) videoAttributes['deletedVideo'] = { uuid }
+ @HasOne(() => VideoAbuseModel, {
+ foreignKey: {
+ name: 'abuseId',
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ VideoAbuse: VideoAbuseModel
+
+ static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
+ const videoWhere: WhereOptions = {}
+
+ if (videoId) videoWhere.videoId = videoId
+ if (uuid) videoWhere.deletedVideo = { uuid }
const query = {
+ include: [
+ {
+ model: VideoAbuseModel,
+ required: true,
+ where: videoWhere
+ }
+ ],
where: {
- id,
- ...videoAttributes
+ id
}
}
- return VideoAbuseModel.findOne(query)
+ return AbuseModel.findOne(query)
}
static listForApi (parameters: {
count: number
sort: string
+ filter?: AbuseFilter
+
serverAccountId: number
user?: MUserAccountId
id?: number
- predefinedReason?: VideoAbusePredefinedReasonsString
- state?: VideoAbuseState
- videoIs?: VideoAbuseVideoIs
+ predefinedReason?: AbusePredefinedReasonsString
+ state?: AbuseState
+ videoIs?: AbuseVideoIs
search?: string
searchReporter?: string
predefinedReason,
searchReportee,
searchVideo,
+ filter,
searchVideoChannel,
searchReporter,
id
} = parameters
const userAccountId = user ? user.Account.id : undefined
- const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
+ const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
const query = {
offset: start,
limit: count,
order: getSort(sort),
- col: 'VideoAbuseModel.id',
+ col: 'AbuseModel.id',
distinct: true
}
const filters = {
id,
+ filter,
predefinedReasonId,
search,
state,
userAccountId
}
- return VideoAbuseModel
+ return AbuseModel
.scope([
{ method: [ ScopeNames.FOR_API, filters ] }
])
})
}
- toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
- const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+ toFormattedJSON (this: MAbuseFormattable): Abuse {
+ const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
const countReportsForVideo = this.get('countReportsForVideo') as number
const nthReportForVideo = this.get('nthReportForVideo') as number
const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
- const video = this.Video
- ? this.Video
- : this.deletedVideo
+ let video: VideoAbuse
+
+ if (this.VideoAbuse) {
+ const abuseModel = this.VideoAbuse
+ const entity = abuseModel.Video || abuseModel.deletedVideo
+
+ video = {
+ id: entity.id,
+ uuid: entity.uuid,
+ name: entity.name,
+ nsfw: entity.nsfw,
+
+ startAt: abuseModel.startAt,
+ endAt: abuseModel.endAt,
+
+ deleted: !abuseModel.Video,
+ blacklisted: abuseModel.Video?.isBlacklisted() || false,
+ thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
+ channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel
+ }
+ }
return {
id: this.id,
reason: this.reason,
predefinedReasons,
- reporterAccount: this.Account.toFormattedJSON(),
+
+ reporterAccount: this.ReporterAccount.toFormattedJSON(),
+
state: {
id: this.state,
- label: VideoAbuseModel.getStateLabel(this.state)
+ label: AbuseModel.getStateLabel(this.state)
},
+
moderationComment: this.moderationComment,
- video: {
- id: video.id,
- uuid: video.uuid,
- name: video.name,
- nsfw: video.nsfw,
- deleted: !this.Video,
- blacklisted: this.Video?.isBlacklisted() || false,
- thumbnailPath: this.Video?.getMiniatureStaticPath(),
- channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
- },
+
+ video,
+ comment: null,
+
createdAt: this.createdAt,
updatedAt: this.updatedAt,
- startAt: this.startAt,
- endAt: this.endAt,
count: countReportsForVideo || 0,
nth: nthReportForVideo || 0,
countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
- countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0)
+ countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0),
+
+ // FIXME: deprecated in 2.3, remove this
+ startAt: null,
+ endAt: null
}
}
- toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
- const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+ toActivityPubObject (this: MAbuseAP): AbuseObject {
+ const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+
+ const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
- const startAt = this.startAt
- const endAt = this.endAt
+ const startAt = this.VideoAbuse?.startAt
+ const endAt = this.VideoAbuse?.endAt
return {
type: 'Flag' as 'Flag',
content: this.reason,
- object: this.Video.url,
+ object,
tag: predefinedReasons.map(r => ({
type: 'Hashtag' as 'Hashtag',
name: r
}
private static getStateLabel (id: number) {
- return VIDEO_ABUSE_STATES[id] || 'Unknown'
+ return ABUSE_STATES[id] || 'Unknown'
}
- private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] {
+ private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] {
return (predefinedReasons || [])
- .filter(r => r in VideoAbusePredefinedReasons)
- .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
+ .filter(r => r in AbusePredefinedReasons)
+ .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString)
}
}
--- /dev/null
+import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { VideoDetails } from '@shared/models'
+import { VideoModel } from '../video/video'
+import { AbuseModel } from './abuse'
+
+@Table({
+ tableName: 'videoAbuse',
+ indexes: [
+ {
+ fields: [ 'abuseId' ]
+ },
+ {
+ fields: [ 'videoId' ]
+ }
+ ]
+})
+export class VideoAbuseModel extends Model<VideoAbuseModel> {
+
+ @CreatedAt
+ createdAt: Date
+
+ @UpdatedAt
+ updatedAt: Date
+
+ @AllowNull(true)
+ @Default(null)
+ @Column
+ startAt: number
+
+ @AllowNull(true)
+ @Default(null)
+ @Column
+ endAt: number
+
+ @AllowNull(true)
+ @Default(null)
+ @Column(DataType.JSONB)
+ deletedVideo: VideoDetails
+
+ @ForeignKey(() => AbuseModel)
+ @Column
+ abuseId: number
+
+ @BelongsTo(() => AbuseModel, {
+ foreignKey: {
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ Abuse: AbuseModel
+
+ @ForeignKey(() => VideoModel)
+ @Column
+ videoId: number
+
+ @BelongsTo(() => VideoModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'set null'
+ })
+ Video: VideoModel
+}
--- /dev/null
+import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { VideoComment } from '@shared/models'
+import { VideoCommentModel } from '../video/video-comment'
+import { AbuseModel } from './abuse'
+
+@Table({
+ tableName: 'commentAbuse',
+ indexes: [
+ {
+ fields: [ 'abuseId' ]
+ },
+ {
+ fields: [ 'videoCommentId' ]
+ }
+ ]
+})
+export class VideoCommentAbuseModel extends Model<VideoCommentAbuseModel> {
+
+ @CreatedAt
+ createdAt: Date
+
+ @UpdatedAt
+ updatedAt: Date
+
+ @AllowNull(true)
+ @Default(null)
+ @Column(DataType.JSONB)
+ deletedComment: VideoComment
+
+ @ForeignKey(() => AbuseModel)
+ @Column
+ abuseId: number
+
+ @BelongsTo(() => AbuseModel, {
+ foreignKey: {
+ allowNull: false
+ },
+ onDelete: 'cascade'
+ })
+ Abuse: AbuseModel
+
+ @ForeignKey(() => VideoCommentModel)
+ @Column
+ videoCommentId: number
+
+ @BelongsTo(() => VideoCommentModel, {
+ foreignKey: {
+ allowNull: true
+ },
+ onDelete: 'set null'
+ })
+ VideoComment: VideoCommentModel
+}
-import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
-import { AccountModel } from './account'
-import { getSort, searchAttribute } from '../utils'
-import { AccountBlock } from '../../../shared/models/blocklist'
-import { Op } from 'sequelize'
import * as Bluebird from 'bluebird'
+import { Op } from 'sequelize'
+import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
+import { AccountBlock } from '../../../shared/models'
import { ActorModel } from '../activitypub/actor'
import { ServerModel } from '../server/server'
+import { getSort, searchAttribute } from '../utils'
+import { AccountModel } from './account'
enum ScopeNames {
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
.findAll(query)
}
+ getClientUrl () {
+ return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
+ }
+
toFormattedJSON (this: MAccountFormattable): Account {
const actor = this.Actor.toFormattedJSON()
const account = {
+import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
import { UserNotification, UserNotificationType } from '../../../shared'
-import { getSort, throwIfNotValid } from '../utils'
import { isBooleanValid } from '../../helpers/custom-validators/misc'
import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
-import { UserModel } from './user'
-import { VideoModel } from '../video/video'
-import { VideoCommentModel } from '../video/video-comment'
-import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
-import { VideoChannelModel } from '../video/video-channel'
-import { AccountModel } from './account'
-import { VideoAbuseModel } from '../video/video-abuse'
-import { VideoBlacklistModel } from '../video/video-blacklist'
-import { VideoImportModel } from '../video/video-import'
+import { AbuseModel } from '../abuse/abuse'
+import { VideoAbuseModel } from '../abuse/video-abuse'
+import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
import { ActorModel } from '../activitypub/actor'
import { ActorFollowModel } from '../activitypub/actor-follow'
import { AvatarModel } from '../avatar/avatar'
import { ServerModel } from '../server/server'
-import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
+import { getSort, throwIfNotValid } from '../utils'
+import { VideoModel } from '../video/video'
+import { VideoBlacklistModel } from '../video/video-blacklist'
+import { VideoChannelModel } from '../video/video-channel'
+import { VideoCommentModel } from '../video/video-comment'
+import { VideoImportModel } from '../video/video-import'
+import { AccountModel } from './account'
+import { UserModel } from './user'
enum ScopeNames {
WITH_ALL = 'WITH_ALL'
{
attributes: [ 'id' ],
- model: VideoAbuseModel.unscoped(),
+ model: AbuseModel.unscoped(),
required: false,
- include: [ buildVideoInclude(true) ]
+ include: [
+ {
+ attributes: [ 'id' ],
+ model: VideoAbuseModel.unscoped(),
+ required: false,
+ include: [ buildVideoInclude(true) ]
+ },
+ {
+ attributes: [ 'id' ],
+ model: VideoCommentAbuseModel.unscoped(),
+ required: false,
+ include: [
+ {
+ attributes: [ 'id', 'originCommentId' ],
+ model: VideoCommentModel,
+ required: true,
+ include: [
+ {
+ attributes: [ 'uuid' ],
+ model: VideoModel.unscoped(),
+ required: true
+ }
+ ]
+ }
+ ]
+ },
+ {
+ model: AccountModel,
+ as: 'FlaggedAccount',
+ required: true,
+ include: [ buildActorWithAvatarInclude() ]
+ }
+ ]
},
{
}
},
{
- fields: [ 'videoAbuseId' ],
+ fields: [ 'abuseId' ],
where: {
- videoAbuseId: {
+ abuseId: {
[Op.ne]: null
}
}
})
Comment: VideoCommentModel
- @ForeignKey(() => VideoAbuseModel)
+ @ForeignKey(() => AbuseModel)
@Column
- videoAbuseId: number
+ abuseId: number
- @BelongsTo(() => VideoAbuseModel, {
+ @BelongsTo(() => AbuseModel, {
foreignKey: {
allowNull: true
},
onDelete: 'cascade'
})
- VideoAbuse: VideoAbuseModel
+ Abuse: AbuseModel
@ForeignKey(() => VideoBlacklistModel)
@Column
video: this.formatVideo(this.Comment.Video)
} : undefined
- const videoAbuse = this.VideoAbuse ? {
- id: this.VideoAbuse.id,
- video: this.formatVideo(this.VideoAbuse.Video)
- } : undefined
+ const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
const videoBlacklist = this.VideoBlacklist ? {
id: this.VideoBlacklist.id,
video,
videoImport,
comment,
- videoAbuse,
+ abuse,
videoBlacklist,
account,
actorFollow,
}
}
+ formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) {
+ const commentAbuse = abuse.VideoCommentAbuse?.VideoComment ? {
+ threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
+
+ video: {
+ uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
+ }
+ } : undefined
+
+ const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
+
+ const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined
+
+ return {
+ id: abuse.id,
+ video: videoAbuse,
+ comment: commentAbuse,
+ account: accountAbuse
+ }
+ }
+
formatActor (
this: UserNotificationModelForApi,
accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
Table,
UpdatedAt
} from 'sequelize-typescript'
-import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
+import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
import { User, UserRole } from '../../../shared/models/users'
import {
isNoInstanceConfigWarningModal,
`SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
'FROM (' +
'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
- `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
+ `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
'FROM "videoAbuse" ' +
'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
+import * as Bluebird from 'bluebird'
+import { Op } from 'sequelize'
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
+import { ServerBlock } from '@shared/models'
import { AccountModel } from '../account/account'
-import { ServerModel } from './server'
-import { ServerBlock } from '../../../shared/models/blocklist'
import { getSort, searchAttribute } from '../utils'
-import * as Bluebird from 'bluebird'
-import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
-import { Op } from 'sequelize'
+import { ServerModel } from './server'
enum ScopeNames {
WITH_ACCOUNT = 'WITH_ACCOUNT',
import * as Bluebird from 'bluebird'
+import { remove } from 'fs-extra'
import { maxBy, minBy, pick } from 'lodash'
import { join } from 'path'
import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
Table,
UpdatedAt
} from 'sequelize-typescript'
-import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared'
+import { buildNSFWFilter } from '@server/helpers/express-utils'
+import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
+import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { getServerActor } from '@server/models/application/application'
+import { ModelCache } from '@server/models/model-cache'
+import { VideoFile } from '@shared/models/videos/video-file.model'
+import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
import { Video, VideoDetails } from '../../../shared/models/videos'
+import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { VideoFilter } from '../../../shared/models/videos/video-query.type'
+import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import { peertubeTruncate } from '../../helpers/core-utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { isBooleanValid } from '../../helpers/custom-validators/misc'
} from '../../helpers/custom-validators/videos'
import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
import {
ACTIVITY_PUB,
API_VERSION,
WEBSERVER
} from '../../initializers/constants'
import { sendDeleteVideo } from '../../lib/activitypub/send'
-import { AccountModel } from '../account/account'
-import { AccountVideoRateModel } from '../account/account-video-rate'
-import { ActorModel } from '../activitypub/actor'
-import { AvatarModel } from '../avatar/avatar'
-import { ServerModel } from '../server/server'
-import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
-import { TagModel } from './tag'
-import { VideoAbuseModel } from './video-abuse'
-import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
-import { VideoCommentModel } from './video-comment'
-import { VideoFileModel } from './video-file'
-import { VideoShareModel } from './video-share'
-import { VideoTagModel } from './video-tag'
-import { ScheduleVideoUpdateModel } from './schedule-video-update'
-import { VideoCaptionModel } from './video-caption'
-import { VideoBlacklistModel } from './video-blacklist'
-import { remove } from 'fs-extra'
-import { VideoViewModel } from './video-view'
-import { VideoRedundancyModel } from '../redundancy/video-redundancy'
-import {
- videoFilesModelToFormattedJSON,
- VideoFormattingJSONOptions,
- videoModelToActivityPubObject,
- videoModelToFormattedDetailsJSON,
- videoModelToFormattedJSON
-} from './video-format-utils'
-import { UserVideoHistoryModel } from '../account/user-video-history'
-import { VideoImportModel } from './video-import'
-import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
-import { VideoPlaylistElementModel } from './video-playlist-element'
-import { CONFIG } from '../../initializers/config'
-import { ThumbnailModel } from './thumbnail'
-import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
-import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
import {
MChannel,
MChannelAccountDefault,
MVideoWithFile,
MVideoWithRights
} from '../../types/models'
-import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
import { MThumbnail } from '../../types/models/video/thumbnail'
-import { VideoFile } from '@shared/models/videos/video-file.model'
-import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
-import { ModelCache } from '@server/models/model-cache'
+import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
+import { VideoAbuseModel } from '../abuse/video-abuse'
+import { AccountModel } from '../account/account'
+import { AccountVideoRateModel } from '../account/account-video-rate'
+import { UserVideoHistoryModel } from '../account/user-video-history'
+import { ActorModel } from '../activitypub/actor'
+import { AvatarModel } from '../avatar/avatar'
+import { VideoRedundancyModel } from '../redundancy/video-redundancy'
+import { ServerModel } from '../server/server'
+import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
+import { ScheduleVideoUpdateModel } from './schedule-video-update'
+import { TagModel } from './tag'
+import { ThumbnailModel } from './thumbnail'
+import { VideoBlacklistModel } from './video-blacklist'
+import { VideoCaptionModel } from './video-caption'
+import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
+import { VideoCommentModel } from './video-comment'
+import { VideoFileModel } from './video-file'
+import {
+ videoFilesModelToFormattedJSON,
+ VideoFormattingJSONOptions,
+ videoModelToActivityPubObject,
+ videoModelToFormattedDetailsJSON,
+ videoModelToFormattedJSON
+} from './video-format-utils'
+import { VideoImportModel } from './video-import'
+import { VideoPlaylistElementModel } from './video-playlist-element'
import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
-import { buildNSFWFilter } from '@server/helpers/express-utils'
-import { getServerActor } from '@server/models/application/application'
-import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video"
+import { VideoShareModel } from './video-share'
+import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
+import { VideoTagModel } from './video-tag'
+import { VideoViewModel } from './video-view'
export enum ScopeNames {
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import 'mocha'
-
+import { AbuseState, VideoAbuseCreate } from '@shared/models'
import {
cleanupTests,
createUser,
checkBadSortPagination,
checkBadStartPagination
} from '../../../../shared/extra-utils/requests/check-api-params'
-import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos'
+
+// FIXME: deprecated in 2.3. Remove this controller
describe('Test video abuses API validators', function () {
let server: ServerInfo
const fields = { reason: 'my super reason' }
const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
- videoAbuseId = res.body.videoAbuse.id
+ videoAbuseId = res.body.abuse.id
})
it('Should fail with a wrong predefined reason', async function () {
})
it('Should succeed with the correct params', async function () {
- const body = { state: VideoAbuseState.ACCEPTED }
+ const body = { state: AbuseState.ACCEPTED }
await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body)
})
})
import * as chai from 'chai'
import 'mocha'
-import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index'
+import { MyUser, User, UserRole, Video, AbuseState, AbuseUpdate, VideoPlaylistType } from '@shared/models'
import {
addVideoCommentThread,
blockUser,
expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created
- const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED }
+ const body: AbuseUpdate = { state: AbuseState.ACCEPTED }
await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body)
const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import * as chai from 'chai'
import 'mocha'
-import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos'
+import * as chai from 'chai'
+import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models'
import {
cleanupTests,
+ createUser,
deleteVideoAbuse,
flushAndRunMultipleServers,
getVideoAbusesList,
getVideosList,
+ removeVideo,
reportVideoAbuse,
ServerInfo,
setAccessTokensToServers,
updateVideoAbuse,
uploadVideo,
- removeVideo,
- createUser,
userLogin
} from '../../../../shared/extra-utils/index'
import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
const expect = chai.expect
+// FIXME: deprecated in 2.3. Remove this controller
+
describe('Test video abuses', function () {
let servers: ServerInfo[] = []
- let abuseServer2: VideoAbuse
+ let abuseServer2: Abuse
before(async function () {
this.timeout(50000)
expect(res1.body.data).to.be.an('array')
expect(res1.body.data.length).to.equal(1)
- const abuse: VideoAbuse = res1.body.data[0]
+ const abuse: Abuse = res1.body.data[0]
expect(abuse.reason).to.equal('my super bad reason')
expect(abuse.reporterAccount.name).to.equal('root')
expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
expect(res1.body.data).to.be.an('array')
expect(res1.body.data.length).to.equal(2)
- const abuse1: VideoAbuse = res1.body.data[0]
+ const abuse1: Abuse = res1.body.data[0]
expect(abuse1.reason).to.equal('my super bad reason')
expect(abuse1.reporterAccount.name).to.equal('root')
expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
expect(abuse1.video.id).to.equal(servers[0].video.id)
- expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING)
+ expect(abuse1.state.id).to.equal(AbuseState.PENDING)
expect(abuse1.state.label).to.equal('Pending')
expect(abuse1.moderationComment).to.be.null
expect(abuse1.count).to.equal(1)
expect(abuse1.nth).to.equal(1)
- const abuse2: VideoAbuse = res1.body.data[1]
+ const abuse2: Abuse = res1.body.data[1]
expect(abuse2.reason).to.equal('my super bad reason 2')
expect(abuse2.reporterAccount.name).to.equal('root')
expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
expect(abuse2.video.id).to.equal(servers[1].video.id)
- expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING)
+ expect(abuse2.state.id).to.equal(AbuseState.PENDING)
expect(abuse2.state.label).to.equal('Pending')
expect(abuse2.moderationComment).to.be.null
expect(abuseServer2.reason).to.equal('my super bad reason 2')
expect(abuseServer2.reporterAccount.name).to.equal('root')
expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
- expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING)
+ expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
expect(abuseServer2.state.label).to.equal('Pending')
expect(abuseServer2.moderationComment).to.be.null
})
it('Should update the state of a video abuse', async function () {
- const body = { state: VideoAbuseState.REJECTED }
+ const body = { state: AbuseState.REJECTED }
await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
- expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED)
+ expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
})
it('Should add a moderation comment', async function () {
- const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' }
+ const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
- expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED)
+ expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
expect(res.body.data[0].moderationComment).to.equal('It is valid')
})
expect(res.body.data.length).to.equal(2, "wrong number of videos returned")
expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video")
- const abuse: VideoAbuse = res.body.data[0]
+ const abuse: Abuse = res.body.data[0]
expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
expect(abuse.video.channel).to.exist
expect(abuse.video.deleted).to.be.true
const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
{
- for (const abuse of res2.body.data as VideoAbuse[]) {
+ for (const abuse of res2.body.data as Abuse[]) {
if (abuse.video.id === video3.id) {
expect(abuse.count).to.equal(1, "wrong reports count for video 3")
expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3")
this.timeout(10000)
const reason5 = 'my super bad reason 5'
- const predefinedReasons5: VideoAbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
+ const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
const createdAbuse = (await reportVideoAbuse(
servers[0].url,
servers[0].accessToken,
predefinedReasons5,
1,
5
- )).body.videoAbuse as VideoAbuse
+ )).body.abuse
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)
+ const abuse = (res.body.data as Abuse[]).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")
+ expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
+ expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
}
})
const res = await getVideoAbusesList(options)
- return res.body.data as VideoAbuse[]
+ return res.body.data as Abuse[]
}
expect(await list({ id: 56 })).to.have.lengthOf(0)
expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
- expect(await list({ searchReportee: 'root' })).to.have.lengthOf(4)
+ expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
- expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0)
- expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(6)
+ expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
+ expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
export * from './account'
+export * from './moderation'
export * from './oauth'
export * from './server'
export * from './user'
--- /dev/null
+import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
+import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
+import { PickWith } from '@shared/core-utils'
+import { AbuseModel } from '../../../models/abuse/abuse'
+import { MAccountDefault, MAccountFormattable, MAccountLight, MAccountUrl } from '../account'
+import { MCommentOwner, MCommentUrl, MVideoUrl, MCommentOwnerVideo } from '../video'
+import { MVideo, MVideoAccountLightBlacklistAllFiles } from '../video/video'
+
+type Use<K extends keyof AbuseModel, M> = PickWith<AbuseModel, K, M>
+type UseVideoAbuse<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
+type UseCommentAbuse<K extends keyof VideoCommentAbuseModel, M> = PickWith<VideoCommentAbuseModel, K, M>
+
+// ############################################################################
+
+export type MAbuse = Omit<AbuseModel, 'VideoCommentAbuse' | 'VideoAbuse' | 'ReporterAccount' | 'FlaggedAccount' | 'toActivityPubObject'>
+
+export type MVideoAbuse = Omit<VideoAbuseModel, 'Abuse' | 'Video'>
+
+export type MCommentAbuse = Omit<VideoCommentAbuseModel, 'Abuse' | 'VideoComment'>
+
+// ############################################################################
+
+export type MVideoAbuseVideo =
+ MVideoAbuse &
+ UseVideoAbuse<'Video', MVideo>
+
+export type MVideoAbuseVideoUrl =
+ MVideoAbuse &
+ UseVideoAbuse<'Video', MVideoUrl>
+
+export type MVideoAbuseVideoFull =
+ MVideoAbuse &
+ UseVideoAbuse<'Video', MVideoAccountLightBlacklistAllFiles>
+
+export type MVideoAbuseFormattable =
+ MVideoAbuse &
+ UseVideoAbuse<'Video', Pick<MVideoAccountLightBlacklistAllFiles,
+ 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>>
+
+// ############################################################################
+
+export type MCommentAbuseAccount =
+ MCommentAbuse &
+ UseCommentAbuse<'VideoComment', MCommentOwner>
+
+export type MCommentAbuseAccountVideo =
+ MCommentAbuse &
+ UseCommentAbuse<'VideoComment', MCommentOwnerVideo>
+
+export type MCommentAbuseUrl =
+ MCommentAbuse &
+ UseCommentAbuse<'VideoComment', MCommentUrl>
+
+// ############################################################################
+
+export type MAbuseId = Pick<AbuseModel, 'id'>
+
+export type MAbuseVideo =
+ MAbuse &
+ Pick<AbuseModel, 'toActivityPubObject'> &
+ Use<'VideoAbuse', MVideoAbuseVideo>
+
+export type MAbuseUrl =
+ MAbuse &
+ Use<'VideoAbuse', MVideoAbuseVideoUrl> &
+ Use<'VideoCommentAbuse', MCommentAbuseUrl>
+
+export type MAbuseAccountVideo =
+ MAbuse &
+ Pick<AbuseModel, 'toActivityPubObject'> &
+ Use<'VideoAbuse', MVideoAbuseVideoFull> &
+ Use<'ReporterAccount', MAccountDefault>
+
+export type MAbuseAP =
+ MAbuse &
+ Pick<AbuseModel, 'toActivityPubObject'> &
+ Use<'ReporterAccount', MAccountUrl> &
+ Use<'FlaggedAccount', MAccountUrl> &
+ Use<'VideoAbuse', MVideoAbuseVideo> &
+ Use<'VideoCommentAbuse', MCommentAbuseAccount>
+
+export type MAbuseFull =
+ MAbuse &
+ Pick<AbuseModel, 'toActivityPubObject'> &
+ Use<'ReporterAccount', MAccountLight> &
+ Use<'FlaggedAccount', MAccountLight> &
+ Use<'VideoAbuse', MVideoAbuseVideoFull> &
+ Use<'VideoCommentAbuse', MCommentAbuseAccountVideo>
+
+// ############################################################################
+
+// Format for API or AP object
+
+export type MAbuseFormattable =
+ MAbuse &
+ Use<'ReporterAccount', MAccountFormattable> &
+ Use<'VideoAbuse', MVideoAbuseFormattable>
--- /dev/null
+export * from './abuse'
-import { UserNotificationModel } from '../../../models/account/user-notification'
+import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
+import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
import { PickWith, PickWithOpt } from '@shared/core-utils'
-import { VideoModel } from '../../../models/video/video'
+import { AbuseModel } from '../../../models/abuse/abuse'
+import { AccountModel } from '../../../models/account/account'
+import { UserNotificationModel } from '../../../models/account/user-notification'
import { ActorModel } from '../../../models/activitypub/actor'
-import { ServerModel } from '../../../models/server/server'
+import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
import { AvatarModel } from '../../../models/avatar/avatar'
+import { ServerModel } from '../../../models/server/server'
+import { VideoModel } from '../../../models/video/video'
+import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
import { VideoChannelModel } from '../../../models/video/video-channel'
-import { AccountModel } from '../../../models/account/account'
import { VideoCommentModel } from '../../../models/video/video-comment'
-import { VideoAbuseModel } from '../../../models/video/video-abuse'
-import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
import { VideoImportModel } from '../../../models/video/video-import'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M>
Pick<VideoAbuseModel, 'id'> &
PickWith<VideoAbuseModel, 'Video', VideoInclude>
+ export type VideoCommentAbuseInclude =
+ Pick<VideoCommentAbuseModel, 'id'> &
+ PickWith<VideoCommentAbuseModel, 'VideoComment',
+ Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> &
+ PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'uuid'>>>
+
+ export type AbuseInclude =
+ Pick<AbuseModel, 'id'> &
+ PickWith<AbuseModel, 'VideoAbuse', VideoAbuseInclude> &
+ PickWith<AbuseModel, 'VideoCommentAbuse', VideoCommentAbuseInclude> &
+ PickWith<AbuseModel, 'FlaggedAccount', AccountIncludeActor>
+
export type VideoBlacklistInclude =
Pick<VideoBlacklistModel, 'id'> &
PickWith<VideoAbuseModel, 'Video', VideoInclude>
// ############################################################################
export type MUserNotification =
- Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' |
+ Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
'VideoImport' | 'Account' | 'ActorFollow'>
// ############################################################################
MUserNotification &
Use<'Video', UserNotificationIncludes.VideoIncludeChannel> &
Use<'Comment', UserNotificationIncludes.VideoCommentInclude> &
- Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> &
+ Use<'Abuse', UserNotificationIncludes.AbuseInclude> &
Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
export * from './tag'
export * from './thumbnail'
export * from './video'
-export * from './video-abuse'
export * from './video-blacklist'
export * from './video-caption'
export * from './video-change-ownership'
+++ /dev/null
-import { VideoAbuseModel } from '../../../models/video/video-abuse'
-import { PickWith } from '@shared/core-utils'
-import { MVideoAccountLightBlacklistAllFiles, MVideo } from './video'
-import { MAccountDefault, MAccountFormattable } from '../account'
-
-type Use<K extends keyof VideoAbuseModel, M> = PickWith<VideoAbuseModel, K, M>
-
-// ############################################################################
-
-export type MVideoAbuse = Omit<VideoAbuseModel, 'Account' | 'Video' | 'toActivityPubObject'>
-
-// ############################################################################
-
-export type MVideoAbuseId = Pick<VideoAbuseModel, 'id'>
-
-export type MVideoAbuseVideo =
- MVideoAbuse &
- Pick<VideoAbuseModel, 'toActivityPubObject'> &
- Use<'Video', MVideo>
-
-export type MVideoAbuseAccountVideo =
- MVideoAbuse &
- Pick<VideoAbuseModel, 'toActivityPubObject'> &
- Use<'Video', MVideoAccountLightBlacklistAllFiles> &
- Use<'Account', MAccountDefault>
-
-// ############################################################################
-
-// Format for API or AP object
-
-export type MVideoAbuseFormattable =
- MVideoAbuse &
- Use<'Account', MAccountFormattable> &
- Use<'Video', Pick<MVideoAccountLightBlacklistAllFiles,
- 'id' | 'uuid' | 'name' | 'nsfw' | 'getMiniatureStaticPath' | 'isBlacklisted' | 'VideoChannel'>>
import { RegisterServerAuthExternalOptions } from '@server/types'
import {
+ MAbuse,
MAccountBlocklist,
MActorUrl,
MStreamingPlaylist,
MComment,
MCommentOwnerVideoReply,
MUserDefault,
- MVideoAbuse,
MVideoBlacklist,
MVideoCaptionVideo,
MVideoFullLight,
videoCaption?: MVideoCaptionVideo
- videoAbuse?: MVideoAbuse
+ abuse?: MAbuse
videoStreamingPlaylist?: MStreamingPlaylist
export * from './videos/video-playlists'
export * from './users/users'
export * from './users/accounts'
+export * from './moderation/abuses'
export * from './videos/video-abuses'
export * from './videos/video-blacklist'
export * from './videos/video-captions'
--- /dev/null
+import * as request from 'supertest'
+import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
+import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
+
+function reportAbuse (
+ url: string,
+ token: string,
+ videoId: number | string,
+ reason: string,
+ predefinedReasons?: AbusePredefinedReasonsString[],
+ startAt?: number,
+ endAt?: number,
+ specialStatus = 200
+) {
+ const path = '/api/v1/videos/' + videoId + '/abuse'
+
+ return request(url)
+ .post(path)
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + token)
+ .send({ reason, predefinedReasons, startAt, endAt })
+ .expect(specialStatus)
+}
+
+function getAbusesList (options: {
+ url: string
+ token: string
+ id?: number
+ predefinedReason?: AbusePredefinedReasonsString
+ search?: string
+ state?: AbuseState
+ videoIs?: AbuseVideoIs
+ searchReporter?: string
+ searchReportee?: string
+ searchVideo?: string
+ searchVideoChannel?: string
+}) {
+ const {
+ url,
+ token,
+ id,
+ predefinedReason,
+ search,
+ state,
+ videoIs,
+ searchReporter,
+ searchReportee,
+ searchVideo,
+ searchVideoChannel
+ } = options
+ const path = '/api/v1/videos/abuse'
+
+ const query = {
+ sort: 'createdAt',
+ id,
+ predefinedReason,
+ search,
+ state,
+ videoIs,
+ searchReporter,
+ searchReportee,
+ searchVideo,
+ searchVideoChannel
+ }
+
+ return makeGetRequest({
+ url,
+ path,
+ token,
+ query,
+ statusCodeExpected: 200
+ })
+}
+
+function updateAbuse (
+ url: string,
+ token: string,
+ videoId: string | number,
+ videoAbuseId: number,
+ body: AbuseUpdate,
+ statusCodeExpected = 204
+) {
+ const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
+
+ return makePutBodyRequest({
+ url,
+ token,
+ path,
+ fields: body,
+ statusCodeExpected
+ })
+}
+
+function deleteAbuse (url: string, token: string, videoId: string | number, videoAbuseId: number, statusCodeExpected = 204) {
+ const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
+
+ return makeDeleteRequest({
+ url,
+ token,
+ path,
+ statusCodeExpected
+ })
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+ reportAbuse,
+ getAbusesList,
+ updateAbuse,
+ deleteAbuse
+}
expect(notification).to.not.be.undefined
expect(notification.type).to.equal(notificationType)
- expect(notification.videoAbuse.id).to.be.a('number')
- checkVideo(notification.videoAbuse.video, videoName, videoUUID)
+ expect(notification.abuse.id).to.be.a('number')
+ checkVideo(notification.abuse.video, videoName, videoUUID)
} else {
expect(notification).to.satisfy((n: UserNotification) => {
- return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID
+ return n === undefined || n.abuse === undefined || n.abuse.video.uuid !== videoUUID
})
}
}
import * as request from 'supertest'
-import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
-import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests'
-import { VideoAbuseState, VideoAbusePredefinedReasonsString } from '@shared/models'
-import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
+import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
+import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
+
+// FIXME: deprecated in 2.3. Remove this file
function reportVideoAbuse (
url: string,
token: string,
videoId: number | string,
reason: string,
- predefinedReasons?: VideoAbusePredefinedReasonsString[],
+ predefinedReasons?: AbusePredefinedReasonsString[],
startAt?: number,
endAt?: number,
specialStatus = 200
url: string
token: string
id?: number
- predefinedReason?: VideoAbusePredefinedReasonsString
+ predefinedReason?: AbusePredefinedReasonsString
search?: string
- state?: VideoAbuseState
- videoIs?: VideoAbuseVideoIs
+ state?: AbuseState
+ videoIs?: AbuseVideoIs
searchReporter?: string
searchReportee?: string
searchVideo?: string
token: string,
videoId: string | number,
videoAbuseId: number,
- body: VideoAbuseUpdate,
+ body: AbuseUpdate,
statusCodeExpected = 204
) {
const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
import { ActivityPubActor } from './activitypub-actor'
import { ActivityPubSignature } from './activitypub-signature'
-import { CacheFileObject, VideoTorrentObject, ActivityFlagReasonObject } from './objects'
+import { ActivityFlagReasonObject, CacheFileObject, VideoTorrentObject } from './objects'
+import { AbuseObject } from './objects/abuse-object'
import { DislikeObject } from './objects/dislike-object'
-import { VideoAbuseObject } from './objects/video-abuse-object'
-import { VideoCommentObject } from './objects/video-comment-object'
-import { ViewObject } from './objects/view-object'
import { APObject } from './objects/object.model'
import { PlaylistObject } from './objects/playlist-object'
+import { VideoCommentObject } from './objects/video-comment-object'
+import { ViewObject } from './objects/view-object'
export type Activity =
ActivityCreate |
export interface ActivityCreate extends BaseActivity {
type: 'Create'
- object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject
+ object: VideoTorrentObject | AbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject
}
export interface ActivityUpdate extends BaseActivity {
import { ActivityFlagReasonObject } from './common-objects'
-export interface VideoAbuseObject {
+export interface AbuseObject {
type: 'Flag'
content: string
object: string | string[]
+
tag?: ActivityFlagReasonObject[]
+
startAt?: number
endAt?: number
}
-import { VideoAbusePredefinedReasonsString } from '@shared/models/videos'
+import { AbusePredefinedReasonsString } from '@shared/models'
export interface ActivityIdentifierObject {
identifier: string
export interface ActivityFlagReasonObject {
type: 'Hashtag'
- name: VideoAbusePredefinedReasonsString
+ name: AbusePredefinedReasonsString
}
export type ActivityTagObject =
+export * from './abuse-object'
export * from './cache-file-object'
export * from './common-objects'
-export * from './video-abuse-object'
+export * from './dislike-object'
export * from './video-torrent-object'
export * from './view-object'
-export * from './dislike-object'
export * from './activitypub'
export * from './actors'
export * from './avatars'
-export * from './blocklist'
+export * from './moderation'
export * from './bulk'
export * from './redundancy'
export * from './users'
export * from './server'
export * from './oauth-client-local.model'
export * from './result-list.model'
-export * from './server/server-config.model'
--- /dev/null
+import { AbusePredefinedReasonsString } from './abuse-reason.model'
+
+export interface AbuseCreate {
+ accountId: number
+
+ reason: string
+ predefinedReasons?: AbusePredefinedReasonsString[]
+
+ video?: {
+ id: number
+ startAt?: number
+ endAt?: number
+ }
+
+ comment?: {
+ id: number
+ }
+}
+
+// FIXME: deprecated in 2.3. Remove it
+export interface VideoAbuseCreate {
+ reason: string
+ predefinedReasons?: AbusePredefinedReasonsString[]
+ startAt?: number
+ endAt?: number
+}
--- /dev/null
+export type AbuseFilter = 'video' | 'comment'
--- /dev/null
+export enum AbusePredefinedReasons {
+ VIOLENT_OR_REPULSIVE = 1,
+ HATEFUL_OR_ABUSIVE,
+ SPAM_OR_MISLEADING,
+ PRIVACY,
+ RIGHTS,
+ SERVER_RULES,
+ THUMBNAILS,
+ CAPTIONS
+}
+
+export type AbusePredefinedReasonsString =
+ 'violentOrRepulsive' |
+ 'hatefulOrAbusive' |
+ 'spamOrMisleading' |
+ 'privacy' |
+ 'rights' |
+ 'serverRules' |
+ 'thumbnails' |
+ 'captions'
+
+export const abusePredefinedReasonsMap: {
+ [key in AbusePredefinedReasonsString]: AbusePredefinedReasons
+} = {
+ violentOrRepulsive: AbusePredefinedReasons.VIOLENT_OR_REPULSIVE,
+ hatefulOrAbusive: AbusePredefinedReasons.HATEFUL_OR_ABUSIVE,
+ spamOrMisleading: AbusePredefinedReasons.SPAM_OR_MISLEADING,
+ privacy: AbusePredefinedReasons.PRIVACY,
+ rights: AbusePredefinedReasons.RIGHTS,
+ serverRules: AbusePredefinedReasons.SERVER_RULES,
+ thumbnails: AbusePredefinedReasons.THUMBNAILS,
+ captions: AbusePredefinedReasons.CAPTIONS
+}
-export enum VideoAbuseState {
+export enum AbuseState {
PENDING = 1,
REJECTED = 2,
ACCEPTED = 3
--- /dev/null
+import { AbuseState } from './abuse-state.model'
+
+export interface AbuseUpdate {
+ moderationComment?: string
+
+ state?: AbuseState
+}
--- /dev/null
+export type AbuseVideoIs = 'deleted' | 'blacklisted'
--- /dev/null
+import { Account } from '../../actors/account.model'
+import { AbuseState } from './abuse-state.model'
+import { AbusePredefinedReasonsString } from './abuse-reason.model'
+import { VideoConstant } from '../../videos/video-constant.model'
+import { VideoChannel } from '../../videos/channel/video-channel.model'
+
+export interface VideoAbuse {
+ id: number
+ name: string
+ uuid: string
+ nsfw: boolean
+ deleted: boolean
+ blacklisted: boolean
+
+ startAt: number | null
+ endAt: number | null
+
+ thumbnailPath?: string
+ channel?: VideoChannel
+}
+
+export interface VideoCommentAbuse {
+ id: number
+ account?: Account
+ text: string
+ deleted: boolean
+}
+
+export interface Abuse {
+ id: number
+ reason: string
+ predefinedReasons?: AbusePredefinedReasonsString[]
+ reporterAccount: Account
+
+ state: VideoConstant<AbuseState>
+ moderationComment?: string
+
+ video?: VideoAbuse
+ comment?: VideoCommentAbuse
+
+ createdAt: Date
+ updatedAt: Date
+
+ // FIXME: deprecated in 2.3, remove this
+ startAt: null
+ endAt: null
+
+ count?: number
+ nth?: number
+
+ countReportsForReporter?: number
+ countReportsForReportee?: number
+}
--- /dev/null
+export * from './abuse-create.model'
+export * from './abuse-reason.model'
+export * from './abuse-state.model'
+export * from './abuse-update.model'
+export * from './abuse-video-is.type'
+export * from './abuse.model'
+export * from './abuse'
export * from './account-block.model'
export * from './server-block.model'
video: VideoInfo
}
- videoAbuse?: {
+ abuse?: {
id: number
- video: VideoInfo
+
+ video?: VideoInfo
+
+ comment?: {
+ threadId: number
+
+ video: {
+ uuid: string
+ }
+ }
+
+ account?: ActorInfo
}
videoBlacklist?: {
MANAGE_SERVER_REDUNDANCY,
- MANAGE_VIDEO_ABUSES,
+ MANAGE_ABUSES,
MANAGE_JOBS,
[UserRole.MODERATOR]: [
UserRight.MANAGE_VIDEO_BLACKLIST,
- UserRight.MANAGE_VIDEO_ABUSES,
+ UserRight.MANAGE_ABUSES,
UserRight.REMOVE_ANY_VIDEO,
UserRight.REMOVE_ANY_VIDEO_CHANNEL,
UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
+++ /dev/null
-export * from './video-abuse-create.model'
-export * from './video-abuse-reason.model'
-export * from './video-abuse-state.model'
-export * from './video-abuse-update.model'
-export * from './video-abuse-video-is.type'
-export * from './video-abuse.model'
+++ /dev/null
-import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
-
-export interface VideoAbuseCreate {
- reason: string
- predefinedReasons?: VideoAbusePredefinedReasonsString[]
- startAt?: number
- endAt?: number
-}
+++ /dev/null
-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
-}
+++ /dev/null
-import { VideoAbuseState } from './video-abuse-state.model'
-
-export interface VideoAbuseUpdate {
- moderationComment?: string
- state?: VideoAbuseState
-}
+++ /dev/null
-export type VideoAbuseVideoIs = 'deleted' | 'blacklisted'
+++ /dev/null
-import { Account } from '../../actors/index'
-import { VideoConstant } from '../video-constant.model'
-import { VideoAbuseState } from './video-abuse-state.model'
-import { VideoChannel } from '../channel/video-channel.model'
-import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
-
-export interface VideoAbuse {
- id: number
- reason: string
- predefinedReasons?: VideoAbusePredefinedReasonsString[]
- reporterAccount: Account
-
- state: VideoConstant<VideoAbuseState>
- moderationComment?: string
-
- video: {
- id: number
- name: string
- uuid: string
- nsfw: boolean
- deleted: boolean
- blacklisted: boolean
- thumbnailPath?: string
- channel?: VideoChannel
- }
-
- createdAt: Date
- updatedAt: Date
-
- startAt: number
- endAt: number
-
- count?: number
- nth?: number
-
- countReportsForReporter?: number
- countReportsForReportee?: number
-}
-export * from './abuse'
export * from './blacklist'
export * from './caption'
export * from './channel'