1
0
Fork 0

Use 3 tables to represent abuses

This commit is contained in:
Chocobozzz 2020-07-01 16:05:30 +02:00 committed by Chocobozzz
parent 72493e44e9
commit d95d155988
103 changed files with 2142 additions and 1167 deletions

View File

@ -91,7 +91,7 @@ export class AdminComponent implements OnInit {
}
hasVideoAbusesRight () {
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES)
}
hasVideoBlocklistRight () {

View File

@ -14,10 +14,10 @@ import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponen
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'
@ -60,8 +60,10 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
ModerationComponent,
VideoBlockListComponent,
VideoAbuseListComponent,
VideoAbuseDetailsComponent,
AbuseListComponent,
AbuseDetailsComponent,
ModerationCommentModalComponent,
InstanceServerBlocklistComponent,
InstanceAccountBlocklistComponent,

View File

@ -0,0 +1,93 @@
<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:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 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:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" 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:&quot;' +abuse.video.channel.ownerAccount.displayName + '&quot;' }" 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:&quot;' +abuse.video.channel.ownerAccount.displayName + '&quot;' }" 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>

View File

@ -1,19 +1,19 @@
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
@ -31,16 +31,16 @@ export class VideoAbuseDetailsComponent {
}
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]
}))

View File

@ -1,5 +1,5 @@
<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"
@ -16,11 +16,11 @@
<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
@ -45,91 +45,91 @@
</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>

View File

@ -1,5 +1,4 @@
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'
@ -7,43 +6,45 @@ import { DomSanitizer } from '@angular/platform-browser'
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,
@ -56,7 +57,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
) {
super()
this.videoAbuseActions = [
this.abuseActions = [
[
{
label: this.i18n('Internal actions'),
@ -64,45 +65,45 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
},
{
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)
@ -111,14 +112,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
},
{
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)
@ -127,20 +128,20 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
},
{
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)
@ -155,8 +156,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
},
{
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(
@ -174,13 +175,13 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
},
{
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 })
)
},
@ -209,11 +210,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
}
getIdentifier () {
return 'VideoAbuseListComponent'
return 'AbuseListComponent'
}
openModerationCommentModal (videoAbuse: VideoAbuse) {
this.moderationCommentModal.openModal(videoAbuse)
openModerationCommentModal (abuse: Abuse) {
this.moderationCommentModal.openModal(abuse)
}
onModerationCommentUpdated () {
@ -240,26 +241,26 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
}
/* 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
})
)
}
@ -268,11 +269,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
($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()
@ -282,8 +283,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
)
}
updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) {
this.videoAbuseService.updateVideoAbuse(videoAbuse, { state })
updateAbuseState (abuse: Abuse, state: AbuseState) {
this.abuseService.updateAbuse(abuse, { state })
.subscribe(
() => this.loadData(),
@ -292,14 +293,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
}
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, {
@ -312,10 +313,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
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)

View File

@ -0,0 +1,3 @@
export * from './abuse-details.component'
export * from './abuse-list.component'
export * from './moderation-comment-modal.component'

View File

@ -1,11 +1,11 @@
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',
@ -16,15 +16,15 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
@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()
@ -32,11 +32,11 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
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 })
@ -54,7 +54,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
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.'))

View File

@ -1,5 +1,5 @@
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'

View File

@ -1,7 +1,7 @@
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'
@ -13,20 +13,25 @@ export const ModerationRoutes: Routes = [
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'
}

View File

@ -1,2 +0,0 @@
export * from './video-abuse-list.component'
export * from './moderation-comment-modal.component'

View File

@ -1,93 +0,0 @@
<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:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" 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:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" 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:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" 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:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" 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>

View File

@ -47,7 +47,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
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,

View File

@ -28,7 +28,7 @@ export class MenuComponent implements OnInit {
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'
@ -126,7 +126,7 @@ export class MenuComponent implements OnInit {
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

View File

@ -4,12 +4,12 @@ import { Injectable } from '@angular/core'
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.'),
@ -18,7 +18,7 @@ export class VideoAbuseValidatorsService {
}
}
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.'),

View File

@ -1,3 +1,4 @@
export * from './abuse-validators.service'
export * from './batch-domains-validators.service'
export * from './custom-config-validators.service'
export * from './form-validator.service'
@ -6,7 +7,6 @@ export * from './instance-validators.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'

View File

@ -11,7 +11,7 @@ import {
LoginValidatorsService,
ResetPasswordValidatorsService,
UserValidatorsService,
VideoAbuseValidatorsService,
AbuseValidatorsService,
VideoAcceptOwnershipValidatorsService,
VideoBlockValidatorsService,
VideoCaptionsValidatorsService,
@ -69,7 +69,7 @@ import { TimestampInputComponent } from './timestamp-input.component'
LoginValidatorsService,
ResetPasswordValidatorsService,
UserValidatorsService,
VideoAbuseValidatorsService,
AbuseValidatorsService,
VideoAcceptOwnershipValidatorsService,
VideoBlockValidatorsService,
VideoCaptionsValidatorsService,

View File

@ -14,7 +14,7 @@ export abstract class Actor implements ActorServer {
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) {

View File

@ -25,9 +25,20 @@ export class UserNotification implements UserNotificationServer {
video: VideoInfo
}
videoAbuse?: {
abuse?: {
id: number
video: VideoInfo
video?: VideoInfo
comment?: {
threadId: number
video: {
uuid: string
}
}
account?: ActorInfo
}
videoBlacklist?: {
@ -55,7 +66,7 @@ export class UserNotification implements UserNotificationServer {
// Additional fields
videoUrl?: string
commentUrl?: any[]
videoAbuseUrl?: string
abuseUrl?: string
videoAutoBlacklistUrl?: string
accountUrl?: string
videoImportIdentifier?: string
@ -78,7 +89,7 @@ export class UserNotification implements UserNotificationServer {
this.comment = hash.comment
if (this.comment) this.setAvatarUrl(this.comment.account)
this.videoAbuse = hash.videoAbuse
this.abuse = hash.abuse
this.videoBlacklist = hash.videoBlacklist
@ -108,8 +119,9 @@ export class UserNotification implements UserNotificationServer {
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:
@ -178,7 +190,7 @@ export class UserNotification implements UserNotificationServer {
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)
}
}

View File

@ -19,7 +19,7 @@
<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>
@ -46,7 +46,7 @@
<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>
@ -65,7 +65,7 @@
<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>
@ -73,7 +73,7 @@
<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>

View File

@ -5,12 +5,12 @@ import { catchError, map } from 'rxjs/operators'
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,
@ -18,13 +18,13 @@ export class VideoAbuseService {
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)
@ -35,9 +35,9 @@ export class VideoAbuseService {
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
}
@ -59,14 +59,14 @@ export class VideoAbuseService {
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' ])
@ -77,8 +77,8 @@ export class VideoAbuseService {
)
}
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(
@ -87,8 +87,8 @@ export class VideoAbuseService {
)
}
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(

View File

@ -1,3 +1,4 @@
export * from './abuse.service'
export * from './account-block.model'
export * from './account-blocklist.component'
export * from './batch-domains-modal.component'
@ -6,7 +7,6 @@ export * from './bulk.service'
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'

View File

@ -8,7 +8,7 @@ import { BlocklistService } from './blocklist.service'
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'
@ -39,7 +39,7 @@ import { VideoReportComponent } from './video-report.component'
providers: [
BlocklistService,
BulkService,
VideoAbuseService,
AbuseService,
VideoBlockService
]
})

View File

@ -3,13 +3,13 @@ import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
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',
@ -22,7 +22,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
@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
@ -30,8 +30,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
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
@ -69,8 +69,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
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,
@ -136,15 +136,18 @@ export class VideoReportComponent extends FormReactive implements OnInit {
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.'))

View File

@ -0,0 +1,168 @@
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 } })
}

View File

@ -3,6 +3,7 @@ import * as express from 'express'
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'
@ -32,6 +33,7 @@ const apiRateLimiter = RateLimit({
apiRouter.use(apiRateLimiter)
apiRouter.use('/server', serverRouter)
apiRouter.use('/abuses', abuseRouter)
apiRouter.use('/bulk', bulkRouter)
apiRouter.use('/oauth-clients', oauthClientsRouter)
apiRouter.use('/config', configRouter)

View File

@ -1,9 +1,10 @@
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,
@ -12,28 +13,21 @@ import {
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,
@ -41,7 +35,7 @@ abuseVideoRouter.get('/abuse',
)
abuseVideoRouter.put('/:videoId/abuse/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
ensureUserHasRight(UserRight.MANAGE_ABUSES),
asyncMiddleware(videoAbuseUpdateValidator),
asyncRetryTransactionMiddleware(updateVideoAbuse)
)
@ -52,7 +46,7 @@ abuseVideoRouter.post('/:videoId/abuse',
)
abuseVideoRouter.delete('/:videoId/abuse/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
ensureUserHasRight(UserRight.MANAGE_ABUSES),
asyncMiddleware(videoAbuseGetValidator),
asyncRetryTransactionMiddleware(deleteVideoAbuse)
)
@ -69,11 +63,12 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
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,
@ -90,74 +85,28 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
}
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 oldBody = req.body as VideoAbuseCreate
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])
req.body = {
accountId: res.locals.videoAll.VideoChannel.accountId,
const abuseToCreate = {
reporterAccountId: reporterAccount.id,
reason: body.reason,
videoId: videoInstance.id,
state: VideoAbuseState.PENDING,
predefinedReasons,
startAt: body.startAt,
endAt: body.endAt
reason: oldBody.reason,
predefinedReasons: oldBody.predefinedReasons,
video: {
id: res.locals.videoAll.id,
startAt: oldBody.startAt,
endAt: oldBody.endAt
}
} as AbuseCreate
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)
}
videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON))
return videoAbuseInstance
})
Notifier.Instance.notifyOnNewVideoAbuse({
videoAbuse: videoAbuseJSON,
videoAbuseInstance,
reporter: reporterAccount.Actor.getIdentifier()
})
logger.info('Abuse report for video "%s" created.', videoInstance.name)
return res.json({ videoAbuse: videoAbuseJSON }).end()
return reportAbuse(req, res)
}

View File

@ -1,15 +1,15 @@
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 { CustomConfig } from '../../shared/models/server/custom-config.model'
import { CONFIG } from '../initializers/config'
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 { jsonLoggerFormat, labelFormatter } from './logger'
function getAuditIdFromRes (res: express.Response) {
return res.locals.oauth.token.User.username
@ -212,18 +212,15 @@ class VideoChannelAuditView extends EntityAuditView {
}
}
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)
}
}
@ -274,6 +271,6 @@ export {
CommentAuditView,
UserAuditView,
VideoAuditView,
VideoAbuseAuditView,
AbuseAuditView,
CustomConfigAuditView
}

View File

@ -0,0 +1,54 @@
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
}

View File

@ -1,9 +1,9 @@
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)
}

View File

@ -1,56 +0,0 @@
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
}

View File

@ -1,19 +1,20 @@
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()
@ -21,12 +22,17 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
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
}

View File

@ -1,5 +1,5 @@
export * from './abuses'
export * from './accounts'
export * from './video-abuses'
export * from './video-blacklists'
export * from './video-captions'
export * from './video-channels'

View File

@ -1,9 +1,17 @@
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'
@ -51,7 +59,6 @@ const SORTABLE_COLUMNS = {
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' ],
@ -66,6 +73,8 @@ const SORTABLE_COLUMNS = {
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' ],
@ -193,7 +202,7 @@ const CONSTRAINTS_FIELDS = {
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
},
@ -378,10 +387,10 @@ const VIDEO_IMPORT_STATES = {
[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 = {
@ -778,7 +787,7 @@ export {
VIDEO_RATE_TYPES,
VIDEO_TRANSCODING_FPS,
FFMPEG_NICE,
VIDEO_ABUSE_STATES,
ABUSE_STATES,
VIDEO_CHANNELS,
LRU_CACHE,
JOB_REQUEST_TIMEOUT,

View File

@ -1,44 +1,45 @@
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
@ -86,6 +87,8 @@ async function initDatabaseModels (silent: boolean) {
TagModel,
AccountVideoRateModel,
UserModel,
AbuseModel,
VideoCommentAbuseModel,
VideoAbuseModel,
VideoModel,
VideoChangeOwnershipModel,

View File

@ -1,5 +1,5 @@
import * as Sequelize from 'sequelize'
import { VideoAbuseState } from '../../../shared/models/videos'
import { AbuseState } from '../../../shared/models'
async function up (utils: {
transaction: Sequelize.Transaction
@ -16,7 +16,7 @@ async function up (utils: {
}
{
const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING
const query = 'UPDATE "videoAbuse" SET "state" = ' + AbuseState.PENDING
await utils.sequelize.query(query)
}

View File

@ -1,24 +1,19 @@
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)
}
// ---------------------------------------------------------------------------
@ -29,55 +24,79 @@ export {
// ---------------------------------------------------------------------------
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 uri = 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
logger.debug('Reporting remote abuse for object %s.', uri)
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
const videoAbuseData = {
reporterAccountId: account.id,
reason: flag.content,
videoId: video.id,
state: VideoAbuseState.PENDING,
predefinedReasons,
startAt,
endAt
await sequelizeTypescript.transaction(async t => {
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 videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
videoAbuseInstance.Video = video
videoAbuseInstance.Account = reporterAccount
const baseAbuse = {
reporterAccountId: reporterAccount.id,
reason: flag.content,
state: AbuseState.PENDING,
predefinedReasons
}
logger.info('Remote abuse for video uuid %s created', flag.object)
if (video) {
return createVideoAbuse({
baseAbuse,
startAt,
endAt,
reporterAccount,
transaction: t,
videoInstance: video
})
}
return videoAbuseInstance
})
if (videoComment) {
return createVideoCommentAbuse({
baseAbuse,
reporterAccount,
transaction: t,
commentInstance: videoComment
})
}
const videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
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 })
}
}
}

View File

@ -1,32 +1,31 @@
import { getVideoAbuseActivityPubUrl } from '../url'
import { unicastTo } from './utils'
import { logger } from '../../../helpers/logger'
import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
import { audiencify, getAudience } from '../audience'
import { Transaction } from 'sequelize'
import { MActor, MVideoFullLight } from '../../../types/models'
import { MVideoAbuseVideo } from '../../../types/models/video'
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 { 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)
@ -35,5 +34,5 @@ function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbus
// ---------------------------------------------------------------------------
export {
sendVideoAbuse
sendAbuse
}

View File

@ -5,10 +5,10 @@ import {
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'
@ -48,8 +48,8 @@ function getAccountActivityPubUrl (accountName: string) {
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) {
@ -118,7 +118,7 @@ export {
getVideoCacheStreamingPlaylistActivityPubUrl,
getVideoChannelActivityPubUrl,
getAccountActivityPubUrl,
getVideoAbuseActivityPubUrl,
getAbuseActivityPubUrl,
getActorFollowActivityPubUrl,
getActorFollowAcceptActivityPubUrl,
getVideoAnnounceActivityPubUrl,

View File

@ -1,26 +1,20 @@
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 {
@ -288,28 +282,70 @@ 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
}
}
}

View File

@ -0,0 +1,14 @@
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;")

View File

@ -1,3 +1,7 @@
mixin channel(channel)
- var handle = `${channel.name}@${channel.host}`
| #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
| #[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}]

View File

@ -6,13 +6,13 @@ block title
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;")

View File

@ -0,0 +1,15 @@
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;")

View File

@ -1,15 +1,33 @@
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 { ActorModel } from '../models/activitypub/actor'
import { VideoModel } from '../models/video/video'
import { VideoCommentModel } from '../models/video/video-comment'
import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
import { VideoCreate, VideoImportCreate } from '../../shared/models/videos'
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 { sendAbuse } from './activitypub/send/send-flag'
import { Notifier } from './notifier'
export type AcceptResult = {
accepted: boolean
@ -73,6 +91,89 @@ function isPostImportVideoAccepted (object: {
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,
@ -80,5 +181,48 @@ export {
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
}

View File

@ -8,23 +8,18 @@ import {
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'
@ -78,9 +73,9 @@ class Notifier {
.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 {
@ -354,33 +349,37 @@ class Notifier {
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 })

View File

@ -0,0 +1,253 @@
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
}

View File

@ -1,3 +1,4 @@
export * from './abuse'
export * from './account'
export * from './blocklist'
export * from './oembed'

View File

@ -5,7 +5,7 @@ import { checkSort, createSortableColumns } from './utils'
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)
@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM
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)
@ -52,7 +52,7 @@ const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COL
export {
usersSortValidator,
videoAbusesSortValidator,
abusesSortValidator,
videoChannelsSortValidator,
videoImportsSortValidator,
videosSearchSortValidator,

View File

@ -1,4 +1,3 @@
export * from './video-abuses'
export * from './video-blacklist'
export * from './video-captions'
export * from './video-channels'

View File

@ -1,135 +0,0 @@
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
}

View File

@ -1,5 +1,6 @@
import * as Bluebird from 'bluebird'
import { literal, Op } from 'sequelize'
import { invert } from 'lodash'
import { literal, Op, WhereOptions } from 'sequelize'
import {
AllowNull,
BelongsTo,
@ -8,36 +9,35 @@ import {
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 { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
import {
VideoAbuseState,
VideoDetails,
VideoAbusePredefinedReasons,
VideoAbusePredefinedReasonsString,
videoAbusePredefinedReasonsMap
} from '../../../shared'
import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
import { VideoAbuse } from '../../../shared/models/videos'
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'
@ -49,20 +49,26 @@ export enum ScopeNames {
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 ]) + ')')
@ -70,33 +76,36 @@ export enum ScopeNames {
}
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$')
]
})
}
@ -106,7 +115,7 @@ export enum ScopeNames {
if (options.videoIs === 'deleted') {
Object.assign(where, {
deletedVideo: {
'$VideoAbuse.deletedVideo$': {
[Op.not]: null
}
})
@ -120,8 +129,6 @@ export enum ScopeNames {
})
}
const onlyBlacklisted = options.videoIs === 'blacklisted'
return {
attributes: {
include: [
@ -131,7 +138,7 @@ export enum ScopeNames {
'(' +
'SELECT count(*) ' +
'FROM "videoAbuse" ' +
'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
'WHERE "videoId" = "VideoAbuse"."videoId" ' +
')'
),
'countReportsForVideo'
@ -146,7 +153,7 @@ export enum ScopeNames {
'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'
@ -159,7 +166,7 @@ export enum ScopeNames {
'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'
@ -169,7 +176,7 @@ export enum ScopeNames {
'(' +
'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'
@ -182,8 +189,8 @@ export enum ScopeNames {
'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'
@ -193,9 +200,9 @@ export enum ScopeNames {
'(' +
'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'
@ -204,32 +211,47 @@ export enum ScopeNames {
},
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
}
]
}
@ -239,55 +261,40 @@ export enum ScopeNames {
}
}))
@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
@ -301,36 +308,65 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
@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
static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
const videoAttributes = {}
if (videoId) videoAttributes['videoId'] = videoId
if (uuid) videoAttributes['deletedVideo'] = { uuid }
@HasOne(() => VideoCommentAbuseModel, {
foreignKey: {
name: 'abuseId',
allowNull: false
},
onDelete: 'cascade'
})
VideoCommentAbuse: VideoCommentAbuseModel
@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: {
@ -338,13 +374,15 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
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
@ -364,24 +402,26 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
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,
@ -394,7 +434,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
userAccountId
}
return VideoAbuseModel
return AbuseModel
.scope([
{ method: [ ScopeNames.FOR_API, filters ] }
])
@ -404,8 +444,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
})
}
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
@ -413,51 +453,70 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
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 startAt = this.startAt
const endAt = this.endAt
const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
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
@ -468,12 +527,12 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
}
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)
}
}

View File

@ -0,0 +1,63 @@
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
}

View File

@ -0,0 +1,53 @@
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
}

View File

@ -1,12 +1,12 @@
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'

View File

@ -388,6 +388,10 @@ export class AccountModel extends Model<AccountModel> {
.findAll(query)
}
getClientUrl () {
return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
}
toFormattedJSON (this: MAccountFormattable): Account {
const actor = this.Actor.toFormattedJSON()
const account = {

View File

@ -1,22 +1,24 @@
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'
@ -87,9 +89,41 @@ function buildAccountInclude (required: boolean, withActor = false) {
{
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() ]
}
]
},
{
@ -179,9 +213,9 @@ function buildAccountInclude (required: boolean, withActor = false) {
}
},
{
fields: [ 'videoAbuseId' ],
fields: [ 'abuseId' ],
where: {
videoAbuseId: {
abuseId: {
[Op.ne]: null
}
}
@ -276,17 +310,17 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
})
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
@ -397,10 +431,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
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,
@ -439,7 +470,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
video,
videoImport,
comment,
videoAbuse,
abuse,
videoBlacklist,
account,
actorFollow,
@ -456,6 +487,27 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
}
}
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

View File

@ -19,7 +19,7 @@ import {
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,
@ -169,7 +169,7 @@ enum ScopeNames {
`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" ' +

View File

@ -1,11 +1,11 @@
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
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 { 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 { getSort, searchAttribute } from '../utils'
import { ServerModel } from './server'
enum ScopeNames {
WITH_ACCOUNT = 'WITH_ACCOUNT',

View File

@ -1,4 +1,5 @@
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'
@ -23,10 +24,18 @@ import {
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'
@ -43,6 +52,7 @@ import {
} 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,
@ -59,40 +69,6 @@ import {
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,
@ -118,15 +94,39 @@ import {
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',

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import 'mocha'
import { AbuseState, VideoAbuseCreate } from '@shared/models'
import {
cleanupTests,
createUser,
@ -20,7 +20,8 @@ import {
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
@ -136,7 +137,7 @@ describe('Test video abuses API validators', function () {
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 () {
@ -190,7 +191,7 @@ describe('Test video abuses API validators', 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)
})
})

View File

@ -2,7 +2,7 @@
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,
@ -937,7 +937,7 @@ describe('Test users', function () {
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)

View File

@ -1,21 +1,21 @@
/* 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'
@ -29,9 +29,11 @@ import {
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)
@ -95,7 +97,7 @@ describe('Test video abuses', function () {
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)
@ -128,23 +130,23 @@ describe('Test video abuses', function () {
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
@ -157,25 +159,25 @@ describe('Test video abuses', function () {
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')
})
@ -243,7 +245,7 @@ describe('Test video abuses', function () {
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
@ -277,7 +279,7 @@ describe('Test video abuses', function () {
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")
@ -295,7 +297,7 @@ describe('Test video abuses', function () {
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,
@ -304,16 +306,16 @@ describe('Test video abuses', function () {
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")
}
})
@ -348,7 +350,7 @@ describe('Test video abuses', function () {
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)
@ -365,14 +367,14 @@ describe('Test video abuses', function () {
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)

View File

@ -1,4 +1,5 @@
export * from './account'
export * from './moderation'
export * from './oauth'
export * from './server'
export * from './user'

View File

@ -0,0 +1,97 @@
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>

View File

@ -0,0 +1 @@
export * from './abuse'

View File

@ -1,16 +1,18 @@
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 { ActorModel } from '../../../models/activitypub/actor'
import { ServerModel } from '../../../models/server/server'
import { AvatarModel } from '../../../models/avatar/avatar'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { AbuseModel } from '../../../models/abuse/abuse'
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 { UserNotificationModel } from '../../../models/account/user-notification'
import { ActorModel } from '../../../models/activitypub/actor'
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 { VideoCommentModel } from '../../../models/video/video-comment'
import { VideoImportModel } from '../../../models/video/video-import'
type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M>
@ -47,6 +49,18 @@ export module UserNotificationIncludes {
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>
@ -76,7 +90,7 @@ export module UserNotificationIncludes {
// ############################################################################
export type MUserNotification =
Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' |
Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
'VideoImport' | 'Account' | 'ActorFollow'>
// ############################################################################
@ -85,7 +99,7 @@ export type UserNotificationModelForApi =
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> &

View File

@ -2,7 +2,6 @@ export * from './schedule-video-update'
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'

View File

@ -1,35 +0,0 @@
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'>>

View File

@ -1,5 +1,6 @@
import { RegisterServerAuthExternalOptions } from '@server/types'
import {
MAbuse,
MAccountBlocklist,
MActorUrl,
MStreamingPlaylist,
@ -26,7 +27,6 @@ import {
MComment,
MCommentOwnerVideoReply,
MUserDefault,
MVideoAbuse,
MVideoBlacklist,
MVideoCaptionVideo,
MVideoFullLight,
@ -77,7 +77,7 @@ declare module 'express' {
videoCaption?: MVideoCaptionVideo
videoAbuse?: MVideoAbuse
abuse?: MAbuse
videoStreamingPlaylist?: MStreamingPlaylist

View File

@ -17,6 +17,7 @@ export * from './videos/services'
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'

View File

@ -0,0 +1,112 @@
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
}

View File

@ -443,11 +443,11 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
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
})
}
}

View File

@ -1,15 +1,15 @@
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
@ -28,10 +28,10 @@ function getVideoAbusesList (options: {
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
@ -79,7 +79,7 @@ function updateVideoAbuse (
token: string,
videoId: string | number,
videoAbuseId: number,
body: VideoAbuseUpdate,
body: AbuseUpdate,
statusCodeExpected = 204
) {
const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId

View File

@ -1,12 +1,12 @@
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 |
@ -53,7 +53,7 @@ export interface BaseActivity {
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 {

View File

@ -1,10 +1,12 @@
import { ActivityFlagReasonObject } from './common-objects'
export interface VideoAbuseObject {
export interface AbuseObject {
type: 'Flag'
content: string
object: string | string[]
tag?: ActivityFlagReasonObject[]
startAt?: number
endAt?: number
}

View File

@ -1,4 +1,4 @@
import { VideoAbusePredefinedReasonsString } from '@shared/models/videos'
import { AbusePredefinedReasonsString } from '@shared/models'
export interface ActivityIdentifierObject {
identifier: string
@ -85,7 +85,7 @@ export interface ActivityMentionObject {
export interface ActivityFlagReasonObject {
type: 'Hashtag'
name: VideoAbusePredefinedReasonsString
name: AbusePredefinedReasonsString
}
export type ActivityTagObject =

View File

@ -1,6 +1,6 @@
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'

View File

@ -1,7 +1,7 @@
export * from './activitypub'
export * from './actors'
export * from './avatars'
export * from './blocklist'
export * from './moderation'
export * from './bulk'
export * from './redundancy'
export * from './users'
@ -14,4 +14,3 @@ export * from './search'
export * from './server'
export * from './oauth-client-local.model'
export * from './result-list.model'
export * from './server/server-config.model'

View File

@ -0,0 +1,26 @@
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
}

View File

@ -0,0 +1 @@
export type AbuseFilter = 'video' | 'comment'

View File

@ -0,0 +1,33 @@
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
}

View File

@ -1,4 +1,4 @@
export enum VideoAbuseState {
export enum AbuseState {
PENDING = 1,
REJECTED = 2,
ACCEPTED = 3

View File

@ -0,0 +1,7 @@
import { AbuseState } from './abuse-state.model'
export interface AbuseUpdate {
moderationComment?: string
state?: AbuseState
}

View File

@ -0,0 +1 @@
export type AbuseVideoIs = 'deleted' | 'blacklisted'

View File

@ -0,0 +1,53 @@
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
}

View File

@ -0,0 +1,6 @@
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'

View File

@ -1,2 +1,3 @@
export * from './abuse'
export * from './account-block.model'
export * from './server-block.model'

View File

@ -64,9 +64,20 @@ export interface UserNotification {
video: VideoInfo
}
videoAbuse?: {
abuse?: {
id: number
video: VideoInfo
video?: VideoInfo
comment?: {
threadId: number
video: {
uuid: string
}
}
account?: ActorInfo
}
videoBlacklist?: {

View File

@ -11,7 +11,7 @@ export enum UserRight {
MANAGE_SERVER_REDUNDANCY,
MANAGE_VIDEO_ABUSES,
MANAGE_ABUSES,
MANAGE_JOBS,

View File

@ -20,7 +20,7 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
[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,

View File

@ -1,6 +0,0 @@
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'

View File

@ -1,8 +0,0 @@
import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
export interface VideoAbuseCreate {
reason: string
predefinedReasons?: VideoAbusePredefinedReasonsString[]
startAt?: number
endAt?: number
}

View File

@ -1,33 +0,0 @@
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
}

View File

@ -1,6 +0,0 @@
import { VideoAbuseState } from './video-abuse-state.model'
export interface VideoAbuseUpdate {
moderationComment?: string
state?: VideoAbuseState
}

Some files were not shown because too many files have changed in this diff Show More