Use 3 tables to represent abuses
This commit is contained in:
parent
72493e44e9
commit
d95d155988
|
@ -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 () {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:"' + abuse.reporterAccount.displayName + '"' }" class="chip">
|
||||
<img
|
||||
class="avatar"
|
||||
[src]="abuse.reporterAccount.avatar?.path"
|
||||
(error)="switchToDefaultAvatar($event)"
|
||||
alt="Avatar"
|
||||
>
|
||||
<div>
|
||||
<span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + abuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
|
||||
{abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Reportee</span>
|
||||
<span class="col-9 moderation-expanded-text">
|
||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.video.channel.ownerAccount.displayName + '"' }" class="chip">
|
||||
<img
|
||||
class="avatar"
|
||||
[src]="abuse.video.channel.ownerAccount?.avatar?.path"
|
||||
(error)="switchToDefaultAvatar($event)"
|
||||
alt="Avatar"
|
||||
>
|
||||
<div>
|
||||
<span class="text-muted">{{ abuse.video.channel.ownerAccount ? abuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +abuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
|
||||
{abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex" *ngIf="abuse.updatedAt">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Updated</span>
|
||||
<time class="col-9 moderation-expanded-text video-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
|
||||
</div>
|
||||
|
||||
<!-- report text -->
|
||||
<div class="mt-3 d-flex">
|
||||
<span class="col-3 moderation-expanded-label">
|
||||
<ng-container i18n>Report</ng-container>
|
||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id }" class="ml-1 text-muted">#{{ abuse.id }}</a>
|
||||
</span>
|
||||
<span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
|
||||
<span class="col-3"></span>
|
||||
<span class="col-9">
|
||||
<a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()">
|
||||
<div>{{ reason.label }}</div>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="abuse.startAt" class="mt-2 d-flex">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Reported part</span>
|
||||
<span class="col-9">
|
||||
{{ startAt }}<ng-container *ngIf="abuse.endAt"> - {{ endAt }}</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Note</span>
|
||||
<span class="col-9 moderation-expanded-text" [innerHTML]="abuse.moderationCommentHtml"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- report right part (video details) -->
|
||||
<div class="col-4">
|
||||
<div class="screenratio">
|
||||
<div *ngIf="abuse.video.deleted || abuse.video.blacklisted">
|
||||
<span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
|
||||
<span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
|
||||
</div>
|
||||
<div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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]
|
||||
}))
|
|
@ -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>
|
|
@ -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)
|
|
@ -0,0 +1,3 @@
|
|||
export * from './abuse-details.component'
|
||||
export * from './abuse-list.component'
|
||||
export * from './moderation-comment-modal.component'
|
|
@ -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.'))
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
export * from './video-abuse-list.component'
|
||||
export * from './moderation-comment-modal.component'
|
|
@ -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:"' + videoAbuse.reporterAccount.displayName + '"' }" class="chip">
|
||||
<img
|
||||
class="avatar"
|
||||
[src]="videoAbuse.reporterAccount.avatar?.path"
|
||||
(error)="switchToDefaultAvatar($event)"
|
||||
alt="Avatar"
|
||||
>
|
||||
<div>
|
||||
<span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:"' + videoAbuse.reporterAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
|
||||
{videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Reportee</span>
|
||||
<span class="col-9 moderation-expanded-text">
|
||||
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +videoAbuse.video.channel.ownerAccount.displayName + '"' }" class="chip">
|
||||
<img
|
||||
class="avatar"
|
||||
[src]="videoAbuse.video.channel.ownerAccount?.avatar?.path"
|
||||
(error)="switchToDefaultAvatar($event)"
|
||||
alt="Avatar"
|
||||
>
|
||||
<div>
|
||||
<span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? videoAbuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
|
||||
</div>
|
||||
</a>
|
||||
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:"' +videoAbuse.video.channel.ownerAccount.displayName + '"' }" class="ml-auto text-muted video-details-links" i18n>
|
||||
{videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="d-flex" *ngIf="videoAbuse.updatedAt">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Updated</span>
|
||||
<time class="col-9 moderation-expanded-text video-details-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time>
|
||||
</div>
|
||||
|
||||
<!-- report text -->
|
||||
<div class="mt-3 d-flex">
|
||||
<span class="col-3 moderation-expanded-label">
|
||||
<ng-container i18n>Report</ng-container>
|
||||
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': '#' + videoAbuse.id }" class="ml-1 text-muted">#{{ videoAbuse.id }}</a>
|
||||
</span>
|
||||
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
|
||||
<span class="col-3"></span>
|
||||
<span class="col-9">
|
||||
<a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()">
|
||||
<div>{{ reason.label }}</div>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div *ngIf="videoAbuse.startAt" class="mt-2 d-flex">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Reported part</span>
|
||||
<span class="col-9">
|
||||
{{ startAt }}<ng-container *ngIf="videoAbuse.endAt"> - {{ endAt }}</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
|
||||
<span class="col-3 moderation-expanded-label" i18n>Note</span>
|
||||
<span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- report right part (video details) -->
|
||||
<div class="col-4">
|
||||
<div class="screenratio">
|
||||
<div *ngIf="videoAbuse.video.deleted || videoAbuse.video.blacklisted">
|
||||
<span i18n *ngIf="videoAbuse.video.deleted">The video was deleted</span>
|
||||
<span i18n *ngIf="!videoAbuse.video.deleted">The video was blocked</span>
|
||||
</div>
|
||||
<div *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" [innerHTML]="videoAbuse.embedHtml"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.'),
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
]
|
||||
})
|
||||
|
|
|
@ -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.'))
|
||||
|
|
|
@ -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 } })
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;")
|
|
@ -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}]
|
||||
|
|
|
@ -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;")
|
||||
|
|
|
@ -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;")
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './abuse'
|
||||
export * from './account'
|
||||
export * from './blocklist'
|
||||
export * from './oembed'
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export * from './video-abuses'
|
||||
export * from './video-blacklist'
|
||||
export * from './video-captions'
|
||||
export * from './video-channels'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" ' +
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export * from './account'
|
||||
export * from './moderation'
|
||||
export * from './oauth'
|
||||
export * from './server'
|
||||
export * from './user'
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
export * from './abuse'
|
|
@ -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> &
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'>>
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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 =
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export type AbuseFilter = 'video' | 'comment'
|
|
@ -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
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export enum VideoAbuseState {
|
||||
export enum AbuseState {
|
||||
PENDING = 1,
|
||||
REJECTED = 2,
|
||||
ACCEPTED = 3
|
|
@ -0,0 +1,7 @@
|
|||
import { AbuseState } from './abuse-state.model'
|
||||
|
||||
export interface AbuseUpdate {
|
||||
moderationComment?: string
|
||||
|
||||
state?: AbuseState
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export type AbuseVideoIs = 'deleted' | 'blacklisted'
|
|
@ -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
|
||||
}
|
|
@ -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'
|
|
@ -1,2 +1,3 @@
|
|||
export * from './abuse'
|
||||
export * from './account-block.model'
|
||||
export * from './server-block.model'
|
|
@ -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?: {
|
||||
|
|
|
@ -11,7 +11,7 @@ export enum UserRight {
|
|||
|
||||
MANAGE_SERVER_REDUNDANCY,
|
||||
|
||||
MANAGE_VIDEO_ABUSES,
|
||||
MANAGE_ABUSES,
|
||||
|
||||
MANAGE_JOBS,
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
|
@ -1,8 +0,0 @@
|
|||
import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
|
||||
|
||||
export interface VideoAbuseCreate {
|
||||
reason: string
|
||||
predefinedReasons?: VideoAbusePredefinedReasonsString[]
|
||||
startAt?: number
|
||||
endAt?: number
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue