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 () {
|
hasVideoAbusesRight () {
|
||||||
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
|
return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES)
|
||||||
}
|
}
|
||||||
|
|
||||||
hasVideoBlocklistRight () {
|
hasVideoBlocklistRight () {
|
||||||
|
|
|
@ -14,10 +14,10 @@ import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponen
|
||||||
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
import { FollowingListComponent } from './follows/following-list/following-list.component'
|
||||||
import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
|
import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
|
||||||
import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.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 { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
|
||||||
import { ModerationComponent } from './moderation/moderation.component'
|
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 { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
|
||||||
import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
|
import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
|
||||||
import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
|
import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
|
||||||
|
@ -60,8 +60,10 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
||||||
|
|
||||||
ModerationComponent,
|
ModerationComponent,
|
||||||
VideoBlockListComponent,
|
VideoBlockListComponent,
|
||||||
VideoAbuseListComponent,
|
|
||||||
VideoAbuseDetailsComponent,
|
AbuseListComponent,
|
||||||
|
AbuseDetailsComponent,
|
||||||
|
|
||||||
ModerationCommentModalComponent,
|
ModerationCommentModalComponent,
|
||||||
InstanceServerBlocklistComponent,
|
InstanceServerBlocklistComponent,
|
||||||
InstanceAccountBlocklistComponent,
|
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 { Component, Input } from '@angular/core'
|
||||||
import { Actor } from '@app/shared/shared-main'
|
import { Actor } from '@app/shared/shared-main'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model'
|
import { AbusePredefinedReasonsString } from '@shared/models'
|
||||||
import { ProcessedVideoAbuse } from './video-abuse-list.component'
|
import { ProcessedAbuse } from './abuse-list.component'
|
||||||
import { durationToString } from '@app/helpers'
|
import { durationToString } from '@app/helpers'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-abuse-details',
|
selector: 'my-abuse-details',
|
||||||
templateUrl: './video-abuse-details.component.html',
|
templateUrl: './abuse-details.component.html',
|
||||||
styleUrls: [ '../moderation.component.scss' ]
|
styleUrls: [ '../moderation.component.scss' ]
|
||||||
})
|
})
|
||||||
export class VideoAbuseDetailsComponent {
|
export class AbuseDetailsComponent {
|
||||||
@Input() videoAbuse: ProcessedVideoAbuse
|
@Input() abuse: ProcessedAbuse
|
||||||
|
|
||||||
private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string }
|
private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
|
@ -31,16 +31,16 @@ export class VideoAbuseDetailsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
get startAt () {
|
get startAt () {
|
||||||
return durationToString(this.videoAbuse.startAt)
|
return durationToString(this.abuse.startAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
get endAt () {
|
get endAt () {
|
||||||
return durationToString(this.videoAbuse.endAt)
|
return durationToString(this.abuse.endAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
getPredefinedReasons () {
|
getPredefinedReasons () {
|
||||||
if (!this.videoAbuse.predefinedReasons) return []
|
if (!this.abuse.predefinedReasons) return []
|
||||||
return this.videoAbuse.predefinedReasons.map(r => ({
|
return this.abuse.predefinedReasons.map(r => ({
|
||||||
id: r,
|
id: r,
|
||||||
label: this.predefinedReasonsTranslations[r]
|
label: this.predefinedReasonsTranslations[r]
|
||||||
}))
|
}))
|
|
@ -1,5 +1,5 @@
|
||||||
<p-table
|
<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"
|
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
|
||||||
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
|
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
|
||||||
|
@ -16,11 +16,11 @@
|
||||||
|
|
||||||
<div role="menu" ngbDropdownMenu>
|
<div role="menu" ngbDropdownMenu>
|
||||||
<h6 class="dropdown-header" i18n>Advanced report filters</h6>
|
<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/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/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/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/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': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
@ -45,91 +45,91 @@
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse>
|
<ng-template pTemplate="body" let-expanded="expanded" let-abuse>
|
||||||
<tr>
|
<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">
|
<span class="expander">
|
||||||
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<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">
|
<div class="chip two-lines">
|
||||||
<img
|
<img
|
||||||
class="avatar"
|
class="avatar"
|
||||||
[src]="videoAbuse.reporterAccount.avatar?.path"
|
[src]="abuse.reporterAccount.avatar?.path"
|
||||||
(error)="switchToDefaultAvatar($event)"
|
(error)="switchToDefaultAvatar($event)"
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{{ videoAbuse.reporterAccount.displayName }}
|
{{ abuse.reporterAccount.displayName }}
|
||||||
<span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
|
<span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td *ngIf="!videoAbuse.video.deleted">
|
<td *ngIf="!abuse.video.deleted">
|
||||||
<a [href]="getVideoUrl(videoAbuse)" class="video-table-video-link" [title]="videoAbuse.video.name" target="_blank" rel="noopener noreferrer">
|
<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">
|
||||||
<div class="video-table-video-image">
|
<div class="video-table-video-image">
|
||||||
<img [src]="videoAbuse.video.thumbnailPath">
|
<img [src]="abuse.video.thumbnailPath">
|
||||||
<span
|
<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."
|
i18n-title title="This video has been reported multiple times."
|
||||||
>
|
>
|
||||||
{{ videoAbuse.nth }}/{{ videoAbuse.count }}
|
{{ abuse.nth }}/{{ abuse.count }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-table-video-text">
|
<div class="video-table-video-text">
|
||||||
<div>
|
<div>
|
||||||
<span *ngIf="!videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
|
<span *ngIf="!abuse.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>
|
<span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
|
||||||
{{ videoAbuse.video.name }}
|
{{ abuse.video.name }}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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" i18n-title title="Video was deleted">
|
||||||
<div class="video-table-video-image">
|
<div class="video-table-video-image">
|
||||||
<span i18n>Deleted</span>
|
<span i18n>Deleted</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="video-table-video-text">
|
<div class="video-table-video-text">
|
||||||
<div>
|
<div>
|
||||||
{{ videoAbuse.video.name }}
|
{{ abuse.video.name }}
|
||||||
<span class="glyphicon glyphicon-trash"></span>
|
<span class="glyphicon glyphicon-trash"></span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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">
|
<td class="c-hand video-abuse-states" [pRowToggler]="abuse">
|
||||||
<span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
|
<span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
|
||||||
<span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
|
<span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
|
||||||
<span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
<span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<my-action-dropdown
|
<my-action-dropdown
|
||||||
[ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
|
[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>
|
></my-action-dropdown>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template pTemplate="rowexpansion" let-videoAbuse>
|
<ng-template pTemplate="rowexpansion" let-abuse>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="expand-cell" colspan="6">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
|
@ -1,5 +1,4 @@
|
||||||
import { SortMeta } from 'primeng/api'
|
import { SortMeta } from 'primeng/api'
|
||||||
import { filter } from 'rxjs/operators'
|
|
||||||
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
|
import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
|
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 { ActivatedRoute, Params, Router } from '@angular/router'
|
||||||
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
|
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
|
||||||
import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
|
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 { 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'
|
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||||
|
|
||||||
export type ProcessedVideoAbuse = VideoAbuse & {
|
export type ProcessedAbuse = Abuse & {
|
||||||
moderationCommentHtml?: string,
|
moderationCommentHtml?: string,
|
||||||
reasonHtml?: string
|
reasonHtml?: string
|
||||||
embedHtml?: string
|
embedHtml?: string
|
||||||
updatedAt?: Date
|
updatedAt?: Date
|
||||||
|
|
||||||
// override bare server-side definitions with rich client-side definitions
|
// override bare server-side definitions with rich client-side definitions
|
||||||
reporterAccount: Account
|
reporterAccount: Account
|
||||||
video: VideoAbuse['video'] & {
|
|
||||||
channel: VideoAbuse['video']['channel'] & {
|
video: Abuse['video'] & {
|
||||||
|
channel: Abuse['video']['channel'] & {
|
||||||
ownerAccount: Account
|
ownerAccount: Account
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-abuse-list',
|
selector: 'my-abuse-list',
|
||||||
templateUrl: './video-abuse-list.component.html',
|
templateUrl: './abuse-list.component.html',
|
||||||
styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ]
|
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
|
@ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
|
||||||
|
|
||||||
videoAbuses: ProcessedVideoAbuse[] = []
|
abuses: ProcessedAbuse[] = []
|
||||||
totalRecords = 0
|
totalRecords = 0
|
||||||
sort: SortMeta = { field: 'createdAt', order: 1 }
|
sort: SortMeta = { field: 'createdAt', order: 1 }
|
||||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||||
|
|
||||||
videoAbuseActions: DropdownAction<VideoAbuse>[][] = []
|
abuseActions: DropdownAction<Abuse>[][] = []
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private videoAbuseService: VideoAbuseService,
|
private abuseService: AbuseService,
|
||||||
private blocklistService: BlocklistService,
|
private blocklistService: BlocklistService,
|
||||||
private videoService: VideoService,
|
private videoService: VideoService,
|
||||||
private videoBlocklistService: VideoBlockService,
|
private videoBlocklistService: VideoBlockService,
|
||||||
|
@ -56,7 +57,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.videoAbuseActions = [
|
this.abuseActions = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: this.i18n('Internal actions'),
|
label: this.i18n('Internal actions'),
|
||||||
|
@ -64,45 +65,45 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Delete report'),
|
label: this.i18n('Delete report'),
|
||||||
handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
|
handler: abuse => this.removeAbuse(abuse)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Add note'),
|
label: this.i18n('Add note'),
|
||||||
handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
|
handler: abuse => this.openModerationCommentModal(abuse),
|
||||||
isDisplayed: videoAbuse => !videoAbuse.moderationComment
|
isDisplayed: abuse => !abuse.moderationComment
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Update note'),
|
label: this.i18n('Update note'),
|
||||||
handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
|
handler: abuse => this.openModerationCommentModal(abuse),
|
||||||
isDisplayed: videoAbuse => !!videoAbuse.moderationComment
|
isDisplayed: abuse => !!abuse.moderationComment
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Mark as accepted'),
|
label: this.i18n('Mark as accepted'),
|
||||||
handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
|
handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
|
||||||
isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
|
isDisplayed: abuse => !this.isAbuseAccepted(abuse)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Mark as rejected'),
|
label: this.i18n('Mark as rejected'),
|
||||||
handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
|
handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
|
||||||
isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
|
isDisplayed: abuse => !this.isAbuseRejected(abuse)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
label: this.i18n('Actions for the video'),
|
label: this.i18n('Actions for the video'),
|
||||||
isHeader: true,
|
isHeader: true,
|
||||||
isDisplayed: videoAbuse => !videoAbuse.video.deleted
|
isDisplayed: abuse => !abuse.video.deleted
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Block video'),
|
label: this.i18n('Block video'),
|
||||||
isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted,
|
isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted,
|
||||||
handler: videoAbuse => {
|
handler: abuse => {
|
||||||
this.videoBlocklistService.blockVideo(videoAbuse.video.id, undefined, true)
|
this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Video blocked.'))
|
this.notifier.success(this.i18n('Video blocked.'))
|
||||||
|
|
||||||
this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
|
this.updateAbuseState(abuse, AbuseState.ACCEPTED)
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
|
@ -111,14 +112,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Unblock video'),
|
label: this.i18n('Unblock video'),
|
||||||
isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted,
|
isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted,
|
||||||
handler: videoAbuse => {
|
handler: abuse => {
|
||||||
this.videoBlocklistService.unblockVideo(videoAbuse.video.id)
|
this.videoBlocklistService.unblockVideo(abuse.video.id)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Video unblocked.'))
|
this.notifier.success(this.i18n('Video unblocked.'))
|
||||||
|
|
||||||
this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
|
this.updateAbuseState(abuse, AbuseState.ACCEPTED)
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
|
@ -127,20 +128,20 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Delete video'),
|
label: this.i18n('Delete video'),
|
||||||
isDisplayed: videoAbuse => !videoAbuse.video.deleted,
|
isDisplayed: abuse => !abuse.video.deleted,
|
||||||
handler: async videoAbuse => {
|
handler: async abuse => {
|
||||||
const res = await this.confirmService.confirm(
|
const res = await this.confirmService.confirm(
|
||||||
this.i18n('Do you really want to delete this video?'),
|
this.i18n('Do you really want to delete this video?'),
|
||||||
this.i18n('Delete')
|
this.i18n('Delete')
|
||||||
)
|
)
|
||||||
if (res === false) return
|
if (res === false) return
|
||||||
|
|
||||||
this.videoService.removeVideo(videoAbuse.video.id)
|
this.videoService.removeVideo(abuse.video.id)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Video deleted.'))
|
this.notifier.success(this.i18n('Video deleted.'))
|
||||||
|
|
||||||
this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
|
this.updateAbuseState(abuse, AbuseState.ACCEPTED)
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
|
@ -155,8 +156,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Mute reporter'),
|
label: this.i18n('Mute reporter'),
|
||||||
handler: async videoAbuse => {
|
handler: async abuse => {
|
||||||
const account = videoAbuse.reporterAccount as Account
|
const account = abuse.reporterAccount as Account
|
||||||
|
|
||||||
this.blocklistService.blockAccountByInstance(account)
|
this.blocklistService.blockAccountByInstance(account)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
@ -174,13 +175,13 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: this.i18n('Mute server'),
|
label: this.i18n('Mute server'),
|
||||||
isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId,
|
isDisplayed: abuse => !abuse.reporterAccount.userId,
|
||||||
handler: async videoAbuse => {
|
handler: async abuse => {
|
||||||
this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host)
|
this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(
|
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 () {
|
getIdentifier () {
|
||||||
return 'VideoAbuseListComponent'
|
return 'AbuseListComponent'
|
||||||
}
|
}
|
||||||
|
|
||||||
openModerationCommentModal (videoAbuse: VideoAbuse) {
|
openModerationCommentModal (abuse: Abuse) {
|
||||||
this.moderationCommentModal.openModal(videoAbuse)
|
this.moderationCommentModal.openModal(abuse)
|
||||||
}
|
}
|
||||||
|
|
||||||
onModerationCommentUpdated () {
|
onModerationCommentUpdated () {
|
||||||
|
@ -240,26 +241,26 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
}
|
}
|
||||||
/* END Table filter functions */
|
/* END Table filter functions */
|
||||||
|
|
||||||
isVideoAbuseAccepted (videoAbuse: VideoAbuse) {
|
isAbuseAccepted (abuse: Abuse) {
|
||||||
return videoAbuse.state.id === VideoAbuseState.ACCEPTED
|
return abuse.state.id === AbuseState.ACCEPTED
|
||||||
}
|
}
|
||||||
|
|
||||||
isVideoAbuseRejected (videoAbuse: VideoAbuse) {
|
isAbuseRejected (abuse: Abuse) {
|
||||||
return videoAbuse.state.id === VideoAbuseState.REJECTED
|
return abuse.state.id === AbuseState.REJECTED
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoUrl (videoAbuse: VideoAbuse) {
|
getVideoUrl (abuse: Abuse) {
|
||||||
return Video.buildClientUrl(videoAbuse.video.uuid)
|
return Video.buildClientUrl(abuse.video.uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideoEmbed (videoAbuse: VideoAbuse) {
|
getVideoEmbed (abuse: Abuse) {
|
||||||
return buildVideoEmbed(
|
return buildVideoEmbed(
|
||||||
buildVideoLink({
|
buildVideoLink({
|
||||||
baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`,
|
baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
|
||||||
title: false,
|
title: false,
|
||||||
warningTitle: false,
|
warningTitle: false,
|
||||||
startTime: videoAbuse.startAt,
|
startTime: abuse.startAt,
|
||||||
stopTime: videoAbuse.endAt
|
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()
|
($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'))
|
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
|
||||||
if (res === false) return
|
if (res === false) return
|
||||||
|
|
||||||
this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
|
this.abuseService.removeAbuse(abuse).subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Abuse deleted.'))
|
this.notifier.success(this.i18n('Abuse deleted.'))
|
||||||
this.loadData()
|
this.loadData()
|
||||||
|
@ -282,8 +283,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) {
|
updateAbuseState (abuse: Abuse, state: AbuseState) {
|
||||||
this.videoAbuseService.updateVideoAbuse(videoAbuse, { state })
|
this.abuseService.updateAbuse(abuse, { state })
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => this.loadData(),
|
() => this.loadData(),
|
||||||
|
|
||||||
|
@ -292,14 +293,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
|
||||||
}
|
}
|
||||||
|
|
||||||
protected loadData () {
|
protected loadData () {
|
||||||
return this.videoAbuseService.getVideoAbuses({
|
return this.abuseService.getAbuses({
|
||||||
pagination: this.pagination,
|
pagination: this.pagination,
|
||||||
sort: this.sort,
|
sort: this.sort,
|
||||||
search: this.search
|
search: this.search
|
||||||
}).subscribe(
|
}).subscribe(
|
||||||
async resultList => {
|
async resultList => {
|
||||||
this.totalRecords = resultList.total
|
this.totalRecords = resultList.total
|
||||||
const videoAbuses = []
|
const abuses = []
|
||||||
|
|
||||||
for (const abuse of resultList.data) {
|
for (const abuse of resultList.data) {
|
||||||
Object.assign(abuse, {
|
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.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
|
||||||
if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
|
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)
|
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 { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier } from '@app/core'
|
||||||
import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms'
|
import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
|
||||||
import { VideoAbuseService } from '@app/shared/shared-moderation'
|
import { AbuseService } from '@app/shared/shared-moderation'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { VideoAbuse } from '@shared/models'
|
import { Abuse } from '@shared/models'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-moderation-comment-modal',
|
selector: 'my-moderation-comment-modal',
|
||||||
|
@ -16,15 +16,15 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
||||||
@ViewChild('modal', { static: true }) modal: NgbModal
|
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||||
@Output() commentUpdated = new EventEmitter<string>()
|
@Output() commentUpdated = new EventEmitter<string>()
|
||||||
|
|
||||||
private abuseToComment: VideoAbuse
|
private abuseToComment: Abuse
|
||||||
private openedModal: NgbModalRef
|
private openedModal: NgbModalRef
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private videoAbuseService: VideoAbuseService,
|
private abuseService: AbuseService,
|
||||||
private videoAbuseValidatorsService: VideoAbuseValidatorsService,
|
private abuseValidatorsService: AbuseValidatorsService,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -32,11 +32,11 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.buildForm({
|
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.abuseToComment = abuseToComment
|
||||||
this.openedModal = this.modalService.open(this.modal, { centered: true })
|
this.openedModal = this.modalService.open(this.modal, { centered: true })
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
||||||
async banUser () {
|
async banUser () {
|
||||||
const moderationComment: string = this.form.value[ 'moderationComment' ]
|
const moderationComment: string = this.form.value[ 'moderationComment' ]
|
||||||
|
|
||||||
this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment })
|
this.abuseService.updateAbuse(this.abuseToComment, { moderationComment })
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Comment updated.'))
|
this.notifier.success(this.i18n('Comment updated.'))
|
|
@ -1,5 +1,5 @@
|
||||||
|
export * from './abuse-list'
|
||||||
export * from './instance-blocklist'
|
export * from './instance-blocklist'
|
||||||
export * from './video-abuse-list'
|
|
||||||
export * from './video-block-list'
|
export * from './video-block-list'
|
||||||
export * from './moderation.component'
|
export * from './moderation.component'
|
||||||
export * from './moderation.routes'
|
export * from './moderation.routes'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Routes } from '@angular/router'
|
import { Routes } from '@angular/router'
|
||||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
|
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
|
||||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
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 { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
|
||||||
import { UserRightGuard } from '@app/core'
|
import { UserRightGuard } from '@app/core'
|
||||||
import { UserRight } from '@shared/models'
|
import { UserRight } from '@shared/models'
|
||||||
|
@ -13,20 +13,25 @@ export const ModerationRoutes: Routes = [
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
redirectTo: 'video-abuses/list',
|
redirectTo: 'abuses/list',
|
||||||
pathMatch: 'full'
|
pathMatch: 'full'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'video-abuses',
|
path: 'video-abuses',
|
||||||
redirectTo: 'video-abuses/list',
|
redirectTo: 'abuses/list',
|
||||||
pathMatch: 'full'
|
pathMatch: 'full'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'video-abuses/list',
|
path: 'video-abuses/list',
|
||||||
component: VideoAbuseListComponent,
|
redirectTo: 'abuses/list',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'abuses/list',
|
||||||
|
component: AbuseListComponent,
|
||||||
canActivate: [ UserRightGuard ],
|
canActivate: [ UserRightGuard ],
|
||||||
data: {
|
data: {
|
||||||
userRight: UserRight.MANAGE_VIDEO_ABUSES,
|
userRight: UserRight.MANAGE_ABUSES,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Video reports'
|
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.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
|
||||||
|
|
||||||
this.rightNotifications = {
|
this.rightNotifications = {
|
||||||
videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
|
videoAbuseAsModerator: UserRight.MANAGE_ABUSES,
|
||||||
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
|
videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
|
||||||
newUserRegistration: UserRight.MANAGE_USERS,
|
newUserRegistration: UserRight.MANAGE_USERS,
|
||||||
newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
|
newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
|
||||||
|
|
|
@ -28,7 +28,7 @@ export class MenuComponent implements OnInit {
|
||||||
private routesPerRight: { [ role in UserRight ]?: string } = {
|
private routesPerRight: { [ role in UserRight ]?: string } = {
|
||||||
[UserRight.MANAGE_USERS]: '/admin/users',
|
[UserRight.MANAGE_USERS]: '/admin/users',
|
||||||
[UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends',
|
[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_VIDEO_BLACKLIST]: '/admin/moderation/video-blocks',
|
||||||
[UserRight.MANAGE_JOBS]: '/admin/jobs',
|
[UserRight.MANAGE_JOBS]: '/admin/jobs',
|
||||||
[UserRight.MANAGE_CONFIGURATION]: '/admin/config'
|
[UserRight.MANAGE_CONFIGURATION]: '/admin/config'
|
||||||
|
@ -126,7 +126,7 @@ export class MenuComponent implements OnInit {
|
||||||
const adminRights = [
|
const adminRights = [
|
||||||
UserRight.MANAGE_USERS,
|
UserRight.MANAGE_USERS,
|
||||||
UserRight.MANAGE_SERVER_FOLLOW,
|
UserRight.MANAGE_SERVER_FOLLOW,
|
||||||
UserRight.MANAGE_VIDEO_ABUSES,
|
UserRight.MANAGE_ABUSES,
|
||||||
UserRight.MANAGE_VIDEO_BLACKLIST,
|
UserRight.MANAGE_VIDEO_BLACKLIST,
|
||||||
UserRight.MANAGE_JOBS,
|
UserRight.MANAGE_JOBS,
|
||||||
UserRight.MANAGE_CONFIGURATION
|
UserRight.MANAGE_CONFIGURATION
|
||||||
|
|
|
@ -4,12 +4,12 @@ import { Injectable } from '@angular/core'
|
||||||
import { BuildFormValidator } from './form-validator.service'
|
import { BuildFormValidator } from './form-validator.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoAbuseValidatorsService {
|
export class AbuseValidatorsService {
|
||||||
readonly VIDEO_ABUSE_REASON: BuildFormValidator
|
readonly ABUSE_REASON: BuildFormValidator
|
||||||
readonly VIDEO_ABUSE_MODERATION_COMMENT: BuildFormValidator
|
readonly ABUSE_MODERATION_COMMENT: BuildFormValidator
|
||||||
|
|
||||||
constructor (private i18n: I18n) {
|
constructor (private i18n: I18n) {
|
||||||
this.VIDEO_ABUSE_REASON = {
|
this.ABUSE_REASON = {
|
||||||
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
|
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
|
||||||
MESSAGES: {
|
MESSAGES: {
|
||||||
'required': this.i18n('Report reason is required.'),
|
'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) ],
|
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
|
||||||
MESSAGES: {
|
MESSAGES: {
|
||||||
'required': this.i18n('Moderation comment is required.'),
|
'required': this.i18n('Moderation comment is required.'),
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './abuse-validators.service'
|
||||||
export * from './batch-domains-validators.service'
|
export * from './batch-domains-validators.service'
|
||||||
export * from './custom-config-validators.service'
|
export * from './custom-config-validators.service'
|
||||||
export * from './form-validator.service'
|
export * from './form-validator.service'
|
||||||
|
@ -6,7 +7,6 @@ export * from './instance-validators.service'
|
||||||
export * from './login-validators.service'
|
export * from './login-validators.service'
|
||||||
export * from './reset-password-validators.service'
|
export * from './reset-password-validators.service'
|
||||||
export * from './user-validators.service'
|
export * from './user-validators.service'
|
||||||
export * from './video-abuse-validators.service'
|
|
||||||
export * from './video-accept-ownership-validators.service'
|
export * from './video-accept-ownership-validators.service'
|
||||||
export * from './video-block-validators.service'
|
export * from './video-block-validators.service'
|
||||||
export * from './video-captions-validators.service'
|
export * from './video-captions-validators.service'
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
LoginValidatorsService,
|
LoginValidatorsService,
|
||||||
ResetPasswordValidatorsService,
|
ResetPasswordValidatorsService,
|
||||||
UserValidatorsService,
|
UserValidatorsService,
|
||||||
VideoAbuseValidatorsService,
|
AbuseValidatorsService,
|
||||||
VideoAcceptOwnershipValidatorsService,
|
VideoAcceptOwnershipValidatorsService,
|
||||||
VideoBlockValidatorsService,
|
VideoBlockValidatorsService,
|
||||||
VideoCaptionsValidatorsService,
|
VideoCaptionsValidatorsService,
|
||||||
|
@ -69,7 +69,7 @@ import { TimestampInputComponent } from './timestamp-input.component'
|
||||||
LoginValidatorsService,
|
LoginValidatorsService,
|
||||||
ResetPasswordValidatorsService,
|
ResetPasswordValidatorsService,
|
||||||
UserValidatorsService,
|
UserValidatorsService,
|
||||||
VideoAbuseValidatorsService,
|
AbuseValidatorsService,
|
||||||
VideoAcceptOwnershipValidatorsService,
|
VideoAcceptOwnershipValidatorsService,
|
||||||
VideoBlockValidatorsService,
|
VideoBlockValidatorsService,
|
||||||
VideoCaptionsValidatorsService,
|
VideoCaptionsValidatorsService,
|
||||||
|
|
|
@ -14,7 +14,7 @@ export abstract class Actor implements ActorServer {
|
||||||
|
|
||||||
avatarUrl: string
|
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?.avatar?.url) return actor.avatar.url
|
||||||
|
|
||||||
if (actor && actor.avatar) {
|
if (actor && actor.avatar) {
|
||||||
|
|
|
@ -25,9 +25,20 @@ export class UserNotification implements UserNotificationServer {
|
||||||
video: VideoInfo
|
video: VideoInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
videoAbuse?: {
|
abuse?: {
|
||||||
id: number
|
id: number
|
||||||
video: VideoInfo
|
|
||||||
|
video?: VideoInfo
|
||||||
|
|
||||||
|
comment?: {
|
||||||
|
threadId: number
|
||||||
|
|
||||||
|
video: {
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account?: ActorInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
videoBlacklist?: {
|
videoBlacklist?: {
|
||||||
|
@ -55,7 +66,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
// Additional fields
|
// Additional fields
|
||||||
videoUrl?: string
|
videoUrl?: string
|
||||||
commentUrl?: any[]
|
commentUrl?: any[]
|
||||||
videoAbuseUrl?: string
|
abuseUrl?: string
|
||||||
videoAutoBlacklistUrl?: string
|
videoAutoBlacklistUrl?: string
|
||||||
accountUrl?: string
|
accountUrl?: string
|
||||||
videoImportIdentifier?: string
|
videoImportIdentifier?: string
|
||||||
|
@ -78,7 +89,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
this.comment = hash.comment
|
this.comment = hash.comment
|
||||||
if (this.comment) this.setAvatarUrl(this.comment.account)
|
if (this.comment) this.setAvatarUrl(this.comment.account)
|
||||||
|
|
||||||
this.videoAbuse = hash.videoAbuse
|
this.abuse = hash.abuse
|
||||||
|
|
||||||
this.videoBlacklist = hash.videoBlacklist
|
this.videoBlacklist = hash.videoBlacklist
|
||||||
|
|
||||||
|
@ -108,8 +119,9 @@ export class UserNotification implements UserNotificationServer {
|
||||||
break
|
break
|
||||||
|
|
||||||
case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS:
|
case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS:
|
||||||
this.videoAbuseUrl = '/admin/moderation/video-abuses/list'
|
this.abuseUrl = '/admin/moderation/abuses/list'
|
||||||
this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
|
|
||||||
|
if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
|
||||||
break
|
break
|
||||||
|
|
||||||
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
|
case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
|
||||||
|
@ -178,7 +190,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
|
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)
|
actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<ng-template #noVideo>
|
<ng-template #noVideo>
|
||||||
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
<div class="message" i18n>
|
<div class="message" i18n>
|
||||||
The notification concerns a video now unavailable
|
The notification concerns a video now unavailable
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
<div class="message" i18n>
|
<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>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
|
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
|
||||||
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
|
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="message" i18n>
|
<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>
|
<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>
|
</div>
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
|
|
||||||
<ng-template #noComment>
|
<ng-template #noComment>
|
||||||
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
|
<my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
|
||||||
|
|
||||||
<div class="message" i18n>
|
<div class="message" i18n>
|
||||||
The notification concerns a comment now unavailable
|
The notification concerns a comment now unavailable
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,12 +5,12 @@ import { catchError, map } from 'rxjs/operators'
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { RestExtractor, RestPagination, RestService } from '@app/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'
|
import { environment } from '../../../environments/environment'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoAbuseService {
|
export class AbuseService {
|
||||||
private static BASE_VIDEO_ABUSE_URL = environment.apiUrl + '/api/v1/videos/'
|
private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses'
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private authHttp: HttpClient,
|
private authHttp: HttpClient,
|
||||||
|
@ -18,13 +18,13 @@ export class VideoAbuseService {
|
||||||
private restExtractor: RestExtractor
|
private restExtractor: RestExtractor
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getVideoAbuses (options: {
|
getAbuses (options: {
|
||||||
pagination: RestPagination,
|
pagination: RestPagination,
|
||||||
sort: SortMeta,
|
sort: SortMeta,
|
||||||
search?: string
|
search?: string
|
||||||
}): Observable<ResultList<VideoAbuse>> {
|
}): Observable<ResultList<Abuse>> {
|
||||||
const { pagination, sort, search } = options
|
const { pagination, sort, search } = options
|
||||||
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse'
|
const url = AbuseService.BASE_ABUSE_URL + 'abuse'
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
@ -35,9 +35,9 @@ export class VideoAbuseService {
|
||||||
state: {
|
state: {
|
||||||
prefix: 'state:',
|
prefix: 'state:',
|
||||||
handler: v => {
|
handler: v => {
|
||||||
if (v === 'accepted') return VideoAbuseState.ACCEPTED
|
if (v === 'accepted') return AbuseState.ACCEPTED
|
||||||
if (v === 'pending') return VideoAbuseState.PENDING
|
if (v === 'pending') return AbuseState.PENDING
|
||||||
if (v === 'rejected') return VideoAbuseState.REJECTED
|
if (v === 'rejected') return AbuseState.REJECTED
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
@ -59,14 +59,14 @@ export class VideoAbuseService {
|
||||||
params = this.restService.addObjectParams(params, filters)
|
params = this.restService.addObjectParams(params, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.authHttp.get<ResultList<VideoAbuse>>(url, { params })
|
return this.authHttp.get<ResultList<Abuse>>(url, { params })
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
reportVideo (parameters: { id: number } & VideoAbuseCreate) {
|
reportVideo (parameters: AbuseCreate) {
|
||||||
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse'
|
const url = AbuseService.BASE_ABUSE_URL
|
||||||
|
|
||||||
const body = omit(parameters, [ 'id' ])
|
const body = omit(parameters, [ 'id' ])
|
||||||
|
|
||||||
|
@ -77,8 +77,8 @@ export class VideoAbuseService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) {
|
updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
|
||||||
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
||||||
|
|
||||||
return this.authHttp.put(url, abuseUpdate)
|
return this.authHttp.put(url, abuseUpdate)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -87,8 +87,8 @@ export class VideoAbuseService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeVideoAbuse (videoAbuse: VideoAbuse) {
|
removeAbuse (abuse: Abuse) {
|
||||||
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
|
const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
|
||||||
|
|
||||||
return this.authHttp.delete(url)
|
return this.authHttp.delete(url)
|
||||||
.pipe(
|
.pipe(
|
|
@ -1,3 +1,4 @@
|
||||||
|
export * from './abuse.service'
|
||||||
export * from './account-block.model'
|
export * from './account-block.model'
|
||||||
export * from './account-blocklist.component'
|
export * from './account-blocklist.component'
|
||||||
export * from './batch-domains-modal.component'
|
export * from './batch-domains-modal.component'
|
||||||
|
@ -6,7 +7,6 @@ export * from './bulk.service'
|
||||||
export * from './server-blocklist.component'
|
export * from './server-blocklist.component'
|
||||||
export * from './user-ban-modal.component'
|
export * from './user-ban-modal.component'
|
||||||
export * from './user-moderation-dropdown.component'
|
export * from './user-moderation-dropdown.component'
|
||||||
export * from './video-abuse.service'
|
|
||||||
export * from './video-block.component'
|
export * from './video-block.component'
|
||||||
export * from './video-block.service'
|
export * from './video-block.service'
|
||||||
export * from './video-report.component'
|
export * from './video-report.component'
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { BlocklistService } from './blocklist.service'
|
||||||
import { BulkService } from './bulk.service'
|
import { BulkService } from './bulk.service'
|
||||||
import { UserBanModalComponent } from './user-ban-modal.component'
|
import { UserBanModalComponent } from './user-ban-modal.component'
|
||||||
import { UserModerationDropdownComponent } from './user-moderation-dropdown.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 { VideoBlockComponent } from './video-block.component'
|
||||||
import { VideoBlockService } from './video-block.service'
|
import { VideoBlockService } from './video-block.service'
|
||||||
import { VideoReportComponent } from './video-report.component'
|
import { VideoReportComponent } from './video-report.component'
|
||||||
|
@ -39,7 +39,7 @@ import { VideoReportComponent } from './video-report.component'
|
||||||
providers: [
|
providers: [
|
||||||
BlocklistService,
|
BlocklistService,
|
||||||
BulkService,
|
BulkService,
|
||||||
VideoAbuseService,
|
AbuseService,
|
||||||
VideoBlockService
|
VideoBlockService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,13 +3,13 @@ import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
|
||||||
import { Component, Input, OnInit, ViewChild } from '@angular/core'
|
import { Component, Input, OnInit, ViewChild } from '@angular/core'
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||||
import { Notifier } from '@app/core'
|
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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
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 { Video } from '../shared-main'
|
||||||
import { VideoAbuseService } from './video-abuse.service'
|
import { AbuseService } from './abuse.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-report',
|
selector: 'my-video-report',
|
||||||
|
@ -22,7 +22,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
@ViewChild('modal', { static: true }) modal: NgbModal
|
@ViewChild('modal', { static: true }) modal: NgbModal
|
||||||
|
|
||||||
error: string = null
|
error: string = null
|
||||||
predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
|
predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
|
||||||
embedHtml: SafeHtml
|
embedHtml: SafeHtml
|
||||||
|
|
||||||
private openedModal: NgbModalRef
|
private openedModal: NgbModalRef
|
||||||
|
@ -30,8 +30,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
constructor (
|
constructor (
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private videoAbuseValidatorsService: VideoAbuseValidatorsService,
|
private abuseValidatorsService: AbuseValidatorsService,
|
||||||
private videoAbuseService: VideoAbuseService,
|
private abuseService: AbuseService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private sanitizer: DomSanitizer,
|
private sanitizer: DomSanitizer,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
|
@ -69,8 +69,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
this.buildForm({
|
this.buildForm({
|
||||||
reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON,
|
reason: this.abuseValidatorsService.ABUSE_REASON,
|
||||||
predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null),
|
predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null),
|
||||||
timestamp: {
|
timestamp: {
|
||||||
hasStart: null,
|
hasStart: null,
|
||||||
startAt: null,
|
startAt: null,
|
||||||
|
@ -136,15 +136,18 @@ export class VideoReportComponent extends FormReactive implements OnInit {
|
||||||
|
|
||||||
report () {
|
report () {
|
||||||
const reason = this.form.get('reason').value
|
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
|
const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
|
||||||
|
|
||||||
this.videoAbuseService.reportVideo({
|
this.abuseService.reportVideo({
|
||||||
id: this.video.id,
|
accountId: this.video.account.id,
|
||||||
reason,
|
reason,
|
||||||
predefinedReasons,
|
predefinedReasons,
|
||||||
startAt: hasStart && startAt ? startAt : undefined,
|
video: {
|
||||||
endAt: hasEnd && endAt ? endAt : undefined
|
id: this.video.id,
|
||||||
|
startAt: hasStart && startAt ? startAt : undefined,
|
||||||
|
endAt: hasEnd && endAt ? endAt : undefined
|
||||||
|
}
|
||||||
}).subscribe(
|
}).subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Video reported.'))
|
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 * as RateLimit from 'express-rate-limit'
|
||||||
import { badRequest } from '../../helpers/express-utils'
|
import { badRequest } from '../../helpers/express-utils'
|
||||||
import { CONFIG } from '../../initializers/config'
|
import { CONFIG } from '../../initializers/config'
|
||||||
|
import { abuseRouter } from './abuse'
|
||||||
import { accountsRouter } from './accounts'
|
import { accountsRouter } from './accounts'
|
||||||
import { bulkRouter } from './bulk'
|
import { bulkRouter } from './bulk'
|
||||||
import { configRouter } from './config'
|
import { configRouter } from './config'
|
||||||
|
@ -32,6 +33,7 @@ const apiRateLimiter = RateLimit({
|
||||||
apiRouter.use(apiRateLimiter)
|
apiRouter.use(apiRateLimiter)
|
||||||
|
|
||||||
apiRouter.use('/server', serverRouter)
|
apiRouter.use('/server', serverRouter)
|
||||||
|
apiRouter.use('/abuses', abuseRouter)
|
||||||
apiRouter.use('/bulk', bulkRouter)
|
apiRouter.use('/bulk', bulkRouter)
|
||||||
apiRouter.use('/oauth-clients', oauthClientsRouter)
|
apiRouter.use('/oauth-clients', oauthClientsRouter)
|
||||||
apiRouter.use('/config', configRouter)
|
apiRouter.use('/config', configRouter)
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared'
|
import { AbuseModel } from '@server/models/abuse/abuse'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { getServerActor } from '@server/models/application/application'
|
||||||
|
import { AbuseCreate, UserRight, VideoAbuseCreate } from '../../../../shared'
|
||||||
import { getFormattedObjects } from '../../../helpers/utils'
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
|
||||||
import {
|
import {
|
||||||
|
abusesSortValidator,
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
|
@ -12,28 +13,21 @@ import {
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
videoAbuseGetValidator,
|
videoAbuseGetValidator,
|
||||||
|
videoAbuseListValidator,
|
||||||
videoAbuseReportValidator,
|
videoAbuseReportValidator,
|
||||||
videoAbusesSortValidator,
|
videoAbuseUpdateValidator
|
||||||
videoAbuseUpdateValidator,
|
|
||||||
videoAbuseListValidator
|
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
import { AccountModel } from '../../../models/account/account'
|
import { deleteAbuse, reportAbuse, updateAbuse } from '../abuse'
|
||||||
import { VideoAbuseModel } from '../../../models/video/video-abuse'
|
|
||||||
import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
|
// FIXME: deprecated in 2.3. Remove this controller
|
||||||
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'
|
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('abuse')
|
|
||||||
const abuseVideoRouter = express.Router()
|
const abuseVideoRouter = express.Router()
|
||||||
|
|
||||||
abuseVideoRouter.get('/abuse',
|
abuseVideoRouter.get('/abuse',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
|
ensureUserHasRight(UserRight.MANAGE_ABUSES),
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
videoAbusesSortValidator,
|
abusesSortValidator,
|
||||||
setDefaultSort,
|
setDefaultSort,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
videoAbuseListValidator,
|
videoAbuseListValidator,
|
||||||
|
@ -41,7 +35,7 @@ abuseVideoRouter.get('/abuse',
|
||||||
)
|
)
|
||||||
abuseVideoRouter.put('/:videoId/abuse/:id',
|
abuseVideoRouter.put('/:videoId/abuse/:id',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
|
ensureUserHasRight(UserRight.MANAGE_ABUSES),
|
||||||
asyncMiddleware(videoAbuseUpdateValidator),
|
asyncMiddleware(videoAbuseUpdateValidator),
|
||||||
asyncRetryTransactionMiddleware(updateVideoAbuse)
|
asyncRetryTransactionMiddleware(updateVideoAbuse)
|
||||||
)
|
)
|
||||||
|
@ -52,7 +46,7 @@ abuseVideoRouter.post('/:videoId/abuse',
|
||||||
)
|
)
|
||||||
abuseVideoRouter.delete('/:videoId/abuse/:id',
|
abuseVideoRouter.delete('/:videoId/abuse/:id',
|
||||||
authenticate,
|
authenticate,
|
||||||
ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
|
ensureUserHasRight(UserRight.MANAGE_ABUSES),
|
||||||
asyncMiddleware(videoAbuseGetValidator),
|
asyncMiddleware(videoAbuseGetValidator),
|
||||||
asyncRetryTransactionMiddleware(deleteVideoAbuse)
|
asyncRetryTransactionMiddleware(deleteVideoAbuse)
|
||||||
)
|
)
|
||||||
|
@ -69,11 +63,12 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
|
||||||
const user = res.locals.oauth.token.user
|
const user = res.locals.oauth.token.user
|
||||||
const serverActor = await getServerActor()
|
const serverActor = await getServerActor()
|
||||||
|
|
||||||
const resultList = await VideoAbuseModel.listForApi({
|
const resultList = await AbuseModel.listForApi({
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: req.query.sort,
|
sort: req.query.sort,
|
||||||
id: req.query.id,
|
id: req.query.id,
|
||||||
|
filter: 'video',
|
||||||
predefinedReason: req.query.predefinedReason,
|
predefinedReason: req.query.predefinedReason,
|
||||||
search: req.query.search,
|
search: req.query.search,
|
||||||
state: req.query.state,
|
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) {
|
async function updateVideoAbuse (req: express.Request, res: express.Response) {
|
||||||
const videoAbuse = res.locals.videoAbuse
|
return updateAbuse(req, res)
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteVideoAbuse (req: express.Request, res: express.Response) {
|
async function deleteVideoAbuse (req: express.Request, res: express.Response) {
|
||||||
const videoAbuse = res.locals.videoAbuse
|
return deleteAbuse(req, res)
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reportVideoAbuse (req: express.Request, res: express.Response) {
|
async function reportVideoAbuse (req: express.Request, res: express.Response) {
|
||||||
const videoInstance = res.locals.videoAll
|
const oldBody = req.body as VideoAbuseCreate
|
||||||
const body: VideoAbuseCreate = req.body
|
|
||||||
let reporterAccount: MAccountDefault
|
|
||||||
let videoAbuseJSON: VideoAbuse
|
|
||||||
|
|
||||||
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
|
req.body = {
|
||||||
reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
|
accountId: res.locals.videoAll.VideoChannel.accountId,
|
||||||
const predefinedReasons = body.predefinedReasons?.map(r => videoAbusePredefinedReasonsMap[r])
|
|
||||||
|
|
||||||
const abuseToCreate = {
|
reason: oldBody.reason,
|
||||||
reporterAccountId: reporterAccount.id,
|
predefinedReasons: oldBody.predefinedReasons,
|
||||||
reason: body.reason,
|
|
||||||
videoId: videoInstance.id,
|
video: {
|
||||||
state: VideoAbuseState.PENDING,
|
id: res.locals.videoAll.id,
|
||||||
predefinedReasons,
|
startAt: oldBody.startAt,
|
||||||
startAt: body.startAt,
|
endAt: oldBody.endAt
|
||||||
endAt: body.endAt
|
|
||||||
}
|
}
|
||||||
|
} as AbuseCreate
|
||||||
|
|
||||||
const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
|
return reportAbuse(req, res)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import * as path from 'path'
|
|
||||||
import * as express from 'express'
|
|
||||||
import { diff } from 'deep-object-diff'
|
import { diff } from 'deep-object-diff'
|
||||||
import { chain } from 'lodash'
|
import * as express from 'express'
|
||||||
import * as flatten from 'flat'
|
import * as flatten from 'flat'
|
||||||
|
import { chain } from 'lodash'
|
||||||
|
import * as path from 'path'
|
||||||
import * as winston from 'winston'
|
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 { 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) {
|
function getAuditIdFromRes (res: express.Response) {
|
||||||
return res.locals.oauth.token.User.username
|
return res.locals.oauth.token.User.username
|
||||||
|
@ -212,18 +212,15 @@ class VideoChannelAuditView extends EntityAuditView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoAbuseKeysToKeep = [
|
const abuseKeysToKeep = [
|
||||||
'id',
|
'id',
|
||||||
'reason',
|
'reason',
|
||||||
'reporterAccount',
|
'reporterAccount',
|
||||||
'video-id',
|
|
||||||
'video-name',
|
|
||||||
'video-uuid',
|
|
||||||
'createdAt'
|
'createdAt'
|
||||||
]
|
]
|
||||||
class VideoAbuseAuditView extends EntityAuditView {
|
class AbuseAuditView extends EntityAuditView {
|
||||||
constructor (private readonly videoAbuse: VideoAbuse) {
|
constructor (private readonly abuse: Abuse) {
|
||||||
super(videoAbuseKeysToKeep, 'abuse', videoAbuse)
|
super(abuseKeysToKeep, 'abuse', abuse)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,6 +271,6 @@ export {
|
||||||
CommentAuditView,
|
CommentAuditView,
|
||||||
UserAuditView,
|
UserAuditView,
|
||||||
VideoAuditView,
|
VideoAuditView,
|
||||||
VideoAbuseAuditView,
|
AbuseAuditView,
|
||||||
CustomConfigAuditView
|
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 { isActivityPubUrlValid } from './misc'
|
||||||
import { isVideoAbuseReasonValid } from '../video-abuses'
|
import { isAbuseReasonValid } from '../abuses'
|
||||||
|
|
||||||
function isFlagActivityValid (activity: any) {
|
function isFlagActivityValid (activity: any) {
|
||||||
return activity.type === 'Flag' &&
|
return activity.type === 'Flag' &&
|
||||||
isVideoAbuseReasonValid(activity.content) &&
|
isAbuseReasonValid(activity.content) &&
|
||||||
isActivityPubUrlValid(activity.object)
|
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 { Response } from 'express'
|
||||||
import { VideoAbuseModel } from '../../models/video/video-abuse'
|
import { AbuseModel } from '../../models/abuse/abuse'
|
||||||
import { fetchVideo } from '../video'
|
import { fetchVideo } from '../video'
|
||||||
|
|
||||||
|
// FIXME: deprecated in 2.3. Remove this function
|
||||||
async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
|
async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
|
||||||
const abuseId = parseInt(abuseIdArg + '', 10)
|
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 userId = res.locals.oauth?.token.User.id
|
||||||
const video = await fetchVideo(videoUUID, 'all', userId)
|
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)
|
res.status(404)
|
||||||
.json({ error: 'Video abuse not found' })
|
.json({ error: 'Video abuse not found' })
|
||||||
.end()
|
.end()
|
||||||
|
@ -21,12 +22,17 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
res.locals.videoAbuse = videoAbuse
|
res.locals.abuse = abuse
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
doesAbuseExist,
|
||||||
doesVideoAbuseExist
|
doesVideoAbuseExist
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
|
export * from './abuses'
|
||||||
export * from './accounts'
|
export * from './accounts'
|
||||||
export * from './video-abuses'
|
|
||||||
export * from './video-blacklists'
|
export * from './video-blacklists'
|
||||||
export * from './video-captions'
|
export * from './video-captions'
|
||||||
export * from './video-channels'
|
export * from './video-channels'
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models'
|
|
||||||
import { ActivityPubActorType } from '../../shared/models/activitypub'
|
import { ActivityPubActorType } from '../../shared/models/activitypub'
|
||||||
import { FollowState } from '../../shared/models/actors'
|
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
|
// Do not use barrels, remain constants as independent as possible
|
||||||
import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
|
import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
|
||||||
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
|
||||||
|
@ -51,7 +59,6 @@ const SORTABLE_COLUMNS = {
|
||||||
USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
|
USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
|
||||||
ACCOUNTS: [ 'createdAt' ],
|
ACCOUNTS: [ 'createdAt' ],
|
||||||
JOBS: [ 'createdAt' ],
|
JOBS: [ 'createdAt' ],
|
||||||
VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],
|
|
||||||
VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
|
VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
|
||||||
VIDEO_IMPORTS: [ 'createdAt' ],
|
VIDEO_IMPORTS: [ 'createdAt' ],
|
||||||
VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ],
|
VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ],
|
||||||
|
@ -66,6 +73,8 @@ const SORTABLE_COLUMNS = {
|
||||||
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
|
VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
|
||||||
VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
|
VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
|
||||||
|
|
||||||
|
ABUSES: [ 'id', 'createdAt', 'state' ],
|
||||||
|
|
||||||
ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
|
ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
|
||||||
SERVERS_BLOCKLIST: [ 'createdAt' ],
|
SERVERS_BLOCKLIST: [ 'createdAt' ],
|
||||||
|
|
||||||
|
@ -193,7 +202,7 @@ const CONSTRAINTS_FIELDS = {
|
||||||
VIDEO_LANGUAGES: { max: 500 }, // Array length
|
VIDEO_LANGUAGES: { max: 500 }, // Array length
|
||||||
BLOCKED_REASON: { min: 3, max: 250 } // Length
|
BLOCKED_REASON: { min: 3, max: 250 } // Length
|
||||||
},
|
},
|
||||||
VIDEO_ABUSES: {
|
ABUSES: {
|
||||||
REASON: { min: 2, max: 3000 }, // Length
|
REASON: { min: 2, max: 3000 }, // Length
|
||||||
MODERATION_COMMENT: { min: 2, max: 3000 } // Length
|
MODERATION_COMMENT: { min: 2, max: 3000 } // Length
|
||||||
},
|
},
|
||||||
|
@ -378,10 +387,10 @@ const VIDEO_IMPORT_STATES = {
|
||||||
[VideoImportState.REJECTED]: 'Rejected'
|
[VideoImportState.REJECTED]: 'Rejected'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_ABUSE_STATES = {
|
const ABUSE_STATES = {
|
||||||
[VideoAbuseState.PENDING]: 'Pending',
|
[AbuseState.PENDING]: 'Pending',
|
||||||
[VideoAbuseState.REJECTED]: 'Rejected',
|
[AbuseState.REJECTED]: 'Rejected',
|
||||||
[VideoAbuseState.ACCEPTED]: 'Accepted'
|
[AbuseState.ACCEPTED]: 'Accepted'
|
||||||
}
|
}
|
||||||
|
|
||||||
const VIDEO_PLAYLIST_PRIVACIES = {
|
const VIDEO_PLAYLIST_PRIVACIES = {
|
||||||
|
@ -778,7 +787,7 @@ export {
|
||||||
VIDEO_RATE_TYPES,
|
VIDEO_RATE_TYPES,
|
||||||
VIDEO_TRANSCODING_FPS,
|
VIDEO_TRANSCODING_FPS,
|
||||||
FFMPEG_NICE,
|
FFMPEG_NICE,
|
||||||
VIDEO_ABUSE_STATES,
|
ABUSE_STATES,
|
||||||
VIDEO_CHANNELS,
|
VIDEO_CHANNELS,
|
||||||
LRU_CACHE,
|
LRU_CACHE,
|
||||||
JOB_REQUEST_TIMEOUT,
|
JOB_REQUEST_TIMEOUT,
|
||||||
|
|
|
@ -1,44 +1,45 @@
|
||||||
|
import { QueryTypes, Transaction } from 'sequelize'
|
||||||
import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
|
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 { isTestInstance } from '../helpers/core-utils'
|
||||||
import { logger } from '../helpers/logger'
|
import { logger } from '../helpers/logger'
|
||||||
|
|
||||||
import { AccountModel } from '../models/account/account'
|
import { AccountModel } from '../models/account/account'
|
||||||
|
import { AccountBlocklistModel } from '../models/account/account-blocklist'
|
||||||
import { AccountVideoRateModel } from '../models/account/account-video-rate'
|
import { AccountVideoRateModel } from '../models/account/account-video-rate'
|
||||||
import { UserModel } from '../models/account/user'
|
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 { ActorModel } from '../models/activitypub/actor'
|
||||||
import { ActorFollowModel } from '../models/activitypub/actor-follow'
|
import { ActorFollowModel } from '../models/activitypub/actor-follow'
|
||||||
import { ApplicationModel } from '../models/application/application'
|
import { ApplicationModel } from '../models/application/application'
|
||||||
import { AvatarModel } from '../models/avatar/avatar'
|
import { AvatarModel } from '../models/avatar/avatar'
|
||||||
import { OAuthClientModel } from '../models/oauth/oauth-client'
|
import { OAuthClientModel } from '../models/oauth/oauth-client'
|
||||||
import { OAuthTokenModel } from '../models/oauth/oauth-token'
|
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 { 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 { TagModel } from '../models/video/tag'
|
||||||
|
import { ThumbnailModel } from '../models/video/thumbnail'
|
||||||
import { VideoModel } from '../models/video/video'
|
import { VideoModel } from '../models/video/video'
|
||||||
import { VideoAbuseModel } from '../models/video/video-abuse'
|
|
||||||
import { VideoBlacklistModel } from '../models/video/video-blacklist'
|
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 { VideoChannelModel } from '../models/video/video-channel'
|
||||||
import { VideoCommentModel } from '../models/video/video-comment'
|
import { VideoCommentModel } from '../models/video/video-comment'
|
||||||
import { VideoFileModel } from '../models/video/video-file'
|
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 { 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 { VideoPlaylistModel } from '../models/video/video-playlist'
|
||||||
import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
|
import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
|
||||||
import { ThumbnailModel } from '../models/video/thumbnail'
|
import { VideoShareModel } from '../models/video/video-share'
|
||||||
import { PluginModel } from '../models/server/plugin'
|
import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
|
||||||
import { QueryTypes, Transaction } from 'sequelize'
|
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
|
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
|
|
||||||
|
@ -86,6 +87,8 @@ async function initDatabaseModels (silent: boolean) {
|
||||||
TagModel,
|
TagModel,
|
||||||
AccountVideoRateModel,
|
AccountVideoRateModel,
|
||||||
UserModel,
|
UserModel,
|
||||||
|
AbuseModel,
|
||||||
|
VideoCommentAbuseModel,
|
||||||
VideoAbuseModel,
|
VideoAbuseModel,
|
||||||
VideoModel,
|
VideoModel,
|
||||||
VideoChangeOwnershipModel,
|
VideoChangeOwnershipModel,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import { VideoAbuseState } from '../../../shared/models/videos'
|
import { AbuseState } from '../../../shared/models'
|
||||||
|
|
||||||
async function up (utils: {
|
async function up (utils: {
|
||||||
transaction: Sequelize.Transaction
|
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)
|
await utils.sequelize.query(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,19 @@
|
||||||
import {
|
import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
|
||||||
ActivityCreate,
|
import { AccountModel } from '@server/models/account/account'
|
||||||
ActivityFlag,
|
import { VideoModel } from '@server/models/video/video'
|
||||||
VideoAbuseState,
|
import { VideoCommentModel } from '@server/models/video/video-comment'
|
||||||
videoAbusePredefinedReasonsMap
|
import { AbuseObject, abusePredefinedReasonsMap, AbuseState, ActivityCreate, ActivityFlag } from '../../../../shared'
|
||||||
} from '../../../../shared'
|
import { getAPId } from '../../../helpers/activitypub'
|
||||||
import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
|
|
||||||
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
import { retryTransactionWrapper } from '../../../helpers/database-utils'
|
||||||
import { logger } from '../../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
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 { APProcessorOptions } from '../../../types/activitypub-processor.model'
|
||||||
import { MActorSignature, MVideoAbuseAccountVideo } from '../../../types/models'
|
import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models'
|
||||||
import { AccountModel } from '@server/models/account/account'
|
|
||||||
|
|
||||||
async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
|
async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
|
||||||
const { activity, byActor } = options
|
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) {
|
async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
|
||||||
const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject)
|
const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject)
|
||||||
|
|
||||||
const account = byActor.Account
|
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 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) {
|
for (const object of objects) {
|
||||||
try {
|
try {
|
||||||
logger.debug('Reporting remote abuse for video %s.', getAPId(object))
|
const uri = getAPId(object)
|
||||||
|
|
||||||
const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
|
logger.debug('Reporting remote abuse for object %s.', uri)
|
||||||
const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
|
|
||||||
const tags = Array.isArray(flag.tag) ? flag.tag : []
|
|
||||||
const predefinedReasons = tags.map(tag => videoAbusePredefinedReasonsMap[tag.name])
|
|
||||||
.filter(v => !isNaN(v))
|
|
||||||
const startAt = flag.startAt
|
|
||||||
const endAt = flag.endAt
|
|
||||||
|
|
||||||
const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
|
await sequelizeTypescript.transaction(async t => {
|
||||||
const videoAbuseData = {
|
|
||||||
reporterAccountId: account.id,
|
const video = await VideoModel.loadByUrlAndPopulateAccount(uri)
|
||||||
reason: flag.content,
|
let videoComment: MCommentOwnerVideo
|
||||||
videoId: video.id,
|
let flaggedAccount: MAccountDefault
|
||||||
state: VideoAbuseState.PENDING,
|
|
||||||
predefinedReasons,
|
if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri)
|
||||||
startAt,
|
if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri)
|
||||||
endAt
|
|
||||||
|
if (!video && !videoComment && !flaggedAccount) {
|
||||||
|
logger.warn('Cannot flag unknown entity %s.', object)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
|
const baseAbuse = {
|
||||||
videoAbuseInstance.Video = video
|
reporterAccountId: reporterAccount.id,
|
||||||
videoAbuseInstance.Account = reporterAccount
|
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()
|
return await createAccountAbuse({
|
||||||
|
baseAbuse,
|
||||||
Notifier.Instance.notifyOnNewVideoAbuse({
|
reporterAccount,
|
||||||
videoAbuse: videoAbuseJSON,
|
transaction: t,
|
||||||
videoAbuseInstance,
|
accountInstance: flaggedAccount
|
||||||
reporter: reporterAccount.Actor.getIdentifier()
|
})
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} 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 { Transaction } from 'sequelize'
|
||||||
import { MActor, MVideoFullLight } from '../../../types/models'
|
import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
|
||||||
import { MVideoAbuseVideo } from '../../../types/models/video'
|
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) {
|
function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
|
||||||
if (!video.VideoChannel.Account.Actor.serverId) return // Local user
|
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
|
// Custom audience, we only send the abuse to the origin instance
|
||||||
const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
|
const audience = { to: [ flaggedAccount.Actor.url ], cc: [] }
|
||||||
const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience)
|
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)
|
if (!audience) audience = getAudience(byActor)
|
||||||
|
|
||||||
const activity = Object.assign(
|
const activity = Object.assign(
|
||||||
{ id: url, actor: byActor.url },
|
{ id: url, actor: byActor.url },
|
||||||
videoAbuse.toActivityPubObject()
|
abuse.toActivityPubObject()
|
||||||
)
|
)
|
||||||
|
|
||||||
return audiencify(activity, audience)
|
return audiencify(activity, audience)
|
||||||
|
@ -35,5 +34,5 @@ function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbus
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
sendVideoAbuse
|
sendAbuse
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import {
|
||||||
MActorId,
|
MActorId,
|
||||||
MActorUrl,
|
MActorUrl,
|
||||||
MCommentId,
|
MCommentId,
|
||||||
MVideoAbuseId,
|
|
||||||
MVideoId,
|
MVideoId,
|
||||||
MVideoUrl,
|
MVideoUrl,
|
||||||
MVideoUUID
|
MVideoUUID,
|
||||||
|
MAbuseId
|
||||||
} from '../../types/models'
|
} from '../../types/models'
|
||||||
import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist'
|
import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist'
|
||||||
import { MVideoFileVideoUUID } from '../../types/models/video/video-file'
|
import { MVideoFileVideoUUID } from '../../types/models/video/video-file'
|
||||||
|
@ -48,8 +48,8 @@ function getAccountActivityPubUrl (accountName: string) {
|
||||||
return WEBSERVER.URL + '/accounts/' + accountName
|
return WEBSERVER.URL + '/accounts/' + accountName
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) {
|
function getAbuseActivityPubUrl (abuse: MAbuseId) {
|
||||||
return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
|
return WEBSERVER.URL + '/admin/abuses/' + abuse.id
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
|
function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
|
||||||
|
@ -118,7 +118,7 @@ export {
|
||||||
getVideoCacheStreamingPlaylistActivityPubUrl,
|
getVideoCacheStreamingPlaylistActivityPubUrl,
|
||||||
getVideoChannelActivityPubUrl,
|
getVideoChannelActivityPubUrl,
|
||||||
getAccountActivityPubUrl,
|
getAccountActivityPubUrl,
|
||||||
getVideoAbuseActivityPubUrl,
|
getAbuseActivityPubUrl,
|
||||||
getActorFollowActivityPubUrl,
|
getActorFollowActivityPubUrl,
|
||||||
getActorFollowAcceptActivityPubUrl,
|
getActorFollowAcceptActivityPubUrl,
|
||||||
getVideoAnnounceActivityPubUrl,
|
getVideoAnnounceActivityPubUrl,
|
||||||
|
|
|
@ -1,26 +1,20 @@
|
||||||
|
import { readFileSync } from 'fs-extra'
|
||||||
|
import { merge } from 'lodash'
|
||||||
import { createTransport, Transporter } from 'nodemailer'
|
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 { isTestInstance, root } from '../helpers/core-utils'
|
||||||
import { bunyanLogger, logger } from '../helpers/logger'
|
import { bunyanLogger, logger } from '../helpers/logger'
|
||||||
import { CONFIG, isEmailEnabled } from '../initializers/config'
|
import { CONFIG, isEmailEnabled } from '../initializers/config'
|
||||||
import { JobQueue } from './job-queue'
|
|
||||||
import { readFileSync } from 'fs-extra'
|
|
||||||
import { WEBSERVER } from '../initializers/constants'
|
import { WEBSERVER } from '../initializers/constants'
|
||||||
import {
|
import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
|
||||||
MCommentOwnerVideo,
|
import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
|
||||||
MVideo,
|
import { JobQueue } from './job-queue'
|
||||||
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'
|
|
||||||
const Email = require('email-templates')
|
const Email = require('email-templates')
|
||||||
|
|
||||||
class Emailer {
|
class Emailer {
|
||||||
|
@ -288,28 +282,70 @@ class Emailer {
|
||||||
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
|
||||||
}
|
}
|
||||||
|
|
||||||
addVideoAbuseModeratorsNotification (to: string[], parameters: {
|
addAbuseModeratorsNotification (to: string[], parameters: {
|
||||||
videoAbuse: VideoAbuse
|
abuse: Abuse
|
||||||
videoAbuseInstance: MVideoAbuseVideo
|
abuseInstance: MAbuseFull
|
||||||
reporter: string
|
reporter: string
|
||||||
}) {
|
}) {
|
||||||
const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id
|
const { abuse, abuseInstance, reporter } = parameters
|
||||||
const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
|
|
||||||
|
|
||||||
const emailPayload: EmailPayload = {
|
const action = {
|
||||||
template: 'video-abuse-new',
|
text: 'View report #' + abuse.id,
|
||||||
to,
|
url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
|
||||||
subject: `New video abuse report from ${parameters.reporter}`,
|
}
|
||||||
locals: {
|
|
||||||
videoUrl,
|
let emailPayload: EmailPayload
|
||||||
videoAbuseUrl,
|
|
||||||
videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(),
|
if (abuseInstance.VideoAbuse) {
|
||||||
videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(),
|
const video = abuseInstance.VideoAbuse.Video
|
||||||
videoAbuse: parameters.videoAbuse,
|
const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
|
||||||
reporter: parameters.reporter,
|
|
||||||
action: {
|
emailPayload = {
|
||||||
text: 'View report #' + parameters.videoAbuse.id,
|
template: 'video-abuse-new',
|
||||||
url: videoAbuseUrl
|
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)
|
mixin channel(channel)
|
||||||
- var handle = `${channel.name}@${channel.host}`
|
- 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
|
block content
|
||||||
p
|
p
|
||||||
| #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video "
|
| #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}video "
|
||||||
a(href=videoUrl) #{videoAbuse.video.name}
|
a(href=videoUrl) #{videoName}
|
||||||
| " by #[+channel(videoAbuse.video.channel)]
|
| " by #[+channel(videoChannel)]
|
||||||
if videoPublishedAt
|
if videoPublishedAt
|
||||||
| , published the #{videoPublishedAt}.
|
| , published the #{videoPublishedAt}.
|
||||||
else
|
else
|
||||||
| , uploaded the #{videoCreatedAt} but not yet published.
|
| , uploaded the #{videoCreatedAt} but not yet published.
|
||||||
p The reporter, #{reporter}, cited the following reason(s):
|
p The reporter, #{reporter}, cited the following reason(s):
|
||||||
blockquote #{videoAbuse.reason}
|
blockquote #{reason}
|
||||||
br(style="display: none;")
|
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 { VideoModel } from '../models/video/video'
|
||||||
import { VideoCommentModel } from '../models/video/video-comment'
|
import { VideoCommentModel } from '../models/video/video-comment'
|
||||||
import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
|
import { sendAbuse } from './activitypub/send/send-flag'
|
||||||
import { VideoCreate, VideoImportCreate } from '../../shared/models/videos'
|
import { Notifier } from './notifier'
|
||||||
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'
|
|
||||||
|
|
||||||
export type AcceptResult = {
|
export type AcceptResult = {
|
||||||
accepted: boolean
|
accepted: boolean
|
||||||
|
@ -73,6 +91,89 @@ function isPostImportVideoAccepted (object: {
|
||||||
return { accepted: true }
|
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 {
|
export {
|
||||||
isLocalVideoAccepted,
|
isLocalVideoAccepted,
|
||||||
isLocalVideoThreadAccepted,
|
isLocalVideoThreadAccepted,
|
||||||
|
@ -80,5 +181,48 @@ export {
|
||||||
isRemoteVideoCommentAccepted,
|
isRemoteVideoCommentAccepted,
|
||||||
isLocalVideoCommentReplyAccepted,
|
isLocalVideoCommentReplyAccepted,
|
||||||
isPreImportVideoAccepted,
|
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,
|
MUserWithNotificationSetting,
|
||||||
UserNotificationModelForApi
|
UserNotificationModelForApi
|
||||||
} from '@server/types/models/user'
|
} 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 { MVideoImportVideo } from '@server/types/models/video/video-import'
|
||||||
|
import { Abuse } from '@shared/models'
|
||||||
import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
|
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 { logger } from '../helpers/logger'
|
||||||
import { CONFIG } from '../initializers/config'
|
import { CONFIG } from '../initializers/config'
|
||||||
import { AccountBlocklistModel } from '../models/account/account-blocklist'
|
import { AccountBlocklistModel } from '../models/account/account-blocklist'
|
||||||
import { UserModel } from '../models/account/user'
|
import { UserModel } from '../models/account/user'
|
||||||
import { UserNotificationModel } from '../models/account/user-notification'
|
import { UserNotificationModel } from '../models/account/user-notification'
|
||||||
import { MAccountServer, MActorFollowFull } from '../types/models'
|
import { MAbuseFull, MAbuseVideo, MAccountServer, MActorFollowFull } from '../types/models'
|
||||||
import {
|
import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
|
||||||
MCommentOwnerVideo,
|
|
||||||
MVideoAbuseVideo,
|
|
||||||
MVideoAccountLight,
|
|
||||||
MVideoBlacklistLightVideo,
|
|
||||||
MVideoBlacklistVideo,
|
|
||||||
MVideoFullLight
|
|
||||||
} from '../types/models/video'
|
|
||||||
import { isBlockedByServerOrAccount } from './blocklist'
|
import { isBlockedByServerOrAccount } from './blocklist'
|
||||||
import { Emailer } from './emailer'
|
import { Emailer } from './emailer'
|
||||||
import { PeerTubeSocket } from './peertube-socket'
|
import { PeerTubeSocket } from './peertube-socket'
|
||||||
|
@ -78,9 +73,9 @@ class Notifier {
|
||||||
.catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
|
.catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void {
|
notifyOnNewAbuse (parameters: { abuse: Abuse, abuseInstance: MAbuseFull, reporter: string }): void {
|
||||||
this.notifyModeratorsOfNewVideoAbuse(parameters)
|
this.notifyModeratorsOfNewAbuse(parameters)
|
||||||
.catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err }))
|
.catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err }))
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
|
notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
|
||||||
|
@ -354,33 +349,37 @@ class Notifier {
|
||||||
return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
|
return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async notifyModeratorsOfNewVideoAbuse (parameters: {
|
private async notifyModeratorsOfNewAbuse (parameters: {
|
||||||
videoAbuse: VideoAbuse
|
abuse: Abuse
|
||||||
videoAbuseInstance: MVideoAbuseVideo
|
abuseInstance: MAbuseFull
|
||||||
reporter: string
|
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
|
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) {
|
function settingGetter (user: MUserWithNotificationSetting) {
|
||||||
return user.NotificationSetting.videoAbuseAsModerator
|
return user.NotificationSetting.videoAbuseAsModerator
|
||||||
}
|
}
|
||||||
|
|
||||||
async function notificationCreator (user: MUserWithNotificationSetting) {
|
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,
|
type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
videoAbuseId: parameters.videoAbuse.id
|
abuseId: abuse.id
|
||||||
})
|
})
|
||||||
notification.VideoAbuse = parameters.videoAbuseInstance
|
notification.Abuse = abuseInstance
|
||||||
|
|
||||||
return notification
|
return notification
|
||||||
}
|
}
|
||||||
|
|
||||||
function emailSender (emails: string[]) {
|
function emailSender (emails: string[]) {
|
||||||
return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters)
|
return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
|
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 './account'
|
||||||
export * from './blocklist'
|
export * from './blocklist'
|
||||||
export * from './oembed'
|
export * from './oembed'
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { checkSort, createSortableColumns } from './utils'
|
||||||
const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
|
const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
|
||||||
const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS)
|
const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS)
|
||||||
const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
|
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_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
|
||||||
const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
|
const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
|
||||||
const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_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 usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
|
||||||
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
|
const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
|
||||||
const jobsSortValidator = checkSort(SORTABLE_JOBS_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 videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
|
||||||
const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
|
const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
|
||||||
const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
|
const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
|
||||||
|
@ -52,7 +52,7 @@ const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COL
|
||||||
|
|
||||||
export {
|
export {
|
||||||
usersSortValidator,
|
usersSortValidator,
|
||||||
videoAbusesSortValidator,
|
abusesSortValidator,
|
||||||
videoChannelsSortValidator,
|
videoChannelsSortValidator,
|
||||||
videoImportsSortValidator,
|
videoImportsSortValidator,
|
||||||
videosSearchSortValidator,
|
videosSearchSortValidator,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
export * from './video-abuses'
|
|
||||||
export * from './video-blacklist'
|
export * from './video-blacklist'
|
||||||
export * from './video-captions'
|
export * from './video-captions'
|
||||||
export * from './video-channels'
|
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 * as Bluebird from 'bluebird'
|
||||||
import { literal, Op } from 'sequelize'
|
import { invert } from 'lodash'
|
||||||
|
import { literal, Op, WhereOptions } from 'sequelize'
|
||||||
import {
|
import {
|
||||||
AllowNull,
|
AllowNull,
|
||||||
BelongsTo,
|
BelongsTo,
|
||||||
|
@ -8,36 +9,35 @@ import {
|
||||||
DataType,
|
DataType,
|
||||||
Default,
|
Default,
|
||||||
ForeignKey,
|
ForeignKey,
|
||||||
|
HasOne,
|
||||||
Is,
|
Is,
|
||||||
Model,
|
Model,
|
||||||
Scopes,
|
Scopes,
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} 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 {
|
import {
|
||||||
VideoAbuseState,
|
Abuse,
|
||||||
VideoDetails,
|
AbuseObject,
|
||||||
VideoAbusePredefinedReasons,
|
AbusePredefinedReasons,
|
||||||
VideoAbusePredefinedReasonsString,
|
abusePredefinedReasonsMap,
|
||||||
videoAbusePredefinedReasonsMap
|
AbusePredefinedReasonsString,
|
||||||
} from '../../../shared'
|
AbuseState,
|
||||||
import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
|
AbuseVideoIs,
|
||||||
import { VideoAbuse } from '../../../shared/models/videos'
|
VideoAbuse
|
||||||
import {
|
} from '@shared/models'
|
||||||
isVideoAbuseModerationCommentValid,
|
import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter'
|
||||||
isVideoAbuseReasonValid,
|
import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
|
||||||
isVideoAbuseStateValid
|
import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
|
||||||
} from '../../helpers/custom-validators/video-abuses'
|
import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
|
||||||
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
|
|
||||||
import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models'
|
|
||||||
import { AccountModel } from '../account/account'
|
|
||||||
import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
|
import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
|
||||||
import { ThumbnailModel } from './thumbnail'
|
import { ThumbnailModel } from '../video/thumbnail'
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from '../video/video'
|
||||||
import { VideoBlacklistModel } from './video-blacklist'
|
import { VideoBlacklistModel } from '../video/video-blacklist'
|
||||||
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
|
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
|
||||||
import { invert } from 'lodash'
|
import { VideoAbuseModel } from './video-abuse'
|
||||||
|
import { VideoCommentAbuseModel } from './video-comment-abuse'
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
FOR_API = 'FOR_API'
|
FOR_API = 'FOR_API'
|
||||||
|
@ -49,20 +49,26 @@ export enum ScopeNames {
|
||||||
search?: string
|
search?: string
|
||||||
searchReporter?: string
|
searchReporter?: string
|
||||||
searchReportee?: string
|
searchReportee?: string
|
||||||
|
|
||||||
|
// video releated
|
||||||
searchVideo?: string
|
searchVideo?: string
|
||||||
searchVideoChannel?: string
|
searchVideoChannel?: string
|
||||||
|
videoIs?: AbuseVideoIs
|
||||||
|
|
||||||
// filters
|
// filters
|
||||||
id?: number
|
id?: number
|
||||||
predefinedReasonId?: number
|
predefinedReasonId?: number
|
||||||
|
filter?: AbuseFilter
|
||||||
|
|
||||||
state?: VideoAbuseState
|
state?: AbuseState
|
||||||
videoIs?: VideoAbuseVideoIs
|
|
||||||
|
|
||||||
// accountIds
|
// accountIds
|
||||||
serverAccountId: number
|
serverAccountId: number
|
||||||
userAccountId: number
|
userAccountId: number
|
||||||
}) => {
|
}) => {
|
||||||
|
const onlyBlacklisted = options.videoIs === 'blacklisted'
|
||||||
|
const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel)
|
||||||
|
|
||||||
const where = {
|
const where = {
|
||||||
reporterAccountId: {
|
reporterAccountId: {
|
||||||
[Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
|
[Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
|
||||||
|
@ -70,33 +76,36 @@ export enum ScopeNames {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.search) {
|
if (options.search) {
|
||||||
|
const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%')
|
||||||
|
|
||||||
Object.assign(where, {
|
Object.assign(where, {
|
||||||
[Op.or]: [
|
[Op.or]: [
|
||||||
{
|
{
|
||||||
[Op.and]: [
|
[Op.and]: [
|
||||||
{ videoId: { [Op.not]: null } },
|
{ '$VideoAbuse.videoId$': { [Op.not]: null } },
|
||||||
searchAttribute(options.search, '$Video.name$')
|
searchAttribute(options.search, '$VideoAbuse.Video.name$')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[Op.and]: [
|
[Op.and]: [
|
||||||
{ videoId: { [Op.not]: null } },
|
{ '$VideoAbuse.videoId$': { [Op.not]: null } },
|
||||||
searchAttribute(options.search, '$Video.VideoChannel.name$')
|
searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$')
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[Op.and]: [
|
[Op.and]: [
|
||||||
{ deletedVideo: { [Op.not]: null } },
|
{ '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
|
||||||
{ deletedVideo: searchAttribute(options.search, 'name') }
|
literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`)
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
[Op.and]: [
|
[Op.and]: [
|
||||||
{ deletedVideo: { [Op.not]: null } },
|
{ '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
|
||||||
{ deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
|
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') {
|
if (options.videoIs === 'deleted') {
|
||||||
Object.assign(where, {
|
Object.assign(where, {
|
||||||
deletedVideo: {
|
'$VideoAbuse.deletedVideo$': {
|
||||||
[Op.not]: null
|
[Op.not]: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -120,8 +129,6 @@ export enum ScopeNames {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onlyBlacklisted = options.videoIs === 'blacklisted'
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attributes: {
|
attributes: {
|
||||||
include: [
|
include: [
|
||||||
|
@ -131,7 +138,7 @@ export enum ScopeNames {
|
||||||
'(' +
|
'(' +
|
||||||
'SELECT count(*) ' +
|
'SELECT count(*) ' +
|
||||||
'FROM "videoAbuse" ' +
|
'FROM "videoAbuse" ' +
|
||||||
'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
|
'WHERE "videoId" = "VideoAbuse"."videoId" ' +
|
||||||
')'
|
')'
|
||||||
),
|
),
|
||||||
'countReportsForVideo'
|
'countReportsForVideo'
|
||||||
|
@ -146,7 +153,7 @@ export enum ScopeNames {
|
||||||
'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
|
'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
|
||||||
'FROM "videoAbuse" ' +
|
'FROM "videoAbuse" ' +
|
||||||
') t ' +
|
') t ' +
|
||||||
'WHERE t.id = "VideoAbuseModel".id ' +
|
'WHERE t.id = "VideoAbuse".id' +
|
||||||
')'
|
')'
|
||||||
),
|
),
|
||||||
'nthReportForVideo'
|
'nthReportForVideo'
|
||||||
|
@ -159,7 +166,7 @@ export enum ScopeNames {
|
||||||
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
|
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
|
||||||
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
||||||
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
|
'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
|
||||||
'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
|
'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
|
||||||
')'
|
')'
|
||||||
),
|
),
|
||||||
'countReportsForReporter__video'
|
'countReportsForReporter__video'
|
||||||
|
@ -169,7 +176,7 @@ export enum ScopeNames {
|
||||||
'(' +
|
'(' +
|
||||||
'SELECT count(DISTINCT "videoAbuse"."id") ' +
|
'SELECT count(DISTINCT "videoAbuse"."id") ' +
|
||||||
'FROM "videoAbuse" ' +
|
'FROM "videoAbuse" ' +
|
||||||
`WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
|
`WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` +
|
||||||
')'
|
')'
|
||||||
),
|
),
|
||||||
'countReportsForReporter__deletedVideo'
|
'countReportsForReporter__deletedVideo'
|
||||||
|
@ -182,8 +189,8 @@ export enum ScopeNames {
|
||||||
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
|
'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
|
||||||
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
||||||
'INNER JOIN "account" ON ' +
|
'INNER JOIN "account" ON ' +
|
||||||
'"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' +
|
'"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' +
|
||||||
`OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
|
`OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
|
||||||
')'
|
')'
|
||||||
),
|
),
|
||||||
'countReportsForReportee__video'
|
'countReportsForReportee__video'
|
||||||
|
@ -193,9 +200,9 @@ export enum ScopeNames {
|
||||||
'(' +
|
'(' +
|
||||||
'SELECT count(DISTINCT "videoAbuse"."id") ' +
|
'SELECT count(DISTINCT "videoAbuse"."id") ' +
|
||||||
'FROM "videoAbuse" ' +
|
'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) = ` +
|
`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'
|
'countReportsForReportee__deletedVideo'
|
||||||
|
@ -204,32 +211,47 @@ export enum ScopeNames {
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: AccountModel,
|
model: AccountModel.scope(AccountScopeNames.SUMMARY),
|
||||||
|
as: 'ReporterAccount',
|
||||||
required: true,
|
required: true,
|
||||||
where: searchAttribute(options.searchReporter, 'name')
|
where: searchAttribute(options.searchReporter, 'name')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
model: VideoModel,
|
model: AccountModel.scope(AccountScopeNames.SUMMARY),
|
||||||
required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
|
as: 'FlaggedAccount',
|
||||||
where: searchAttribute(options.searchVideo, 'name'),
|
required: true,
|
||||||
|
where: searchAttribute(options.searchReportee, 'name')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model: VideoAbuseModel,
|
||||||
|
required: options.filter === 'video' || !!options.videoIs || videoRequired,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: ThumbnailModel
|
model: VideoModel,
|
||||||
},
|
required: videoRequired,
|
||||||
{
|
where: searchAttribute(options.searchVideo, 'name'),
|
||||||
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
|
|
||||||
where: searchAttribute(options.searchVideoChannel, 'name'),
|
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: AccountModel,
|
model: ThumbnailModel
|
||||||
where: searchAttribute(options.searchReportee, 'name')
|
},
|
||||||
|
{
|
||||||
|
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({
|
@Table({
|
||||||
tableName: 'videoAbuse',
|
tableName: 'abuse',
|
||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
fields: [ 'videoId' ]
|
fields: [ 'reporterAccountId' ]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fields: [ 'reporterAccountId' ]
|
fields: [ 'flaggedAccountId' ]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
export class AbuseModel extends Model<AbuseModel> {
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Default(null)
|
@Default(null)
|
||||||
@Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
|
@Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
|
||||||
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
|
@Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
|
||||||
reason: string
|
reason: string
|
||||||
|
|
||||||
@AllowNull(false)
|
@AllowNull(false)
|
||||||
@Default(null)
|
@Default(null)
|
||||||
@Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
|
@Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
|
||||||
@Column
|
@Column
|
||||||
state: VideoAbuseState
|
state: AbuseState
|
||||||
|
|
||||||
@AllowNull(true)
|
@AllowNull(true)
|
||||||
@Default(null)
|
@Default(null)
|
||||||
@Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
|
@Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
|
||||||
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
|
@Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
|
||||||
moderationComment: string
|
moderationComment: string
|
||||||
|
|
||||||
@AllowNull(true)
|
|
||||||
@Default(null)
|
|
||||||
@Column(DataType.JSONB)
|
|
||||||
deletedVideo: VideoDetails
|
|
||||||
|
|
||||||
@AllowNull(true)
|
@AllowNull(true)
|
||||||
@Default(null)
|
@Default(null)
|
||||||
@Column(DataType.ARRAY(DataType.INTEGER))
|
@Column(DataType.ARRAY(DataType.INTEGER))
|
||||||
predefinedReasons: VideoAbusePredefinedReasons[]
|
predefinedReasons: AbusePredefinedReasons[]
|
||||||
|
|
||||||
@AllowNull(true)
|
|
||||||
@Default(null)
|
|
||||||
@Column
|
|
||||||
startAt: number
|
|
||||||
|
|
||||||
@AllowNull(true)
|
|
||||||
@Default(null)
|
|
||||||
@Column
|
|
||||||
endAt: number
|
|
||||||
|
|
||||||
@CreatedAt
|
@CreatedAt
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
|
@ -301,36 +308,65 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
|
|
||||||
@BelongsTo(() => AccountModel, {
|
@BelongsTo(() => AccountModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
|
name: 'reporterAccountId',
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
|
as: 'ReporterAccount',
|
||||||
onDelete: 'set null'
|
onDelete: 'set null'
|
||||||
})
|
})
|
||||||
Account: AccountModel
|
ReporterAccount: AccountModel
|
||||||
|
|
||||||
@ForeignKey(() => VideoModel)
|
@ForeignKey(() => AccountModel)
|
||||||
@Column
|
@Column
|
||||||
videoId: number
|
flaggedAccountId: number
|
||||||
|
|
||||||
@BelongsTo(() => VideoModel, {
|
@BelongsTo(() => AccountModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
|
name: 'flaggedAccountId',
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
|
as: 'FlaggedAccount',
|
||||||
onDelete: 'set null'
|
onDelete: 'set null'
|
||||||
})
|
})
|
||||||
Video: VideoModel
|
FlaggedAccount: AccountModel
|
||||||
|
|
||||||
static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
|
@HasOne(() => VideoCommentAbuseModel, {
|
||||||
const videoAttributes = {}
|
foreignKey: {
|
||||||
if (videoId) videoAttributes['videoId'] = videoId
|
name: 'abuseId',
|
||||||
if (uuid) videoAttributes['deletedVideo'] = { uuid }
|
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 = {
|
const query = {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: VideoAbuseModel,
|
||||||
|
required: true,
|
||||||
|
where: videoWhere
|
||||||
|
}
|
||||||
|
],
|
||||||
where: {
|
where: {
|
||||||
id,
|
id
|
||||||
...videoAttributes
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return VideoAbuseModel.findOne(query)
|
return AbuseModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static listForApi (parameters: {
|
static listForApi (parameters: {
|
||||||
|
@ -338,13 +374,15 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
count: number
|
count: number
|
||||||
sort: string
|
sort: string
|
||||||
|
|
||||||
|
filter?: AbuseFilter
|
||||||
|
|
||||||
serverAccountId: number
|
serverAccountId: number
|
||||||
user?: MUserAccountId
|
user?: MUserAccountId
|
||||||
|
|
||||||
id?: number
|
id?: number
|
||||||
predefinedReason?: VideoAbusePredefinedReasonsString
|
predefinedReason?: AbusePredefinedReasonsString
|
||||||
state?: VideoAbuseState
|
state?: AbuseState
|
||||||
videoIs?: VideoAbuseVideoIs
|
videoIs?: AbuseVideoIs
|
||||||
|
|
||||||
search?: string
|
search?: string
|
||||||
searchReporter?: string
|
searchReporter?: string
|
||||||
|
@ -364,24 +402,26 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
predefinedReason,
|
predefinedReason,
|
||||||
searchReportee,
|
searchReportee,
|
||||||
searchVideo,
|
searchVideo,
|
||||||
|
filter,
|
||||||
searchVideoChannel,
|
searchVideoChannel,
|
||||||
searchReporter,
|
searchReporter,
|
||||||
id
|
id
|
||||||
} = parameters
|
} = parameters
|
||||||
|
|
||||||
const userAccountId = user ? user.Account.id : undefined
|
const userAccountId = user ? user.Account.id : undefined
|
||||||
const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
|
const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
offset: start,
|
offset: start,
|
||||||
limit: count,
|
limit: count,
|
||||||
order: getSort(sort),
|
order: getSort(sort),
|
||||||
col: 'VideoAbuseModel.id',
|
col: 'AbuseModel.id',
|
||||||
distinct: true
|
distinct: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
id,
|
id,
|
||||||
|
filter,
|
||||||
predefinedReasonId,
|
predefinedReasonId,
|
||||||
search,
|
search,
|
||||||
state,
|
state,
|
||||||
|
@ -394,7 +434,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
userAccountId
|
userAccountId
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoAbuseModel
|
return AbuseModel
|
||||||
.scope([
|
.scope([
|
||||||
{ method: [ ScopeNames.FOR_API, filters ] }
|
{ method: [ ScopeNames.FOR_API, filters ] }
|
||||||
])
|
])
|
||||||
|
@ -404,8 +444,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
|
toFormattedJSON (this: MAbuseFormattable): Abuse {
|
||||||
const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
|
const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
|
||||||
const countReportsForVideo = this.get('countReportsForVideo') as number
|
const countReportsForVideo = this.get('countReportsForVideo') as number
|
||||||
const nthReportForVideo = this.get('nthReportForVideo') as number
|
const nthReportForVideo = this.get('nthReportForVideo') as number
|
||||||
const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
|
const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
|
||||||
|
@ -413,51 +453,70 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
|
const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
|
||||||
const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
|
const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
|
||||||
|
|
||||||
const video = this.Video
|
let video: VideoAbuse
|
||||||
? this.Video
|
|
||||||
: this.deletedVideo
|
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 {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
reason: this.reason,
|
reason: this.reason,
|
||||||
predefinedReasons,
|
predefinedReasons,
|
||||||
reporterAccount: this.Account.toFormattedJSON(),
|
|
||||||
|
reporterAccount: this.ReporterAccount.toFormattedJSON(),
|
||||||
|
|
||||||
state: {
|
state: {
|
||||||
id: this.state,
|
id: this.state,
|
||||||
label: VideoAbuseModel.getStateLabel(this.state)
|
label: AbuseModel.getStateLabel(this.state)
|
||||||
},
|
},
|
||||||
|
|
||||||
moderationComment: this.moderationComment,
|
moderationComment: this.moderationComment,
|
||||||
video: {
|
|
||||||
id: video.id,
|
video,
|
||||||
uuid: video.uuid,
|
comment: null,
|
||||||
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
|
|
||||||
},
|
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
startAt: this.startAt,
|
|
||||||
endAt: this.endAt,
|
|
||||||
count: countReportsForVideo || 0,
|
count: countReportsForVideo || 0,
|
||||||
nth: nthReportForVideo || 0,
|
nth: nthReportForVideo || 0,
|
||||||
countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
|
countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
|
||||||
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 {
|
toActivityPubObject (this: MAbuseAP): AbuseObject {
|
||||||
const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
|
const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
|
||||||
|
|
||||||
const startAt = this.startAt
|
const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
|
||||||
const endAt = this.endAt
|
|
||||||
|
const startAt = this.VideoAbuse?.startAt
|
||||||
|
const endAt = this.VideoAbuse?.endAt
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Flag' as 'Flag',
|
type: 'Flag' as 'Flag',
|
||||||
content: this.reason,
|
content: this.reason,
|
||||||
object: this.Video.url,
|
object,
|
||||||
tag: predefinedReasons.map(r => ({
|
tag: predefinedReasons.map(r => ({
|
||||||
type: 'Hashtag' as 'Hashtag',
|
type: 'Hashtag' as 'Hashtag',
|
||||||
name: r
|
name: r
|
||||||
|
@ -468,12 +527,12 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getStateLabel (id: number) {
|
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 || [])
|
return (predefinedReasons || [])
|
||||||
.filter(r => r in VideoAbusePredefinedReasons)
|
.filter(r => r in AbusePredefinedReasons)
|
||||||
.map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
|
.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 * 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 { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
|
||||||
|
import { AccountBlock } from '../../../shared/models'
|
||||||
import { ActorModel } from '../activitypub/actor'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
import { ServerModel } from '../server/server'
|
import { ServerModel } from '../server/server'
|
||||||
|
import { getSort, searchAttribute } from '../utils'
|
||||||
|
import { AccountModel } from './account'
|
||||||
|
|
||||||
enum ScopeNames {
|
enum ScopeNames {
|
||||||
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
|
WITH_ACCOUNTS = 'WITH_ACCOUNTS'
|
||||||
|
|
|
@ -388,6 +388,10 @@ export class AccountModel extends Model<AccountModel> {
|
||||||
.findAll(query)
|
.findAll(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getClientUrl () {
|
||||||
|
return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
|
||||||
|
}
|
||||||
|
|
||||||
toFormattedJSON (this: MAccountFormattable): Account {
|
toFormattedJSON (this: MAccountFormattable): Account {
|
||||||
const actor = this.Actor.toFormattedJSON()
|
const actor = this.Actor.toFormattedJSON()
|
||||||
const account = {
|
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 { 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 { UserNotification, UserNotificationType } from '../../../shared'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
|
||||||
import { isBooleanValid } from '../../helpers/custom-validators/misc'
|
import { isBooleanValid } from '../../helpers/custom-validators/misc'
|
||||||
import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
|
import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
|
||||||
import { UserModel } from './user'
|
import { AbuseModel } from '../abuse/abuse'
|
||||||
import { VideoModel } from '../video/video'
|
import { VideoAbuseModel } from '../abuse/video-abuse'
|
||||||
import { VideoCommentModel } from '../video/video-comment'
|
import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
|
||||||
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 { ActorModel } from '../activitypub/actor'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
import { ActorFollowModel } from '../activitypub/actor-follow'
|
import { ActorFollowModel } from '../activitypub/actor-follow'
|
||||||
import { AvatarModel } from '../avatar/avatar'
|
import { AvatarModel } from '../avatar/avatar'
|
||||||
import { ServerModel } from '../server/server'
|
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 {
|
enum ScopeNames {
|
||||||
WITH_ALL = 'WITH_ALL'
|
WITH_ALL = 'WITH_ALL'
|
||||||
|
@ -87,9 +89,41 @@ function buildAccountInclude (required: boolean, withActor = false) {
|
||||||
|
|
||||||
{
|
{
|
||||||
attributes: [ 'id' ],
|
attributes: [ 'id' ],
|
||||||
model: VideoAbuseModel.unscoped(),
|
model: AbuseModel.unscoped(),
|
||||||
required: false,
|
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: {
|
where: {
|
||||||
videoAbuseId: {
|
abuseId: {
|
||||||
[Op.ne]: null
|
[Op.ne]: null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -276,17 +310,17 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
|
||||||
})
|
})
|
||||||
Comment: VideoCommentModel
|
Comment: VideoCommentModel
|
||||||
|
|
||||||
@ForeignKey(() => VideoAbuseModel)
|
@ForeignKey(() => AbuseModel)
|
||||||
@Column
|
@Column
|
||||||
videoAbuseId: number
|
abuseId: number
|
||||||
|
|
||||||
@BelongsTo(() => VideoAbuseModel, {
|
@BelongsTo(() => AbuseModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
onDelete: 'cascade'
|
onDelete: 'cascade'
|
||||||
})
|
})
|
||||||
VideoAbuse: VideoAbuseModel
|
Abuse: AbuseModel
|
||||||
|
|
||||||
@ForeignKey(() => VideoBlacklistModel)
|
@ForeignKey(() => VideoBlacklistModel)
|
||||||
@Column
|
@Column
|
||||||
|
@ -397,10 +431,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
|
||||||
video: this.formatVideo(this.Comment.Video)
|
video: this.formatVideo(this.Comment.Video)
|
||||||
} : undefined
|
} : undefined
|
||||||
|
|
||||||
const videoAbuse = this.VideoAbuse ? {
|
const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
|
||||||
id: this.VideoAbuse.id,
|
|
||||||
video: this.formatVideo(this.VideoAbuse.Video)
|
|
||||||
} : undefined
|
|
||||||
|
|
||||||
const videoBlacklist = this.VideoBlacklist ? {
|
const videoBlacklist = this.VideoBlacklist ? {
|
||||||
id: this.VideoBlacklist.id,
|
id: this.VideoBlacklist.id,
|
||||||
|
@ -439,7 +470,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
|
||||||
video,
|
video,
|
||||||
videoImport,
|
videoImport,
|
||||||
comment,
|
comment,
|
||||||
videoAbuse,
|
abuse,
|
||||||
videoBlacklist,
|
videoBlacklist,
|
||||||
account,
|
account,
|
||||||
actorFollow,
|
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 (
|
formatActor (
|
||||||
this: UserNotificationModelForApi,
|
this: UserNotificationModelForApi,
|
||||||
accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
|
accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
|
||||||
|
|
|
@ -19,7 +19,7 @@ import {
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} 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 { User, UserRole } from '../../../shared/models/users'
|
||||||
import {
|
import {
|
||||||
isNoInstanceConfigWarningModal,
|
isNoInstanceConfigWarningModal,
|
||||||
|
@ -169,7 +169,7 @@ enum ScopeNames {
|
||||||
`SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
|
`SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
|
||||||
'FROM (' +
|
'FROM (' +
|
||||||
'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
|
'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" ' +
|
'FROM "videoAbuse" ' +
|
||||||
'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
|
'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
|
||||||
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
|
'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 * as Bluebird from 'bluebird'
|
||||||
import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
|
|
||||||
import { Op } from 'sequelize'
|
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 {
|
enum ScopeNames {
|
||||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import * as Bluebird from 'bluebird'
|
import * as Bluebird from 'bluebird'
|
||||||
|
import { remove } from 'fs-extra'
|
||||||
import { maxBy, minBy, pick } from 'lodash'
|
import { maxBy, minBy, pick } from 'lodash'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
|
import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
|
||||||
|
@ -23,10 +24,18 @@ import {
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} 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 { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
|
||||||
import { Video, VideoDetails } from '../../../shared/models/videos'
|
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 { 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 { peertubeTruncate } from '../../helpers/core-utils'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||||
import { isBooleanValid } from '../../helpers/custom-validators/misc'
|
import { isBooleanValid } from '../../helpers/custom-validators/misc'
|
||||||
|
@ -43,6 +52,7 @@ import {
|
||||||
} from '../../helpers/custom-validators/videos'
|
} from '../../helpers/custom-validators/videos'
|
||||||
import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
|
import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
|
import { CONFIG } from '../../initializers/config'
|
||||||
import {
|
import {
|
||||||
ACTIVITY_PUB,
|
ACTIVITY_PUB,
|
||||||
API_VERSION,
|
API_VERSION,
|
||||||
|
@ -59,40 +69,6 @@ import {
|
||||||
WEBSERVER
|
WEBSERVER
|
||||||
} from '../../initializers/constants'
|
} from '../../initializers/constants'
|
||||||
import { sendDeleteVideo } from '../../lib/activitypub/send'
|
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 {
|
import {
|
||||||
MChannel,
|
MChannel,
|
||||||
MChannelAccountDefault,
|
MChannelAccountDefault,
|
||||||
|
@ -118,15 +94,39 @@ import {
|
||||||
MVideoWithFile,
|
MVideoWithFile,
|
||||||
MVideoWithRights
|
MVideoWithRights
|
||||||
} from '../../types/models'
|
} from '../../types/models'
|
||||||
import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
|
|
||||||
import { MThumbnail } from '../../types/models/video/thumbnail'
|
import { MThumbnail } from '../../types/models/video/thumbnail'
|
||||||
import { VideoFile } from '@shared/models/videos/video-file.model'
|
import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
|
||||||
import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
import { VideoAbuseModel } from '../abuse/video-abuse'
|
||||||
import { ModelCache } from '@server/models/model-cache'
|
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 { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
|
||||||
import { buildNSFWFilter } from '@server/helpers/express-utils'
|
import { VideoShareModel } from './video-share'
|
||||||
import { getServerActor } from '@server/models/application/application'
|
import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
|
||||||
import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video"
|
import { VideoTagModel } from './video-tag'
|
||||||
|
import { VideoViewModel } from './video-view'
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
|
import { AbuseState, VideoAbuseCreate } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
createUser,
|
createUser,
|
||||||
|
@ -20,7 +20,8 @@ import {
|
||||||
checkBadSortPagination,
|
checkBadSortPagination,
|
||||||
checkBadStartPagination
|
checkBadStartPagination
|
||||||
} from '../../../../shared/extra-utils/requests/check-api-params'
|
} 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 () {
|
describe('Test video abuses API validators', function () {
|
||||||
let server: ServerInfo
|
let server: ServerInfo
|
||||||
|
@ -136,7 +137,7 @@ describe('Test video abuses API validators', function () {
|
||||||
const fields = { reason: 'my super reason' }
|
const fields = { reason: 'my super reason' }
|
||||||
|
|
||||||
const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
|
const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
|
||||||
videoAbuseId = res.body.videoAbuse.id
|
videoAbuseId = res.body.abuse.id
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a wrong predefined reason', async function () {
|
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 () {
|
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)
|
await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import 'mocha'
|
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 {
|
import {
|
||||||
addVideoCommentThread,
|
addVideoCommentThread,
|
||||||
blockUser,
|
blockUser,
|
||||||
|
@ -937,7 +937,7 @@ describe('Test users', function () {
|
||||||
expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
|
expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
|
||||||
expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created
|
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)
|
await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body)
|
||||||
|
|
||||||
const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
|
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 */
|
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||||
|
|
||||||
import * as chai from 'chai'
|
|
||||||
import 'mocha'
|
import 'mocha'
|
||||||
import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos'
|
import * as chai from 'chai'
|
||||||
|
import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupTests,
|
cleanupTests,
|
||||||
|
createUser,
|
||||||
deleteVideoAbuse,
|
deleteVideoAbuse,
|
||||||
flushAndRunMultipleServers,
|
flushAndRunMultipleServers,
|
||||||
getVideoAbusesList,
|
getVideoAbusesList,
|
||||||
getVideosList,
|
getVideosList,
|
||||||
|
removeVideo,
|
||||||
reportVideoAbuse,
|
reportVideoAbuse,
|
||||||
ServerInfo,
|
ServerInfo,
|
||||||
setAccessTokensToServers,
|
setAccessTokensToServers,
|
||||||
updateVideoAbuse,
|
updateVideoAbuse,
|
||||||
uploadVideo,
|
uploadVideo,
|
||||||
removeVideo,
|
|
||||||
createUser,
|
|
||||||
userLogin
|
userLogin
|
||||||
} from '../../../../shared/extra-utils/index'
|
} from '../../../../shared/extra-utils/index'
|
||||||
import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
|
import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
|
||||||
|
@ -29,9 +29,11 @@ import {
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
|
// FIXME: deprecated in 2.3. Remove this controller
|
||||||
|
|
||||||
describe('Test video abuses', function () {
|
describe('Test video abuses', function () {
|
||||||
let servers: ServerInfo[] = []
|
let servers: ServerInfo[] = []
|
||||||
let abuseServer2: VideoAbuse
|
let abuseServer2: Abuse
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
this.timeout(50000)
|
this.timeout(50000)
|
||||||
|
@ -95,7 +97,7 @@ describe('Test video abuses', function () {
|
||||||
expect(res1.body.data).to.be.an('array')
|
expect(res1.body.data).to.be.an('array')
|
||||||
expect(res1.body.data.length).to.equal(1)
|
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.reason).to.equal('my super bad reason')
|
||||||
expect(abuse.reporterAccount.name).to.equal('root')
|
expect(abuse.reporterAccount.name).to.equal('root')
|
||||||
expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
|
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).to.be.an('array')
|
||||||
expect(res1.body.data.length).to.equal(2)
|
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.reason).to.equal('my super bad reason')
|
||||||
expect(abuse1.reporterAccount.name).to.equal('root')
|
expect(abuse1.reporterAccount.name).to.equal('root')
|
||||||
expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
|
expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
|
||||||
expect(abuse1.video.id).to.equal(servers[0].video.id)
|
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.state.label).to.equal('Pending')
|
||||||
expect(abuse1.moderationComment).to.be.null
|
expect(abuse1.moderationComment).to.be.null
|
||||||
expect(abuse1.count).to.equal(1)
|
expect(abuse1.count).to.equal(1)
|
||||||
expect(abuse1.nth).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.reason).to.equal('my super bad reason 2')
|
||||||
expect(abuse2.reporterAccount.name).to.equal('root')
|
expect(abuse2.reporterAccount.name).to.equal('root')
|
||||||
expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
|
expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
|
||||||
expect(abuse2.video.id).to.equal(servers[1].video.id)
|
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.state.label).to.equal('Pending')
|
||||||
expect(abuse2.moderationComment).to.be.null
|
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.reason).to.equal('my super bad reason 2')
|
||||||
expect(abuseServer2.reporterAccount.name).to.equal('root')
|
expect(abuseServer2.reporterAccount.name).to.equal('root')
|
||||||
expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
|
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.state.label).to.equal('Pending')
|
||||||
expect(abuseServer2.moderationComment).to.be.null
|
expect(abuseServer2.moderationComment).to.be.null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should update the state of a video abuse', async function () {
|
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)
|
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 })
|
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 () {
|
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)
|
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 })
|
const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
|
||||||
expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED)
|
expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
|
||||||
expect(res.body.data[0].moderationComment).to.equal('It is valid')
|
expect(res.body.data[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.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")
|
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.id).to.equal(abuseServer2.video.id, "wrong video id")
|
||||||
expect(abuse.video.channel).to.exist
|
expect(abuse.video.channel).to.exist
|
||||||
expect(abuse.video.deleted).to.be.true
|
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 })
|
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) {
|
if (abuse.video.id === video3.id) {
|
||||||
expect(abuse.count).to.equal(1, "wrong reports count for video 3")
|
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")
|
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)
|
this.timeout(10000)
|
||||||
|
|
||||||
const reason5 = 'my super bad reason 5'
|
const reason5 = 'my super bad reason 5'
|
||||||
const predefinedReasons5: VideoAbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
|
const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
|
||||||
const createdAbuse = (await reportVideoAbuse(
|
const createdAbuse = (await reportVideoAbuse(
|
||||||
servers[0].url,
|
servers[0].url,
|
||||||
servers[0].accessToken,
|
servers[0].accessToken,
|
||||||
|
@ -304,16 +306,16 @@ describe('Test video abuses', function () {
|
||||||
predefinedReasons5,
|
predefinedReasons5,
|
||||||
1,
|
1,
|
||||||
5
|
5
|
||||||
)).body.videoAbuse as VideoAbuse
|
)).body.abuse
|
||||||
|
|
||||||
const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
|
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.reason).to.equals(reason5)
|
||||||
expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
|
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.video.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.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)
|
const res = await getVideoAbusesList(options)
|
||||||
|
|
||||||
return res.body.data as VideoAbuse[]
|
return res.body.data as Abuse[]
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(await list({ id: 56 })).to.have.lengthOf(0)
|
expect(await list({ 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: 'user2' })).to.have.lengthOf(1)
|
||||||
expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
|
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({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
|
||||||
|
|
||||||
expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
|
expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
|
||||||
expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
|
expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
|
||||||
|
|
||||||
expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0)
|
expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
|
||||||
expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(6)
|
expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
|
||||||
|
|
||||||
expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
|
expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
|
||||||
expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
|
expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export * from './account'
|
export * from './account'
|
||||||
|
export * from './moderation'
|
||||||
export * from './oauth'
|
export * from './oauth'
|
||||||
export * from './server'
|
export * from './server'
|
||||||
export * from './user'
|
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 { PickWith, PickWithOpt } from '@shared/core-utils'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { AbuseModel } from '../../../models/abuse/abuse'
|
||||||
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 { AccountModel } from '../../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
import { UserNotificationModel } from '../../../models/account/user-notification'
|
||||||
import { VideoAbuseModel } from '../../../models/video/video-abuse'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
|
|
||||||
import { VideoImportModel } from '../../../models/video/video-import'
|
|
||||||
import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
|
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>
|
type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M>
|
||||||
|
|
||||||
|
@ -47,6 +49,18 @@ export module UserNotificationIncludes {
|
||||||
Pick<VideoAbuseModel, 'id'> &
|
Pick<VideoAbuseModel, 'id'> &
|
||||||
PickWith<VideoAbuseModel, 'Video', VideoInclude>
|
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 =
|
export type VideoBlacklistInclude =
|
||||||
Pick<VideoBlacklistModel, 'id'> &
|
Pick<VideoBlacklistModel, 'id'> &
|
||||||
PickWith<VideoAbuseModel, 'Video', VideoInclude>
|
PickWith<VideoAbuseModel, 'Video', VideoInclude>
|
||||||
|
@ -76,7 +90,7 @@ export module UserNotificationIncludes {
|
||||||
// ############################################################################
|
// ############################################################################
|
||||||
|
|
||||||
export type MUserNotification =
|
export type MUserNotification =
|
||||||
Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' |
|
Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
|
||||||
'VideoImport' | 'Account' | 'ActorFollow'>
|
'VideoImport' | 'Account' | 'ActorFollow'>
|
||||||
|
|
||||||
// ############################################################################
|
// ############################################################################
|
||||||
|
@ -85,7 +99,7 @@ export type UserNotificationModelForApi =
|
||||||
MUserNotification &
|
MUserNotification &
|
||||||
Use<'Video', UserNotificationIncludes.VideoIncludeChannel> &
|
Use<'Video', UserNotificationIncludes.VideoIncludeChannel> &
|
||||||
Use<'Comment', UserNotificationIncludes.VideoCommentInclude> &
|
Use<'Comment', UserNotificationIncludes.VideoCommentInclude> &
|
||||||
Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> &
|
Use<'Abuse', UserNotificationIncludes.AbuseInclude> &
|
||||||
Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
|
Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
|
||||||
Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
|
Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
|
||||||
Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
|
Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
|
||||||
|
|
|
@ -2,7 +2,6 @@ export * from './schedule-video-update'
|
||||||
export * from './tag'
|
export * from './tag'
|
||||||
export * from './thumbnail'
|
export * from './thumbnail'
|
||||||
export * from './video'
|
export * from './video'
|
||||||
export * from './video-abuse'
|
|
||||||
export * from './video-blacklist'
|
export * from './video-blacklist'
|
||||||
export * from './video-caption'
|
export * from './video-caption'
|
||||||
export * from './video-change-ownership'
|
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 { RegisterServerAuthExternalOptions } from '@server/types'
|
||||||
import {
|
import {
|
||||||
|
MAbuse,
|
||||||
MAccountBlocklist,
|
MAccountBlocklist,
|
||||||
MActorUrl,
|
MActorUrl,
|
||||||
MStreamingPlaylist,
|
MStreamingPlaylist,
|
||||||
|
@ -26,7 +27,6 @@ import {
|
||||||
MComment,
|
MComment,
|
||||||
MCommentOwnerVideoReply,
|
MCommentOwnerVideoReply,
|
||||||
MUserDefault,
|
MUserDefault,
|
||||||
MVideoAbuse,
|
|
||||||
MVideoBlacklist,
|
MVideoBlacklist,
|
||||||
MVideoCaptionVideo,
|
MVideoCaptionVideo,
|
||||||
MVideoFullLight,
|
MVideoFullLight,
|
||||||
|
@ -77,7 +77,7 @@ declare module 'express' {
|
||||||
|
|
||||||
videoCaption?: MVideoCaptionVideo
|
videoCaption?: MVideoCaptionVideo
|
||||||
|
|
||||||
videoAbuse?: MVideoAbuse
|
abuse?: MAbuse
|
||||||
|
|
||||||
videoStreamingPlaylist?: MStreamingPlaylist
|
videoStreamingPlaylist?: MStreamingPlaylist
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ export * from './videos/services'
|
||||||
export * from './videos/video-playlists'
|
export * from './videos/video-playlists'
|
||||||
export * from './users/users'
|
export * from './users/users'
|
||||||
export * from './users/accounts'
|
export * from './users/accounts'
|
||||||
|
export * from './moderation/abuses'
|
||||||
export * from './videos/video-abuses'
|
export * from './videos/video-abuses'
|
||||||
export * from './videos/video-blacklist'
|
export * from './videos/video-blacklist'
|
||||||
export * from './videos/video-captions'
|
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).to.not.be.undefined
|
||||||
expect(notification.type).to.equal(notificationType)
|
expect(notification.type).to.equal(notificationType)
|
||||||
|
|
||||||
expect(notification.videoAbuse.id).to.be.a('number')
|
expect(notification.abuse.id).to.be.a('number')
|
||||||
checkVideo(notification.videoAbuse.video, videoName, videoUUID)
|
checkVideo(notification.abuse.video, videoName, videoUUID)
|
||||||
} else {
|
} else {
|
||||||
expect(notification).to.satisfy((n: UserNotification) => {
|
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 * as request from 'supertest'
|
||||||
import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
|
import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
|
||||||
import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests'
|
import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
|
||||||
import { VideoAbuseState, VideoAbusePredefinedReasonsString } from '@shared/models'
|
|
||||||
import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
|
// FIXME: deprecated in 2.3. Remove this file
|
||||||
|
|
||||||
function reportVideoAbuse (
|
function reportVideoAbuse (
|
||||||
url: string,
|
url: string,
|
||||||
token: string,
|
token: string,
|
||||||
videoId: number | string,
|
videoId: number | string,
|
||||||
reason: string,
|
reason: string,
|
||||||
predefinedReasons?: VideoAbusePredefinedReasonsString[],
|
predefinedReasons?: AbusePredefinedReasonsString[],
|
||||||
startAt?: number,
|
startAt?: number,
|
||||||
endAt?: number,
|
endAt?: number,
|
||||||
specialStatus = 200
|
specialStatus = 200
|
||||||
|
@ -28,10 +28,10 @@ function getVideoAbusesList (options: {
|
||||||
url: string
|
url: string
|
||||||
token: string
|
token: string
|
||||||
id?: number
|
id?: number
|
||||||
predefinedReason?: VideoAbusePredefinedReasonsString
|
predefinedReason?: AbusePredefinedReasonsString
|
||||||
search?: string
|
search?: string
|
||||||
state?: VideoAbuseState
|
state?: AbuseState
|
||||||
videoIs?: VideoAbuseVideoIs
|
videoIs?: AbuseVideoIs
|
||||||
searchReporter?: string
|
searchReporter?: string
|
||||||
searchReportee?: string
|
searchReportee?: string
|
||||||
searchVideo?: string
|
searchVideo?: string
|
||||||
|
@ -79,7 +79,7 @@ function updateVideoAbuse (
|
||||||
token: string,
|
token: string,
|
||||||
videoId: string | number,
|
videoId: string | number,
|
||||||
videoAbuseId: number,
|
videoAbuseId: number,
|
||||||
body: VideoAbuseUpdate,
|
body: AbuseUpdate,
|
||||||
statusCodeExpected = 204
|
statusCodeExpected = 204
|
||||||
) {
|
) {
|
||||||
const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
|
const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { ActivityPubActor } from './activitypub-actor'
|
import { ActivityPubActor } from './activitypub-actor'
|
||||||
import { ActivityPubSignature } from './activitypub-signature'
|
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 { 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 { APObject } from './objects/object.model'
|
||||||
import { PlaylistObject } from './objects/playlist-object'
|
import { PlaylistObject } from './objects/playlist-object'
|
||||||
|
import { VideoCommentObject } from './objects/video-comment-object'
|
||||||
|
import { ViewObject } from './objects/view-object'
|
||||||
|
|
||||||
export type Activity =
|
export type Activity =
|
||||||
ActivityCreate |
|
ActivityCreate |
|
||||||
|
@ -53,7 +53,7 @@ export interface BaseActivity {
|
||||||
|
|
||||||
export interface ActivityCreate extends BaseActivity {
|
export interface ActivityCreate extends BaseActivity {
|
||||||
type: 'Create'
|
type: 'Create'
|
||||||
object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject
|
object: VideoTorrentObject | AbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActivityUpdate extends BaseActivity {
|
export interface ActivityUpdate extends BaseActivity {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { ActivityFlagReasonObject } from './common-objects'
|
import { ActivityFlagReasonObject } from './common-objects'
|
||||||
|
|
||||||
export interface VideoAbuseObject {
|
export interface AbuseObject {
|
||||||
type: 'Flag'
|
type: 'Flag'
|
||||||
content: string
|
content: string
|
||||||
object: string | string[]
|
object: string | string[]
|
||||||
|
|
||||||
tag?: ActivityFlagReasonObject[]
|
tag?: ActivityFlagReasonObject[]
|
||||||
|
|
||||||
startAt?: number
|
startAt?: number
|
||||||
endAt?: number
|
endAt?: number
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { VideoAbusePredefinedReasonsString } from '@shared/models/videos'
|
import { AbusePredefinedReasonsString } from '@shared/models'
|
||||||
|
|
||||||
export interface ActivityIdentifierObject {
|
export interface ActivityIdentifierObject {
|
||||||
identifier: string
|
identifier: string
|
||||||
|
@ -85,7 +85,7 @@ export interface ActivityMentionObject {
|
||||||
|
|
||||||
export interface ActivityFlagReasonObject {
|
export interface ActivityFlagReasonObject {
|
||||||
type: 'Hashtag'
|
type: 'Hashtag'
|
||||||
name: VideoAbusePredefinedReasonsString
|
name: AbusePredefinedReasonsString
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ActivityTagObject =
|
export type ActivityTagObject =
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
export * from './abuse-object'
|
||||||
export * from './cache-file-object'
|
export * from './cache-file-object'
|
||||||
export * from './common-objects'
|
export * from './common-objects'
|
||||||
export * from './video-abuse-object'
|
export * from './dislike-object'
|
||||||
export * from './video-torrent-object'
|
export * from './video-torrent-object'
|
||||||
export * from './view-object'
|
export * from './view-object'
|
||||||
export * from './dislike-object'
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
export * from './activitypub'
|
export * from './activitypub'
|
||||||
export * from './actors'
|
export * from './actors'
|
||||||
export * from './avatars'
|
export * from './avatars'
|
||||||
export * from './blocklist'
|
export * from './moderation'
|
||||||
export * from './bulk'
|
export * from './bulk'
|
||||||
export * from './redundancy'
|
export * from './redundancy'
|
||||||
export * from './users'
|
export * from './users'
|
||||||
|
@ -14,4 +14,3 @@ export * from './search'
|
||||||
export * from './server'
|
export * from './server'
|
||||||
export * from './oauth-client-local.model'
|
export * from './oauth-client-local.model'
|
||||||
export * from './result-list.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,
|
PENDING = 1,
|
||||||
REJECTED = 2,
|
REJECTED = 2,
|
||||||
ACCEPTED = 3
|
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 './account-block.model'
|
||||||
export * from './server-block.model'
|
export * from './server-block.model'
|
|
@ -64,9 +64,20 @@ export interface UserNotification {
|
||||||
video: VideoInfo
|
video: VideoInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
videoAbuse?: {
|
abuse?: {
|
||||||
id: number
|
id: number
|
||||||
video: VideoInfo
|
|
||||||
|
video?: VideoInfo
|
||||||
|
|
||||||
|
comment?: {
|
||||||
|
threadId: number
|
||||||
|
|
||||||
|
video: {
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account?: ActorInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
videoBlacklist?: {
|
videoBlacklist?: {
|
||||||
|
|
|
@ -11,7 +11,7 @@ export enum UserRight {
|
||||||
|
|
||||||
MANAGE_SERVER_REDUNDANCY,
|
MANAGE_SERVER_REDUNDANCY,
|
||||||
|
|
||||||
MANAGE_VIDEO_ABUSES,
|
MANAGE_ABUSES,
|
||||||
|
|
||||||
MANAGE_JOBS,
|
MANAGE_JOBS,
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
|
||||||
|
|
||||||
[UserRole.MODERATOR]: [
|
[UserRole.MODERATOR]: [
|
||||||
UserRight.MANAGE_VIDEO_BLACKLIST,
|
UserRight.MANAGE_VIDEO_BLACKLIST,
|
||||||
UserRight.MANAGE_VIDEO_ABUSES,
|
UserRight.MANAGE_ABUSES,
|
||||||
UserRight.REMOVE_ANY_VIDEO,
|
UserRight.REMOVE_ANY_VIDEO,
|
||||||
UserRight.REMOVE_ANY_VIDEO_CHANNEL,
|
UserRight.REMOVE_ANY_VIDEO_CHANNEL,
|
||||||
UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
|
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