]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Use 3 tables to represent abuses
authorChocobozzz <me@florianbigard.com>
Wed, 1 Jul 2020 14:05:30 +0000 (16:05 +0200)
committerChocobozzz <chocobozzz@cpy.re>
Fri, 10 Jul 2020 12:02:41 +0000 (14:02 +0200)
103 files changed:
client/src/app/+admin/admin.component.ts
client/src/app/+admin/admin.module.ts
client/src/app/+admin/moderation/abuse-list/abuse-details.component.html [new file with mode: 0644]
client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts [moved from client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts with 59% similarity]
client/src/app/+admin/moderation/abuse-list/abuse-list.component.html [moved from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html with 52% similarity]
client/src/app/+admin/moderation/abuse-list/abuse-list.component.scss [moved from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.scss with 100% similarity]
client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts [moved from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts with 64% similarity]
client/src/app/+admin/moderation/abuse-list/index.ts [new file with mode: 0644]
client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.html [moved from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html with 100% similarity]
client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.scss [moved from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.scss with 100% similarity]
client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts [moved from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts with 73% similarity]
client/src/app/+admin/moderation/index.ts
client/src/app/+admin/moderation/moderation.routes.ts
client/src/app/+admin/moderation/video-abuse-list/index.ts [deleted file]
client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html [deleted file]
client/src/app/+my-account/my-account-settings/my-account-notification-preferences/my-account-notification-preferences.component.ts
client/src/app/menu/menu.component.ts
client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts [moved from client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts with 81% similarity]
client/src/app/shared/shared-forms/form-validators/index.ts
client/src/app/shared/shared-forms/shared-form.module.ts
client/src/app/shared/shared-main/account/actor.model.ts
client/src/app/shared/shared-main/users/user-notification.model.ts
client/src/app/shared/shared-main/users/user-notifications.component.html
client/src/app/shared/shared-moderation/abuse.service.ts [moved from client/src/app/shared/shared-moderation/video-abuse.service.ts with 67% similarity]
client/src/app/shared/shared-moderation/index.ts
client/src/app/shared/shared-moderation/shared-moderation.module.ts
client/src/app/shared/shared-moderation/video-report.component.ts
server/controllers/api/abuse.ts [new file with mode: 0644]
server/controllers/api/index.ts
server/controllers/api/videos/abuse.ts
server/helpers/audit-logger.ts
server/helpers/custom-validators/abuses.ts [new file with mode: 0644]
server/helpers/custom-validators/activitypub/flag.ts
server/helpers/custom-validators/video-abuses.ts [deleted file]
server/helpers/middlewares/abuses.ts [moved from server/helpers/middlewares/video-abuses.ts with 56% similarity]
server/helpers/middlewares/index.ts
server/initializers/constants.ts
server/initializers/database.ts
server/initializers/migrations/0250-video-abuse-state.ts
server/lib/activitypub/process/process-flag.ts
server/lib/activitypub/send/send-flag.ts
server/lib/activitypub/url.ts
server/lib/emailer.ts
server/lib/emails/account-abuse-new/html.pug [new file with mode: 0644]
server/lib/emails/common/mixins.pug
server/lib/emails/video-abuse-new/html.pug
server/lib/emails/video-comment-abuse-new/html.pug [new file with mode: 0644]
server/lib/moderation.ts
server/lib/notifier.ts
server/middlewares/validators/abuse.ts [new file with mode: 0644]
server/middlewares/validators/index.ts
server/middlewares/validators/sort.ts
server/middlewares/validators/videos/index.ts
server/middlewares/validators/videos/video-abuses.ts [deleted file]
server/models/abuse/abuse.ts [moved from server/models/video/video-abuse.ts with 52% similarity]
server/models/abuse/video-abuse.ts [new file with mode: 0644]
server/models/abuse/video-comment-abuse.ts [new file with mode: 0644]
server/models/account/account-blocklist.ts
server/models/account/account.ts
server/models/account/user-notification.ts
server/models/account/user.ts
server/models/server/server-blocklist.ts
server/models/video/video.ts
server/tests/api/check-params/video-abuses.ts
server/tests/api/users/users.ts
server/tests/api/videos/video-abuse.ts
server/types/models/index.ts
server/types/models/moderation/abuse.ts [new file with mode: 0644]
server/types/models/moderation/index.ts [new file with mode: 0644]
server/types/models/user/user-notification.ts
server/types/models/video/index.ts
server/types/models/video/video-abuse.ts [deleted file]
server/typings/express/index.d.ts
shared/extra-utils/index.ts
shared/extra-utils/moderation/abuses.ts [new file with mode: 0644]
shared/extra-utils/users/user-notifications.ts
shared/extra-utils/videos/video-abuses.ts
shared/models/activitypub/activity.ts
shared/models/activitypub/objects/abuse-object.ts [moved from shared/models/activitypub/objects/video-abuse-object.ts with 84% similarity]
shared/models/activitypub/objects/common-objects.ts
shared/models/activitypub/objects/index.ts
shared/models/index.ts
shared/models/moderation/abuse/abuse-create.model.ts [new file with mode: 0644]
shared/models/moderation/abuse/abuse-filter.ts [new file with mode: 0644]
shared/models/moderation/abuse/abuse-reason.model.ts [new file with mode: 0644]
shared/models/moderation/abuse/abuse-state.model.ts [moved from shared/models/videos/abuse/video-abuse-state.model.ts with 61% similarity]
shared/models/moderation/abuse/abuse-update.model.ts [new file with mode: 0644]
shared/models/moderation/abuse/abuse-video-is.type.ts [new file with mode: 0644]
shared/models/moderation/abuse/abuse.model.ts [new file with mode: 0644]
shared/models/moderation/abuse/index.ts [new file with mode: 0644]
shared/models/moderation/account-block.model.ts [moved from shared/models/blocklist/account-block.model.ts with 100% similarity]
shared/models/moderation/index.ts [moved from shared/models/blocklist/index.ts with 75% similarity]
shared/models/moderation/server-block.model.ts [moved from shared/models/blocklist/server-block.model.ts with 100% similarity]
shared/models/users/user-notification.model.ts
shared/models/users/user-right.enum.ts
shared/models/users/user-role.ts
shared/models/videos/abuse/index.ts [deleted file]
shared/models/videos/abuse/video-abuse-create.model.ts [deleted file]
shared/models/videos/abuse/video-abuse-reason.model.ts [deleted file]
shared/models/videos/abuse/video-abuse-update.model.ts [deleted file]
shared/models/videos/abuse/video-abuse-video-is.type.ts [deleted file]
shared/models/videos/abuse/video-abuse.model.ts [deleted file]
shared/models/videos/index.ts

index 6f340884f8d9e0e39360dcb7126543a964a1cbff..1e137e63ed78701904bdbe8d168b5fdb7699247f 100644 (file)
@@ -91,7 +91,7 @@ export class AdminComponent implements OnInit {
   }
 
   hasVideoAbusesRight () {
-    return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
+    return this.auth.getUser().hasRight(UserRight.MANAGE_ABUSES)
   }
 
   hasVideoBlocklistRight () {
index 728227a84d076d48ff12bd225e2c56d3988fbc50..c59bd292752aa74a5b87fe8e0a912cb672bc060f 100644 (file)
@@ -14,10 +14,10 @@ import { FollowersListComponent, FollowsComponent, VideoRedundanciesListComponen
 import { FollowingListComponent } from './follows/following-list/following-list.component'
 import { RedundancyCheckboxComponent } from './follows/shared/redundancy-checkbox.component'
 import { VideoRedundancyInformationComponent } from './follows/video-redundancies-list/video-redundancy-information.component'
-import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlockListComponent } from './moderation'
+import { ModerationCommentModalComponent, AbuseListComponent, VideoBlockListComponent } from './moderation'
 import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
 import { ModerationComponent } from './moderation/moderation.component'
-import { VideoAbuseDetailsComponent } from './moderation/video-abuse-list/video-abuse-details.component'
+import { AbuseDetailsComponent } from './moderation/abuse-list/abuse-details.component'
 import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component'
 import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component'
 import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component'
@@ -60,8 +60,10 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
 
     ModerationComponent,
     VideoBlockListComponent,
-    VideoAbuseListComponent,
-    VideoAbuseDetailsComponent,
+
+    AbuseListComponent,
+    AbuseDetailsComponent,
+
     ModerationCommentModalComponent,
     InstanceServerBlocklistComponent,
     InstanceAccountBlocklistComponent,
diff --git a/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html b/client/src/app/+admin/moderation/abuse-list/abuse-details.component.html
new file mode 100644 (file)
index 0000000..d031ea8
--- /dev/null
@@ -0,0 +1,93 @@
+<div class="d-flex moderation-expanded">
+  <!-- report left part (report details) -->
+  <div class="col-8">
+
+    <!-- report metadata -->
+    <div class="d-flex">
+      <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
+      <span class="col-9 moderation-expanded-text">
+        <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" class="chip">
+          <img
+            class="avatar"
+            [src]="abuse.reporterAccount.avatar?.path"
+            (error)="switchToDefaultAvatar($event)"
+            alt="Avatar"
+          >
+          <div>
+            <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
+          </div>
+        </a>
+        <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + abuse.reporterAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
+          {abuse.countReportsForReporter, plural, =1 {1 report} other {{{ abuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
+        </a>
+      </span>
+    </div>
+
+    <div class="d-flex">
+      <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
+      <span class="col-9 moderation-expanded-text">
+        <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.video.channel.ownerAccount.displayName + '&quot;' }" class="chip">
+          <img
+            class="avatar"
+            [src]="abuse.video.channel.ownerAccount?.avatar?.path"
+            (error)="switchToDefaultAvatar($event)"
+            alt="Avatar"
+          >
+          <div>
+            <span class="text-muted">{{ abuse.video.channel.ownerAccount ? abuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
+          </div>
+        </a>
+        <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +abuse.video.channel.ownerAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
+          {abuse.countReportsForReportee, plural, =1 {1 report} other {{{ abuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
+        </a>
+      </span>
+    </div>
+
+    <div class="d-flex" *ngIf="abuse.updatedAt">
+      <span class="col-3 moderation-expanded-label" i18n>Updated</span>
+      <time class="col-9 moderation-expanded-text video-details-date-updated">{{ abuse.updatedAt | date: 'medium' }}</time>
+    </div>
+
+    <!-- report text -->
+    <div class="mt-3 d-flex">
+      <span class="col-3 moderation-expanded-label">
+        <ng-container i18n>Report</ng-container>
+        <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': '#' + abuse.id  }" class="ml-1 text-muted">#{{ abuse.id }}</a>
+      </span>
+      <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.reasonHtml"></span>
+    </div>
+
+    <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
+      <span class="col-3"></span>
+      <span class="col-9">
+        <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id  }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()">
+          <div>{{ reason.label }}</div>
+        </a>
+      </span>
+    </div>
+
+    <div *ngIf="abuse.startAt" class="mt-2 d-flex">
+      <span class="col-3 moderation-expanded-label" i18n>Reported part</span>
+      <span class="col-9">
+        {{ startAt }}<ng-container *ngIf="abuse.endAt"> - {{ endAt }}</ng-container>
+      </span>
+    </div>
+
+    <div class="mt-3 d-flex" *ngIf="abuse.moderationComment">
+      <span class="col-3 moderation-expanded-label" i18n>Note</span>
+      <span class="col-9 moderation-expanded-text" [innerHTML]="abuse.moderationCommentHtml"></span>
+    </div>
+
+  </div>
+
+  <!-- report right part (video details) -->
+  <div class="col-4">
+    <div class="screenratio">
+      <div *ngIf="abuse.video.deleted || abuse.video.blacklisted">
+        <span i18n *ngIf="abuse.video.deleted">The video was deleted</span>
+        <span i18n *ngIf="!abuse.video.deleted">The video was blocked</span>
+      </div>
+      <div *ngIf="!abuse.video.deleted && !abuse.video.blacklisted" [innerHTML]="abuse.embedHtml"></div>
+    </div>
+  </div>
+</div>
similarity index 59%
rename from client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.ts
rename to client/src/app/+admin/moderation/abuse-list/abuse-details.component.ts
index 5db2887fa8bd7ef48ee83d5400f8e900d8922fbf..8f87630b82759cf381e1f8ec509a9508765b6c3f 100644 (file)
@@ -1,19 +1,19 @@
 import { Component, Input } from '@angular/core'
 import { Actor } from '@app/shared/shared-main'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { VideoAbusePredefinedReasonsString } from '../../../../../../shared/models/videos/abuse/video-abuse-reason.model'
-import { ProcessedVideoAbuse } from './video-abuse-list.component'
+import { AbusePredefinedReasonsString } from '@shared/models'
+import { ProcessedAbuse } from './abuse-list.component'
 import { durationToString } from '@app/helpers'
 
 @Component({
-  selector: 'my-video-abuse-details',
-  templateUrl: './video-abuse-details.component.html',
+  selector: 'my-abuse-details',
+  templateUrl: './abuse-details.component.html',
   styleUrls: [ '../moderation.component.scss' ]
 })
-export class VideoAbuseDetailsComponent {
-  @Input() videoAbuse: ProcessedVideoAbuse
+export class AbuseDetailsComponent {
+  @Input() abuse: ProcessedAbuse
 
-  private predefinedReasonsTranslations: { [key in VideoAbusePredefinedReasonsString]: string }
+  private predefinedReasonsTranslations: { [key in AbusePredefinedReasonsString]: string }
 
   constructor (
     private i18n: I18n
@@ -31,16 +31,16 @@ export class VideoAbuseDetailsComponent {
   }
 
   get startAt () {
-    return durationToString(this.videoAbuse.startAt)
+    return durationToString(this.abuse.startAt)
   }
 
   get endAt () {
-    return durationToString(this.videoAbuse.endAt)
+    return durationToString(this.abuse.endAt)
   }
 
   getPredefinedReasons () {
-    if (!this.videoAbuse.predefinedReasons) return []
-    return this.videoAbuse.predefinedReasons.map(r => ({
+    if (!this.abuse.predefinedReasons) return []
+    return this.abuse.predefinedReasons.map(r => ({
       id: r,
       label: this.predefinedReasonsTranslations[r]
     }))
similarity index 52%
rename from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html
rename to client/src/app/+admin/moderation/abuse-list/abuse-list.component.html
index 64641b28a731b0081a11787204274ac357666722..167f32fe681c4c4d0a0c50d93aa36ee61f202915 100644 (file)
@@ -1,5 +1,5 @@
 <p-table
-  [value]="videoAbuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
+  [value]="abuses" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
   [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" [resizableColumns]="true"
   [showCurrentPageReport]="true" i18n-currentPageReportTemplate
   currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} reports"
 
             <div role="menu" ngbDropdownMenu>
               <h6 class="dropdown-header" i18n>Advanced report filters</h6>
-              <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
-              <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
-              <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
-              <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
-              <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
+              <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:pending' }" class="dropdown-item" i18n>Unsolved reports</a>
+              <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:accepted' }" class="dropdown-item" i18n>Accepted reports</a>
+              <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'state:rejected' }" class="dropdown-item" i18n>Refused reports</a>
+              <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:blacklisted' }" class="dropdown-item" i18n>Reports with blocked videos</a>
+              <a [routerLink]="[ '/admin/moderation/abuses/list' ]" [queryParams]="{ 'search': 'videoIs:deleted' }" class="dropdown-item" i18n>Reports with deleted videos</a>
             </div>
           </div>
           <input
     </tr>
   </ng-template>
 
-  <ng-template pTemplate="body" let-expanded="expanded" let-videoAbuse>
+  <ng-template pTemplate="body" let-expanded="expanded" let-abuse>
     <tr>
-      <td class="c-hand" [pRowToggler]="videoAbuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
+      <td class="c-hand" [pRowToggler]="abuse" i18n-ngbTooltip ngbTooltip="More information" placement="top-left" container="body">
         <span class="expander">
           <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
         </span>
       </td>
 
       <td>
-        <a [href]="videoAbuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
+        <a [href]="abuse.reporterAccount.url" i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer">
           <div class="chip two-lines">
             <img
               class="avatar"
-              [src]="videoAbuse.reporterAccount.avatar?.path"
+              [src]="abuse.reporterAccount.avatar?.path"
               (error)="switchToDefaultAvatar($event)"
               alt="Avatar"
             >
             <div>
-              {{ videoAbuse.reporterAccount.displayName }}
-              <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
+              {{ abuse.reporterAccount.displayName }}
+              <span class="text-muted">{{ abuse.reporterAccount.nameWithHost }}</span>
             </div>
           </div>
         </a>
       </td>
 
-      <td *ngIf="!videoAbuse.video.deleted">
-        <a [href]="getVideoUrl(videoAbuse)" class="video-table-video-link" [title]="videoAbuse.video.name" target="_blank" rel="noopener noreferrer">
+      <td *ngIf="!abuse.video.deleted">
+        <a [href]="getVideoUrl(abuse)" class="video-table-video-link" [title]="abuse.video.name" target="_blank" rel="noopener noreferrer">
           <div class="video-table-video">
             <div class="video-table-video-image">
-              <img [src]="videoAbuse.video.thumbnailPath">
+              <img [src]="abuse.video.thumbnailPath">
               <span
-                class="video-table-video-image-label" *ngIf="videoAbuse.count > 1"
+                class="video-table-video-image-label" *ngIf="abuse.count > 1"
                 i18n-title title="This video has been reported multiple times."
               >
-                {{ videoAbuse.nth }}/{{ videoAbuse.count }}
+                {{ abuse.nth }}/{{ abuse.count }}
               </span>
             </div>
             <div class="video-table-video-text">
               <div>
-                <span *ngIf="!videoAbuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
-                <span *ngIf="videoAbuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
-                {{ videoAbuse.video.name }}
+                <span *ngIf="!abuse.video.blacklisted" class="glyphicon glyphicon-new-window"></span>
+                <span *ngIf="abuse.video.blacklisted" i18n-title title="The video was blocked" class="glyphicon glyphicon-ban-circle"></span>
+                {{ abuse.video.name }}
               </div>
-              <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
+              <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
             </div>
           </div>
         </a>
       </td>
 
-      <td *ngIf="videoAbuse.video.deleted" class="c-hand" [pRowToggler]="videoAbuse">
+      <td *ngIf="abuse.video.deleted" class="c-hand" [pRowToggler]="abuse">
         <div class="video-table-video" i18n-title title="Video was deleted">
           <div class="video-table-video-image">
             <span i18n>Deleted</span>
           </div>
           <div class="video-table-video-text">
             <div>
-              {{ videoAbuse.video.name }}
+              {{ abuse.video.name }}
               <span class="glyphicon glyphicon-trash"></span>
             </div>
-            <div class="text-muted" i18n>by {{ videoAbuse.video.channel?.displayName }} on {{ videoAbuse.video.channel?.host }} </div>
+            <div class="text-muted" i18n>by {{ abuse.video.channel?.displayName }} on {{ abuse.video.channel?.host }} </div>
           </div>
         </div>
       </td>
 
-      <td class="c-hand" [pRowToggler]="videoAbuse">{{ videoAbuse.createdAt | date: 'short'  }}</td>
+      <td class="c-hand" [pRowToggler]="abuse">{{ abuse.createdAt | date: 'short'  }}</td>
 
-      <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
-        <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
-        <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
-        <span *ngIf="videoAbuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
+      <td class="c-hand video-abuse-states" [pRowToggler]="abuse">
+        <span *ngIf="isAbuseAccepted(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-ok"></span>
+        <span *ngIf="isAbuseRejected(abuse)" [title]="abuse.state.label" class="glyphicon glyphicon-remove"></span>
+        <span *ngIf="abuse.moderationComment" container="body" placement="left auto" [ngbTooltip]="abuse.moderationComment" class="glyphicon glyphicon-comment"></span>
       </td>
 
       <td class="action-cell">
         <my-action-dropdown
           [ngClass]="{ 'show': expanded }" placement="bottom-right top-right left auto" container="body"
-          i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"
+          i18n-label label="Actions" [actions]="abuseActions" [entry]="abuse"
         ></my-action-dropdown>
       </td>
     </tr>
   </ng-template>
 
-  <ng-template pTemplate="rowexpansion" let-videoAbuse>
+  <ng-template pTemplate="rowexpansion" let-abuse>
       <tr>
         <td class="expand-cell" colspan="6">
-          <my-video-abuse-details [videoAbuse]="videoAbuse"></my-video-abuse-details>
+          <my-abuse-details [abuse]="abuse"></my-abuse-details>
         </td>
       </tr>
   </ng-template>
similarity index 64%
rename from client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts
rename to client/src/app/+admin/moderation/abuse-list/abuse-list.component.ts
index 409dd42c7e68285d2294275f905ca73ebceddaf1..427ec4d5d3a7194008ec6a06b052a2f3b4da84b1 100644 (file)
@@ -1,5 +1,4 @@
 import { SortMeta } from 'primeng/api'
-import { filter } from 'rxjs/operators'
 import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
 import { environment } from 'src/environments/environment'
 import { AfterViewInit, Component, OnInit, ViewChild } from '@angular/core'
@@ -7,43 +6,45 @@ import { DomSanitizer } from '@angular/platform-browser'
 import { ActivatedRoute, Params, Router } from '@angular/router'
 import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable } from '@app/core'
 import { Account, Actor, DropdownAction, Video, VideoService } from '@app/shared/shared-main'
-import { BlocklistService, VideoAbuseService, VideoBlockService } from '@app/shared/shared-moderation'
+import { AbuseService, BlocklistService, VideoBlockService } from '@app/shared/shared-moderation'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { VideoAbuse, VideoAbuseState } from '@shared/models'
+import { Abuse, AbuseState } from '@shared/models'
 import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
 
-export type ProcessedVideoAbuse = VideoAbuse & {
+export type ProcessedAbuse = Abuse & {
   moderationCommentHtml?: string,
   reasonHtml?: string
   embedHtml?: string
   updatedAt?: Date
+
   // override bare server-side definitions with rich client-side definitions
   reporterAccount: Account
-  video: VideoAbuse['video'] & {
-    channel: VideoAbuse['video']['channel'] & {
+
+  video: Abuse['video'] & {
+    channel: Abuse['video']['channel'] & {
       ownerAccount: Account
     }
   }
 }
 
 @Component({
-  selector: 'my-video-abuse-list',
-  templateUrl: './video-abuse-list.component.html',
-  styleUrls: [ '../moderation.component.scss', './video-abuse-list.component.scss' ]
+  selector: 'my-abuse-list',
+  templateUrl: './abuse-list.component.html',
+  styleUrls: [ '../moderation.component.scss', './abuse-list.component.scss' ]
 })
-export class VideoAbuseListComponent extends RestTable implements OnInit, AfterViewInit {
+export class AbuseListComponent extends RestTable implements OnInit, AfterViewInit {
   @ViewChild('moderationCommentModal', { static: true }) moderationCommentModal: ModerationCommentModalComponent
 
-  videoAbuses: ProcessedVideoAbuse[] = []
+  abuses: ProcessedAbuse[] = []
   totalRecords = 0
   sort: SortMeta = { field: 'createdAt', order: 1 }
   pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
 
-  videoAbuseActions: DropdownAction<VideoAbuse>[][] = []
+  abuseActions: DropdownAction<Abuse>[][] = []
 
   constructor (
     private notifier: Notifier,
-    private videoAbuseService: VideoAbuseService,
+    private abuseService: AbuseService,
     private blocklistService: BlocklistService,
     private videoService: VideoService,
     private videoBlocklistService: VideoBlockService,
@@ -56,7 +57,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
   ) {
     super()
 
-    this.videoAbuseActions = [
+    this.abuseActions = [
       [
         {
           label: this.i18n('Internal actions'),
@@ -64,45 +65,45 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
         },
         {
           label: this.i18n('Delete report'),
-          handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
+          handler: abuse => this.removeAbuse(abuse)
         },
         {
           label: this.i18n('Add note'),
-          handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
-          isDisplayed: videoAbuse => !videoAbuse.moderationComment
+          handler: abuse => this.openModerationCommentModal(abuse),
+          isDisplayed: abuse => !abuse.moderationComment
         },
         {
           label: this.i18n('Update note'),
-          handler: videoAbuse => this.openModerationCommentModal(videoAbuse),
-          isDisplayed: videoAbuse => !!videoAbuse.moderationComment
+          handler: abuse => this.openModerationCommentModal(abuse),
+          isDisplayed: abuse => !!abuse.moderationComment
         },
         {
           label: this.i18n('Mark as accepted'),
-          handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED),
-          isDisplayed: videoAbuse => !this.isVideoAbuseAccepted(videoAbuse)
+          handler: abuse => this.updateAbuseState(abuse, AbuseState.ACCEPTED),
+          isDisplayed: abuse => !this.isAbuseAccepted(abuse)
         },
         {
           label: this.i18n('Mark as rejected'),
-          handler: videoAbuse => this.updateVideoAbuseState(videoAbuse, VideoAbuseState.REJECTED),
-          isDisplayed: videoAbuse => !this.isVideoAbuseRejected(videoAbuse)
+          handler: abuse => this.updateAbuseState(abuse, AbuseState.REJECTED),
+          isDisplayed: abuse => !this.isAbuseRejected(abuse)
         }
       ],
       [
         {
           label: this.i18n('Actions for the video'),
           isHeader: true,
-          isDisplayed: videoAbuse => !videoAbuse.video.deleted
+          isDisplayed: abuse => !abuse.video.deleted
         },
         {
           label: this.i18n('Block video'),
-          isDisplayed: videoAbuse => !videoAbuse.video.deleted && !videoAbuse.video.blacklisted,
-          handler: videoAbuse => {
-            this.videoBlocklistService.blockVideo(videoAbuse.video.id, undefined, true)
+          isDisplayed: abuse => !abuse.video.deleted && !abuse.video.blacklisted,
+          handler: abuse => {
+            this.videoBlocklistService.blockVideo(abuse.video.id, undefined, true)
               .subscribe(
                 () => {
                   this.notifier.success(this.i18n('Video blocked.'))
 
-                  this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
+                  this.updateAbuseState(abuse, AbuseState.ACCEPTED)
                 },
 
                 err => this.notifier.error(err.message)
@@ -111,14 +112,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
         },
         {
           label: this.i18n('Unblock video'),
-          isDisplayed: videoAbuse => !videoAbuse.video.deleted && videoAbuse.video.blacklisted,
-          handler: videoAbuse => {
-            this.videoBlocklistService.unblockVideo(videoAbuse.video.id)
+          isDisplayed: abuse => !abuse.video.deleted && abuse.video.blacklisted,
+          handler: abuse => {
+            this.videoBlocklistService.unblockVideo(abuse.video.id)
               .subscribe(
                 () => {
                   this.notifier.success(this.i18n('Video unblocked.'))
 
-                  this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
+                  this.updateAbuseState(abuse, AbuseState.ACCEPTED)
                 },
 
                 err => this.notifier.error(err.message)
@@ -127,20 +128,20 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
         },
         {
           label: this.i18n('Delete video'),
-          isDisplayed: videoAbuse => !videoAbuse.video.deleted,
-          handler: async videoAbuse => {
+          isDisplayed: abuse => !abuse.video.deleted,
+          handler: async abuse => {
             const res = await this.confirmService.confirm(
               this.i18n('Do you really want to delete this video?'),
               this.i18n('Delete')
             )
             if (res === false) return
 
-            this.videoService.removeVideo(videoAbuse.video.id)
+            this.videoService.removeVideo(abuse.video.id)
               .subscribe(
                 () => {
                   this.notifier.success(this.i18n('Video deleted.'))
 
-                  this.updateVideoAbuseState(videoAbuse, VideoAbuseState.ACCEPTED)
+                  this.updateAbuseState(abuse, AbuseState.ACCEPTED)
                 },
 
                 err => this.notifier.error(err.message)
@@ -155,8 +156,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
         },
         {
           label: this.i18n('Mute reporter'),
-          handler: async videoAbuse => {
-            const account = videoAbuse.reporterAccount as Account
+          handler: async abuse => {
+            const account = abuse.reporterAccount as Account
 
             this.blocklistService.blockAccountByInstance(account)
               .subscribe(
@@ -174,13 +175,13 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
         },
         {
           label: this.i18n('Mute server'),
-          isDisplayed: videoAbuse => !videoAbuse.reporterAccount.userId,
-          handler: async videoAbuse => {
-            this.blocklistService.blockServerByInstance(videoAbuse.reporterAccount.host)
+          isDisplayed: abuse => !abuse.reporterAccount.userId,
+          handler: async abuse => {
+            this.blocklistService.blockServerByInstance(abuse.reporterAccount.host)
               .subscribe(
                 () => {
                   this.notifier.success(
-                    this.i18n('Server {{host}} muted by the instance.', { host: videoAbuse.reporterAccount.host })
+                    this.i18n('Server {{host}} muted by the instance.', { host: abuse.reporterAccount.host })
                   )
                 },
 
@@ -209,11 +210,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
   }
 
   getIdentifier () {
-    return 'VideoAbuseListComponent'
+    return 'AbuseListComponent'
   }
 
-  openModerationCommentModal (videoAbuse: VideoAbuse) {
-    this.moderationCommentModal.openModal(videoAbuse)
+  openModerationCommentModal (abuse: Abuse) {
+    this.moderationCommentModal.openModal(abuse)
   }
 
   onModerationCommentUpdated () {
@@ -240,26 +241,26 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
   }
   /* END Table filter functions */
 
-  isVideoAbuseAccepted (videoAbuse: VideoAbuse) {
-    return videoAbuse.state.id === VideoAbuseState.ACCEPTED
+  isAbuseAccepted (abuse: Abuse) {
+    return abuse.state.id === AbuseState.ACCEPTED
   }
 
-  isVideoAbuseRejected (videoAbuse: VideoAbuse) {
-    return videoAbuse.state.id === VideoAbuseState.REJECTED
+  isAbuseRejected (abuse: Abuse) {
+    return abuse.state.id === AbuseState.REJECTED
   }
 
-  getVideoUrl (videoAbuse: VideoAbuse) {
-    return Video.buildClientUrl(videoAbuse.video.uuid)
+  getVideoUrl (abuse: Abuse) {
+    return Video.buildClientUrl(abuse.video.uuid)
   }
 
-  getVideoEmbed (videoAbuse: VideoAbuse) {
+  getVideoEmbed (abuse: Abuse) {
     return buildVideoEmbed(
       buildVideoLink({
-        baseUrl: `${environment.embedUrl}/videos/embed/${videoAbuse.video.uuid}`,
+        baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`,
         title: false,
         warningTitle: false,
-        startTime: videoAbuse.startAt,
-        stopTime: videoAbuse.endAt
+        startTime: abuse.startAt,
+        stopTime: abuse.endAt
       })
     )
   }
@@ -268,11 +269,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
     ($event.target as HTMLImageElement).src = Actor.GET_DEFAULT_AVATAR_URL()
   }
 
-  async removeVideoAbuse (videoAbuse: VideoAbuse) {
+  async removeAbuse (abuse: Abuse) {
     const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
     if (res === false) return
 
-    this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
+    this.abuseService.removeAbuse(abuse).subscribe(
       () => {
         this.notifier.success(this.i18n('Abuse deleted.'))
         this.loadData()
@@ -282,8 +283,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
     )
   }
 
-  updateVideoAbuseState (videoAbuse: VideoAbuse, state: VideoAbuseState) {
-    this.videoAbuseService.updateVideoAbuse(videoAbuse, { state })
+  updateAbuseState (abuse: Abuse, state: AbuseState) {
+    this.abuseService.updateAbuse(abuse, { state })
       .subscribe(
         () => this.loadData(),
 
@@ -292,14 +293,14 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
   }
 
   protected loadData () {
-    return this.videoAbuseService.getVideoAbuses({
+    return this.abuseService.getAbuses({
       pagination: this.pagination,
       sort: this.sort,
       search: this.search
     }).subscribe(
         async resultList => {
           this.totalRecords = resultList.total
-          const videoAbuses = []
+          const abuses = []
 
           for (const abuse of resultList.data) {
             Object.assign(abuse, {
@@ -312,10 +313,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit, AfterV
             if (abuse.video.channel?.ownerAccount) abuse.video.channel.ownerAccount = new Account(abuse.video.channel.ownerAccount)
             if (abuse.updatedAt === abuse.createdAt) delete abuse.updatedAt
 
-            videoAbuses.push(abuse as ProcessedVideoAbuse)
+            abuses.push(abuse as ProcessedAbuse)
           }
 
-          this.videoAbuses = videoAbuses
+          this.abuses = abuses
         },
 
         err => this.notifier.error(err.message)
diff --git a/client/src/app/+admin/moderation/abuse-list/index.ts b/client/src/app/+admin/moderation/abuse-list/index.ts
new file mode 100644 (file)
index 0000000..c6037da
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './abuse-details.component'
+export * from './abuse-list.component'
+export * from './moderation-comment-modal.component'
similarity index 73%
rename from client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts
rename to client/src/app/+admin/moderation/abuse-list/moderation-comment-modal.component.ts
index 3cd763ca46ee758ddf6b0ae641b7b1ed0593a16e..23738f9cd0c7a9df0040859d9a9d4a27d33bbe2a 100644 (file)
@@ -1,11 +1,11 @@
 import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
 import { Notifier } from '@app/core'
-import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms'
-import { VideoAbuseService } from '@app/shared/shared-moderation'
+import { FormReactive, FormValidatorService, AbuseValidatorsService } from '@app/shared/shared-forms'
+import { AbuseService } from '@app/shared/shared-moderation'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { VideoAbuse } from '@shared/models'
+import { Abuse } from '@shared/models'
 
 @Component({
   selector: 'my-moderation-comment-modal',
@@ -16,15 +16,15 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
   @ViewChild('modal', { static: true }) modal: NgbModal
   @Output() commentUpdated = new EventEmitter<string>()
 
-  private abuseToComment: VideoAbuse
+  private abuseToComment: Abuse
   private openedModal: NgbModalRef
 
   constructor (
     protected formValidatorService: FormValidatorService,
     private modalService: NgbModal,
     private notifier: Notifier,
-    private videoAbuseService: VideoAbuseService,
-    private videoAbuseValidatorsService: VideoAbuseValidatorsService,
+    private abuseService: AbuseService,
+    private abuseValidatorsService: AbuseValidatorsService,
     private i18n: I18n
   ) {
     super()
@@ -32,11 +32,11 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
 
   ngOnInit () {
     this.buildForm({
-      moderationComment: this.videoAbuseValidatorsService.VIDEO_ABUSE_MODERATION_COMMENT
+      moderationComment: this.abuseValidatorsService.ABUSE_MODERATION_COMMENT
     })
   }
 
-  openModal (abuseToComment: VideoAbuse) {
+  openModal (abuseToComment: Abuse) {
     this.abuseToComment = abuseToComment
     this.openedModal = this.modalService.open(this.modal, { centered: true })
 
@@ -54,7 +54,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
   async banUser () {
     const moderationComment: string = this.form.value[ 'moderationComment' ]
 
-    this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment })
+    this.abuseService.updateAbuse(this.abuseToComment, { moderationComment })
         .subscribe(
           () => {
             this.notifier.success(this.i18n('Comment updated.'))
index 16249236c781e3f608c3b36e5ef94c4805a4094c..53e4bc9913245fbb69357d72f0c42417558b7027 100644 (file)
@@ -1,5 +1,5 @@
+export * from './abuse-list'
 export * from './instance-blocklist'
-export * from './video-abuse-list'
 export * from './video-block-list'
 export * from './moderation.component'
 export * from './moderation.routes'
index cd837bcb948c04cb4c0eaa3499db8bccaa134b6e..1e207e5e87510fb2436a13885b18d1de146e767c 100644 (file)
@@ -1,7 +1,7 @@
 import { Routes } from '@angular/router'
 import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
 import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
-import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
+import { AbuseListComponent } from '@app/+admin/moderation/abuse-list'
 import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
 import { UserRightGuard } from '@app/core'
 import { UserRight } from '@shared/models'
@@ -13,20 +13,25 @@ export const ModerationRoutes: Routes = [
     children: [
       {
         path: '',
-        redirectTo: 'video-abuses/list',
+        redirectTo: 'abuses/list',
         pathMatch: 'full'
       },
       {
         path: 'video-abuses',
-        redirectTo: 'video-abuses/list',
+        redirectTo: 'abuses/list',
         pathMatch: 'full'
       },
       {
         path: 'video-abuses/list',
-        component: VideoAbuseListComponent,
+        redirectTo: 'abuses/list',
+        pathMatch: 'full'
+      },
+      {
+        path: 'abuses/list',
+        component: AbuseListComponent,
         canActivate: [ UserRightGuard ],
         data: {
-          userRight: UserRight.MANAGE_VIDEO_ABUSES,
+          userRight: UserRight.MANAGE_ABUSES,
           meta: {
             title: 'Video reports'
           }
diff --git a/client/src/app/+admin/moderation/video-abuse-list/index.ts b/client/src/app/+admin/moderation/video-abuse-list/index.ts
deleted file mode 100644 (file)
index da7176e..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './video-abuse-list.component'
-export * from './moderation-comment-modal.component'
diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-details.component.html
deleted file mode 100644 (file)
index ec808cd..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-<div class="d-flex moderation-expanded">
-  <!-- report left part (report details) -->
-  <div class="col-8">
-
-    <!-- report metadata -->
-    <div class="d-flex">
-      <span class="col-3 moderation-expanded-label" i18n>Reporter</span>
-      <span class="col-9 moderation-expanded-text">
-        <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" class="chip">
-          <img
-            class="avatar"
-            [src]="videoAbuse.reporterAccount.avatar?.path"
-            (error)="switchToDefaultAvatar($event)"
-            alt="Avatar"
-          >
-          <div>
-            <span class="text-muted">{{ videoAbuse.reporterAccount.nameWithHost }}</span>
-          </div>
-        </a>
-        <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reporter:&quot;' + videoAbuse.reporterAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
-          {videoAbuse.countReportsForReporter, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReporter }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
-        </a>
-      </span>
-    </div>
-
-    <div class="d-flex">
-      <span class="col-3 moderation-expanded-label" i18n>Reportee</span>
-      <span class="col-9 moderation-expanded-text">
-        <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" class="chip">
-          <img
-            class="avatar"
-            [src]="videoAbuse.video.channel.ownerAccount?.avatar?.path"
-            (error)="switchToDefaultAvatar($event)"
-            alt="Avatar"
-          >
-          <div>
-            <span class="text-muted">{{ videoAbuse.video.channel.ownerAccount ? videoAbuse.video.channel.ownerAccount.nameWithHost : '' }}</span>
-          </div>
-        </a>
-        <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'reportee:&quot;' +videoAbuse.video.channel.ownerAccount.displayName + '&quot;' }" class="ml-auto text-muted video-details-links" i18n>
-          {videoAbuse.countReportsForReportee, plural, =1 {1 report} other {{{ videoAbuse.countReportsForReportee }} reports}}<span class="ml-1 glyphicon glyphicon-flag"></span>
-        </a>
-      </span>
-    </div>
-
-    <div class="d-flex" *ngIf="videoAbuse.updatedAt">
-      <span class="col-3 moderation-expanded-label" i18n>Updated</span>
-      <time class="col-9 moderation-expanded-text video-details-date-updated">{{ videoAbuse.updatedAt | date: 'medium' }}</time>
-    </div>
-
-    <!-- report text -->
-    <div class="mt-3 d-flex">
-      <span class="col-3 moderation-expanded-label">
-        <ng-container i18n>Report</ng-container>
-        <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': '#' + videoAbuse.id  }" class="ml-1 text-muted">#{{ videoAbuse.id }}</a>
-      </span>
-      <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.reasonHtml"></span>
-    </div>
-
-    <div *ngIf="getPredefinedReasons()" class="mt-2 d-flex">
-      <span class="col-3"></span>
-      <span class="col-9">
-        <a [routerLink]="[ '/admin/moderation/video-abuses/list' ]" [queryParams]="{ 'search': 'tag:' + reason.id  }" class="chip rectangular bg-secondary text-light" *ngFor="let reason of getPredefinedReasons()">
-          <div>{{ reason.label }}</div>
-        </a>
-      </span>
-    </div>
-
-    <div *ngIf="videoAbuse.startAt" class="mt-2 d-flex">
-      <span class="col-3 moderation-expanded-label" i18n>Reported part</span>
-      <span class="col-9">
-        {{ startAt }}<ng-container *ngIf="videoAbuse.endAt"> - {{ endAt }}</ng-container>
-      </span>
-    </div>
-
-    <div class="mt-3 d-flex" *ngIf="videoAbuse.moderationComment">
-      <span class="col-3 moderation-expanded-label" i18n>Note</span>
-      <span class="col-9 moderation-expanded-text" [innerHTML]="videoAbuse.moderationCommentHtml"></span>
-    </div>
-
-  </div>
-
-  <!-- report right part (video details) -->
-  <div class="col-4">
-    <div class="screenratio">
-      <div *ngIf="videoAbuse.video.deleted || videoAbuse.video.blacklisted">
-        <span i18n *ngIf="videoAbuse.video.deleted">The video was deleted</span>
-        <span i18n *ngIf="!videoAbuse.video.deleted">The video was blocked</span>
-      </div>
-      <div *ngIf="!videoAbuse.video.deleted && !videoAbuse.video.blacklisted" [innerHTML]="videoAbuse.embedHtml"></div>
-    </div>
-  </div>
-</div>
index cfa514b260995de938cdd305a4c4f5f44543dd4f..adc18b587d46f1ad38121414b81a6984dcef49e6 100644 (file)
@@ -47,7 +47,7 @@ export class MyAccountNotificationPreferencesComponent implements OnInit {
     this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
 
     this.rightNotifications = {
-      videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
+      videoAbuseAsModerator: UserRight.MANAGE_ABUSES,
       videoAutoBlacklistAsModerator: UserRight.MANAGE_VIDEO_BLACKLIST,
       newUserRegistration: UserRight.MANAGE_USERS,
       newInstanceFollower: UserRight.MANAGE_SERVER_FOLLOW,
index 2dbe695c9c3f51420b7767b579d32e341a453c5e..0ea251f1c602cb7e1132959210a8d8067ecef6c4 100644 (file)
@@ -28,7 +28,7 @@ export class MenuComponent implements OnInit {
   private routesPerRight: { [ role in UserRight ]?: string } = {
     [UserRight.MANAGE_USERS]: '/admin/users',
     [UserRight.MANAGE_SERVER_FOLLOW]: '/admin/friends',
-    [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/moderation/video-abuses',
+    [UserRight.MANAGE_ABUSES]: '/admin/moderation/abuses',
     [UserRight.MANAGE_VIDEO_BLACKLIST]: '/admin/moderation/video-blocks',
     [UserRight.MANAGE_JOBS]: '/admin/jobs',
     [UserRight.MANAGE_CONFIGURATION]: '/admin/config'
@@ -126,7 +126,7 @@ export class MenuComponent implements OnInit {
     const adminRights = [
       UserRight.MANAGE_USERS,
       UserRight.MANAGE_SERVER_FOLLOW,
-      UserRight.MANAGE_VIDEO_ABUSES,
+      UserRight.MANAGE_ABUSES,
       UserRight.MANAGE_VIDEO_BLACKLIST,
       UserRight.MANAGE_JOBS,
       UserRight.MANAGE_CONFIGURATION
similarity index 81%
rename from client/src/app/shared/shared-forms/form-validators/video-abuse-validators.service.ts
rename to client/src/app/shared/shared-forms/form-validators/abuse-validators.service.ts
index aae56d6072151444e99d4fd27d535983c5d70a90..739115e1952238407671e874718f7e3ca07fcae4 100644 (file)
@@ -4,12 +4,12 @@ import { Injectable } from '@angular/core'
 import { BuildFormValidator } from './form-validator.service'
 
 @Injectable()
-export class VideoAbuseValidatorsService {
-  readonly VIDEO_ABUSE_REASON: BuildFormValidator
-  readonly VIDEO_ABUSE_MODERATION_COMMENT: BuildFormValidator
+export class AbuseValidatorsService {
+  readonly ABUSE_REASON: BuildFormValidator
+  readonly ABUSE_MODERATION_COMMENT: BuildFormValidator
 
   constructor (private i18n: I18n) {
-    this.VIDEO_ABUSE_REASON = {
+    this.ABUSE_REASON = {
       VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
       MESSAGES: {
         'required': this.i18n('Report reason is required.'),
@@ -18,7 +18,7 @@ export class VideoAbuseValidatorsService {
       }
     }
 
-    this.VIDEO_ABUSE_MODERATION_COMMENT = {
+    this.ABUSE_MODERATION_COMMENT = {
       VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
       MESSAGES: {
         'required': this.i18n('Moderation comment is required.'),
index 8b71841a9db3a68a844566592f44938675a8427c..b06a326ffb45f654edbb14148e88dcc39a25981e 100644 (file)
@@ -1,3 +1,4 @@
+export * from './abuse-validators.service'
 export * from './batch-domains-validators.service'
 export * from './custom-config-validators.service'
 export * from './form-validator.service'
@@ -6,7 +7,6 @@ export * from './instance-validators.service'
 export * from './login-validators.service'
 export * from './reset-password-validators.service'
 export * from './user-validators.service'
-export * from './video-abuse-validators.service'
 export * from './video-accept-ownership-validators.service'
 export * from './video-block-validators.service'
 export * from './video-captions-validators.service'
index e82fa97d436df9ab58f313be8db1504cadc09b61..ba33704cf23c9c27fa8fd4a59061481619448d12 100644 (file)
@@ -11,7 +11,7 @@ import {
   LoginValidatorsService,
   ResetPasswordValidatorsService,
   UserValidatorsService,
-  VideoAbuseValidatorsService,
+  AbuseValidatorsService,
   VideoAcceptOwnershipValidatorsService,
   VideoBlockValidatorsService,
   VideoCaptionsValidatorsService,
@@ -69,7 +69,7 @@ import { TimestampInputComponent } from './timestamp-input.component'
     LoginValidatorsService,
     ResetPasswordValidatorsService,
     UserValidatorsService,
-    VideoAbuseValidatorsService,
+    AbuseValidatorsService,
     VideoAcceptOwnershipValidatorsService,
     VideoBlockValidatorsService,
     VideoCaptionsValidatorsService,
index 5fc7989dd6c833f8eaa0fc3091bf484fc976078e..0fa161ce6fc3a1f6e25685089cd47bde44ab51ac 100644 (file)
@@ -14,7 +14,7 @@ export abstract class Actor implements ActorServer {
 
   avatarUrl: string
 
-  static GET_ACTOR_AVATAR_URL (actor: { avatar?: Avatar }) {
+  static GET_ACTOR_AVATAR_URL (actor: { avatar?: { url?: string, path: string } }) {
     if (actor?.avatar?.url) return actor.avatar.url
 
     if (actor && actor.avatar) {
index de25d3ab90aba90e3963c517e852aa20bb9228e0..389a242fd1a429e80f1d37f1dcaed238dee8f3b3 100644 (file)
@@ -25,9 +25,20 @@ export class UserNotification implements UserNotificationServer {
     video: VideoInfo
   }
 
-  videoAbuse?: {
+  abuse?: {
     id: number
-    video: VideoInfo
+
+    video?: VideoInfo
+
+    comment?: {
+      threadId: number
+
+      video: {
+        uuid: string
+      }
+    }
+
+    account?: ActorInfo
   }
 
   videoBlacklist?: {
@@ -55,7 +66,7 @@ export class UserNotification implements UserNotificationServer {
   // Additional fields
   videoUrl?: string
   commentUrl?: any[]
-  videoAbuseUrl?: string
+  abuseUrl?: string
   videoAutoBlacklistUrl?: string
   accountUrl?: string
   videoImportIdentifier?: string
@@ -78,7 +89,7 @@ export class UserNotification implements UserNotificationServer {
       this.comment = hash.comment
       if (this.comment) this.setAvatarUrl(this.comment.account)
 
-      this.videoAbuse = hash.videoAbuse
+      this.abuse = hash.abuse
 
       this.videoBlacklist = hash.videoBlacklist
 
@@ -108,8 +119,9 @@ export class UserNotification implements UserNotificationServer {
           break
 
         case UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS:
-          this.videoAbuseUrl = '/admin/moderation/video-abuses/list'
-          this.videoUrl = this.buildVideoUrl(this.videoAbuse.video)
+          this.abuseUrl = '/admin/moderation/abuses/list'
+
+          if (this.abuse.video) this.videoUrl = this.buildVideoUrl(this.abuse.video)
           break
 
         case UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS:
@@ -178,7 +190,7 @@ export class UserNotification implements UserNotificationServer {
     return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
   }
 
-  private setAvatarUrl (actor: { avatarUrl?: string, avatar?: Avatar }) {
+  private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { url?: string, path: string } }) {
     actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
   }
 }
index d5be1470ebe3cea2be27ef95cb5393983da9707e..8d31eab0d9af048749e4ceab4ddc74f4309a0022 100644 (file)
@@ -19,7 +19,7 @@
 
         <ng-template #noVideo>
           <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
-  
+
           <div class="message" i18n>
             The notification concerns a video now unavailable
           </div>
@@ -46,7 +46,7 @@
         <my-global-icon iconName="flag" aria-hidden="true"></my-global-icon>
 
         <div class="message" i18n>
-          <a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a>
+          <a (click)="markAsRead(notification)" [routerLink]="notification.abuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.abuse.video.name }}</a>
         </div>
       </ng-container>
 
@@ -65,7 +65,7 @@
           <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">
             <img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
           </a>
-  
+
           <div class="message" i18n>
             <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a>
           </div>
@@ -73,7 +73,7 @@
 
         <ng-template #noComment>
           <my-global-icon iconName="alert" aria-hidden="true"></my-global-icon>
-  
+
           <div class="message" i18n>
             The notification concerns a comment now unavailable
           </div>
similarity index 67%
rename from client/src/app/shared/shared-moderation/video-abuse.service.ts
rename to client/src/app/shared/shared-moderation/abuse.service.ts
index 44dea44a59e3673d490c3bd809ce838760905fc9..f45018d5c6cbf4c207bf63e2fb5acdf6b3002a0e 100644 (file)
@@ -5,12 +5,12 @@ import { catchError, map } from 'rxjs/operators'
 import { HttpClient, HttpParams } from '@angular/common/http'
 import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService } from '@app/core'
-import { ResultList, VideoAbuse, VideoAbuseCreate, VideoAbuseState, VideoAbuseUpdate } from '@shared/models'
+import { AbuseUpdate, ResultList, Abuse, AbuseCreate, AbuseState } from '@shared/models'
 import { environment } from '../../../environments/environment'
 
 @Injectable()
-export class VideoAbuseService {
-  private static BASE_VIDEO_ABUSE_URL = environment.apiUrl + '/api/v1/videos/'
+export class AbuseService {
+  private static BASE_ABUSE_URL = environment.apiUrl + '/api/v1/abuses'
 
   constructor (
     private authHttp: HttpClient,
@@ -18,13 +18,13 @@ export class VideoAbuseService {
     private restExtractor: RestExtractor
   ) {}
 
-  getVideoAbuses (options: {
+  getAbuses (options: {
     pagination: RestPagination,
     sort: SortMeta,
     search?: string
-  }): Observable<ResultList<VideoAbuse>> {
+  }): Observable<ResultList<Abuse>> {
     const { pagination, sort, search } = options
-    const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse'
+    const url = AbuseService.BASE_ABUSE_URL + 'abuse'
 
     let params = new HttpParams()
     params = this.restService.addRestGetParams(params, pagination, sort)
@@ -35,9 +35,9 @@ export class VideoAbuseService {
         state: {
           prefix: 'state:',
           handler: v => {
-            if (v === 'accepted') return VideoAbuseState.ACCEPTED
-            if (v === 'pending') return VideoAbuseState.PENDING
-            if (v === 'rejected') return VideoAbuseState.REJECTED
+            if (v === 'accepted') return AbuseState.ACCEPTED
+            if (v === 'pending') return AbuseState.PENDING
+            if (v === 'rejected') return AbuseState.REJECTED
 
             return undefined
           }
@@ -59,14 +59,14 @@ export class VideoAbuseService {
       params = this.restService.addObjectParams(params, filters)
     }
 
-    return this.authHttp.get<ResultList<VideoAbuse>>(url, { params })
+    return this.authHttp.get<ResultList<Abuse>>(url, { params })
                .pipe(
                  catchError(res => this.restExtractor.handleError(res))
                )
   }
 
-  reportVideo (parameters: { id: number } & VideoAbuseCreate) {
-    const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + parameters.id + '/abuse'
+  reportVideo (parameters: AbuseCreate) {
+    const url = AbuseService.BASE_ABUSE_URL
 
     const body = omit(parameters, [ 'id' ])
 
@@ -77,8 +77,8 @@ export class VideoAbuseService {
                )
   }
 
-  updateVideoAbuse (videoAbuse: VideoAbuse, abuseUpdate: VideoAbuseUpdate) {
-    const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
+  updateAbuse (abuse: Abuse, abuseUpdate: AbuseUpdate) {
+    const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
 
     return this.authHttp.put(url, abuseUpdate)
                .pipe(
@@ -87,8 +87,8 @@ export class VideoAbuseService {
                )
   }
 
-  removeVideoAbuse (videoAbuse: VideoAbuse) {
-    const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + videoAbuse.video.uuid + '/abuse/' + videoAbuse.id
+  removeAbuse (abuse: Abuse) {
+    const url = AbuseService.BASE_ABUSE_URL + '/' + abuse.id
 
     return this.authHttp.delete(url)
                .pipe(
index 8e74254f6957fb869f66c6c235abdb44d81d8064..d6c4a10be58373836cee239046c23cc6b71315ab 100644 (file)
@@ -1,3 +1,4 @@
+export * from './abuse.service'
 export * from './account-block.model'
 export * from './account-blocklist.component'
 export * from './batch-domains-modal.component'
@@ -6,7 +7,6 @@ export * from './bulk.service'
 export * from './server-blocklist.component'
 export * from './user-ban-modal.component'
 export * from './user-moderation-dropdown.component'
-export * from './video-abuse.service'
 export * from './video-block.component'
 export * from './video-block.service'
 export * from './video-report.component'
index f7e64dfa3422c95a41f33a5697bc3556079156cf..742193e58261311798d55b8c6a6366388d6a9d42 100644 (file)
@@ -8,7 +8,7 @@ import { BlocklistService } from './blocklist.service'
 import { BulkService } from './bulk.service'
 import { UserBanModalComponent } from './user-ban-modal.component'
 import { UserModerationDropdownComponent } from './user-moderation-dropdown.component'
-import { VideoAbuseService } from './video-abuse.service'
+import { AbuseService } from './abuse.service'
 import { VideoBlockComponent } from './video-block.component'
 import { VideoBlockService } from './video-block.service'
 import { VideoReportComponent } from './video-report.component'
@@ -39,7 +39,7 @@ import { VideoReportComponent } from './video-report.component'
   providers: [
     BlocklistService,
     BulkService,
-    VideoAbuseService,
+    AbuseService,
     VideoBlockService
   ]
 })
index 11c8056361b59da273cc95668917d4906f2ed41d..b8d9f8d270b50e9d9f4df124c2b2a3ad8f831707 100644 (file)
@@ -3,13 +3,13 @@ import { buildVideoEmbed, buildVideoLink } from 'src/assets/player/utils'
 import { Component, Input, OnInit, ViewChild } from '@angular/core'
 import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
 import { Notifier } from '@app/core'
-import { FormReactive, FormValidatorService, VideoAbuseValidatorsService } from '@app/shared/shared-forms'
+import { AbuseValidatorsService, FormReactive, FormValidatorService } from '@app/shared/shared-forms'
 import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
 import { I18n } from '@ngx-translate/i18n-polyfill'
-import { videoAbusePredefinedReasonsMap, VideoAbusePredefinedReasonsString } from '@shared/models/videos/abuse/video-abuse-reason.model'
+import { abusePredefinedReasonsMap, AbusePredefinedReasonsString } from '@shared/models'
 import { Video } from '../shared-main'
-import { VideoAbuseService } from './video-abuse.service'
+import { AbuseService } from './abuse.service'
 
 @Component({
   selector: 'my-video-report',
@@ -22,7 +22,7 @@ export class VideoReportComponent extends FormReactive implements OnInit {
   @ViewChild('modal', { static: true }) modal: NgbModal
 
   error: string = null
-  predefinedReasons: { id: VideoAbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
+  predefinedReasons: { id: AbusePredefinedReasonsString, label: string, description?: string, help?: string }[] = []
   embedHtml: SafeHtml
 
   private openedModal: NgbModalRef
@@ -30,8 +30,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
   constructor (
     protected formValidatorService: FormValidatorService,
     private modalService: NgbModal,
-    private videoAbuseValidatorsService: VideoAbuseValidatorsService,
-    private videoAbuseService: VideoAbuseService,
+    private abuseValidatorsService: AbuseValidatorsService,
+    private abuseService: AbuseService,
     private notifier: Notifier,
     private sanitizer: DomSanitizer,
     private i18n: I18n
@@ -69,8 +69,8 @@ export class VideoReportComponent extends FormReactive implements OnInit {
 
   ngOnInit () {
     this.buildForm({
-      reason: this.videoAbuseValidatorsService.VIDEO_ABUSE_REASON,
-      predefinedReasons: mapValues(videoAbusePredefinedReasonsMap, r => null),
+      reason: this.abuseValidatorsService.ABUSE_REASON,
+      predefinedReasons: mapValues(abusePredefinedReasonsMap, r => null),
       timestamp: {
         hasStart: null,
         startAt: null,
@@ -136,15 +136,18 @@ export class VideoReportComponent extends FormReactive implements OnInit {
 
   report () {
     const reason = this.form.get('reason').value
-    const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as VideoAbusePredefinedReasonsString[]
+    const predefinedReasons = Object.keys(pickBy(this.form.get('predefinedReasons').value)) as AbusePredefinedReasonsString[]
     const { hasStart, startAt, hasEnd, endAt } = this.form.get('timestamp').value
 
-    this.videoAbuseService.reportVideo({
-      id: this.video.id,
+    this.abuseService.reportVideo({
+      accountId: this.video.account.id,
       reason,
       predefinedReasons,
-      startAt: hasStart && startAt ? startAt : undefined,
-      endAt: hasEnd && endAt ? endAt : undefined
+      video: {
+        id: this.video.id,
+        startAt: hasStart && startAt ? startAt : undefined,
+        endAt: hasEnd && endAt ? endAt : undefined
+      }
     }).subscribe(
       () => {
         this.notifier.success(this.i18n('Video reported.'))
diff --git a/server/controllers/api/abuse.ts b/server/controllers/api/abuse.ts
new file mode 100644 (file)
index 0000000..ee046cb
--- /dev/null
@@ -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 } })
+}
index c334a26b48c3807ce7f66c2aa0e7c13ee4b7ba8e..eda9e04d197053bbb894bb0ea2ffe33d6c96a9bf 100644 (file)
@@ -3,6 +3,7 @@ import * as express from 'express'
 import * as RateLimit from 'express-rate-limit'
 import { badRequest } from '../../helpers/express-utils'
 import { CONFIG } from '../../initializers/config'
+import { abuseRouter } from './abuse'
 import { accountsRouter } from './accounts'
 import { bulkRouter } from './bulk'
 import { configRouter } from './config'
@@ -32,6 +33,7 @@ const apiRateLimiter = RateLimit({
 apiRouter.use(apiRateLimiter)
 
 apiRouter.use('/server', serverRouter)
+apiRouter.use('/abuses', abuseRouter)
 apiRouter.use('/bulk', bulkRouter)
 apiRouter.use('/oauth-clients', oauthClientsRouter)
 apiRouter.use('/config', configRouter)
index ab207445950daa9169502744b9b75a68e3774e36..b92a66360a684ad4599c742fea705a958690123e 100644 (file)
@@ -1,9 +1,10 @@
 import * as express from 'express'
-import { UserRight, VideoAbuseCreate, VideoAbuseState, VideoAbuse, videoAbusePredefinedReasonsMap } from '../../../../shared'
-import { logger } from '../../../helpers/logger'
+import { AbuseModel } from '@server/models/abuse/abuse'
+import { getServerActor } from '@server/models/application/application'
+import { AbuseCreate, UserRight, VideoAbuseCreate } from '../../../../shared'
 import { getFormattedObjects } from '../../../helpers/utils'
-import { sequelizeTypescript } from '../../../initializers/database'
 import {
+  abusesSortValidator,
   asyncMiddleware,
   asyncRetryTransactionMiddleware,
   authenticate,
@@ -12,28 +13,21 @@ import {
   setDefaultPagination,
   setDefaultSort,
   videoAbuseGetValidator,
+  videoAbuseListValidator,
   videoAbuseReportValidator,
-  videoAbusesSortValidator,
-  videoAbuseUpdateValidator,
-  videoAbuseListValidator
+  videoAbuseUpdateValidator
 } from '../../../middlewares'
-import { AccountModel } from '../../../models/account/account'
-import { VideoAbuseModel } from '../../../models/video/video-abuse'
-import { auditLoggerFactory, VideoAbuseAuditView } from '../../../helpers/audit-logger'
-import { Notifier } from '../../../lib/notifier'
-import { sendVideoAbuse } from '../../../lib/activitypub/send/send-flag'
-import { MVideoAbuseAccountVideo } from '../../../types/models/video'
-import { getServerActor } from '@server/models/application/application'
-import { MAccountDefault } from '@server/types/models'
+import { deleteAbuse, reportAbuse, updateAbuse } from '../abuse'
+
+// FIXME: deprecated in 2.3. Remove this controller
 
-const auditLogger = auditLoggerFactory('abuse')
 const abuseVideoRouter = express.Router()
 
 abuseVideoRouter.get('/abuse',
   authenticate,
-  ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
+  ensureUserHasRight(UserRight.MANAGE_ABUSES),
   paginationValidator,
-  videoAbusesSortValidator,
+  abusesSortValidator,
   setDefaultSort,
   setDefaultPagination,
   videoAbuseListValidator,
@@ -41,7 +35,7 @@ abuseVideoRouter.get('/abuse',
 )
 abuseVideoRouter.put('/:videoId/abuse/:id',
   authenticate,
-  ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
+  ensureUserHasRight(UserRight.MANAGE_ABUSES),
   asyncMiddleware(videoAbuseUpdateValidator),
   asyncRetryTransactionMiddleware(updateVideoAbuse)
 )
@@ -52,7 +46,7 @@ abuseVideoRouter.post('/:videoId/abuse',
 )
 abuseVideoRouter.delete('/:videoId/abuse/:id',
   authenticate,
-  ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
+  ensureUserHasRight(UserRight.MANAGE_ABUSES),
   asyncMiddleware(videoAbuseGetValidator),
   asyncRetryTransactionMiddleware(deleteVideoAbuse)
 )
@@ -69,11 +63,12 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
   const user = res.locals.oauth.token.user
   const serverActor = await getServerActor()
 
-  const resultList = await VideoAbuseModel.listForApi({
+  const resultList = await AbuseModel.listForApi({
     start: req.query.start,
     count: req.query.count,
     sort: req.query.sort,
     id: req.query.id,
+    filter: 'video',
     predefinedReason: req.query.predefinedReason,
     search: req.query.search,
     state: req.query.state,
@@ -90,74 +85,28 @@ async function listVideoAbuses (req: express.Request, res: express.Response) {
 }
 
 async function updateVideoAbuse (req: express.Request, res: express.Response) {
-  const videoAbuse = res.locals.videoAbuse
-
-  if (req.body.moderationComment !== undefined) videoAbuse.moderationComment = req.body.moderationComment
-  if (req.body.state !== undefined) videoAbuse.state = req.body.state
-
-  await sequelizeTypescript.transaction(t => {
-    return videoAbuse.save({ transaction: t })
-  })
-
-  // Do not send the delete to other instances, we updated OUR copy of this video abuse
-
-  return res.type('json').status(204).end()
+  return updateAbuse(req, res)
 }
 
 async function deleteVideoAbuse (req: express.Request, res: express.Response) {
-  const videoAbuse = res.locals.videoAbuse
-
-  await sequelizeTypescript.transaction(t => {
-    return videoAbuse.destroy({ transaction: t })
-  })
-
-  // Do not send the delete to other instances, we delete OUR copy of this video abuse
-
-  return res.type('json').status(204).end()
+  return deleteAbuse(req, res)
 }
 
 async function reportVideoAbuse (req: express.Request, res: express.Response) {
-  const videoInstance = res.locals.videoAll
-  const body: VideoAbuseCreate = req.body
-  let reporterAccount: MAccountDefault
-  let videoAbuseJSON: VideoAbuse
-
-  const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
-    reporterAccount = await AccountModel.load(res.locals.oauth.token.User.Account.id, t)
-    const predefinedReasons = body.predefinedReasons?.map(r => videoAbusePredefinedReasonsMap[r])
-
-    const abuseToCreate = {
-      reporterAccountId: reporterAccount.id,
-      reason: body.reason,
-      videoId: videoInstance.id,
-      state: VideoAbuseState.PENDING,
-      predefinedReasons,
-      startAt: body.startAt,
-      endAt: body.endAt
-    }
-
-    const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(abuseToCreate, { transaction: t })
-    videoAbuseInstance.Video = videoInstance
-    videoAbuseInstance.Account = reporterAccount
-
-    // We send the video abuse to the origin server
-    if (videoInstance.isOwned() === false) {
-      await sendVideoAbuse(reporterAccount.Actor, videoAbuseInstance, videoInstance, t)
-    }
+  const oldBody = req.body as VideoAbuseCreate
 
-    videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
-    auditLogger.create(reporterAccount.Actor.getIdentifier(), new VideoAbuseAuditView(videoAbuseJSON))
+  req.body = {
+    accountId: res.locals.videoAll.VideoChannel.accountId,
 
-    return videoAbuseInstance
-  })
+    reason: oldBody.reason,
+    predefinedReasons: oldBody.predefinedReasons,
 
-  Notifier.Instance.notifyOnNewVideoAbuse({
-    videoAbuse: videoAbuseJSON,
-    videoAbuseInstance,
-    reporter: reporterAccount.Actor.getIdentifier()
-  })
-
-  logger.info('Abuse report for video "%s" created.', videoInstance.name)
+    video: {
+      id: res.locals.videoAll.id,
+      startAt: oldBody.startAt,
+      endAt: oldBody.endAt
+    }
+  } as AbuseCreate
 
-  return res.json({ videoAbuse: videoAbuseJSON }).end()
+  return reportAbuse(req, res)
 }
index 0bbfbc753e559a3ba89465ff18d43aac6e4268a7..954b0b69da8a3abe9666c3484c562d8712d38092 100644 (file)
@@ -1,15 +1,15 @@
-import * as path from 'path'
-import * as express from 'express'
 import { diff } from 'deep-object-diff'
-import { chain } from 'lodash'
+import * as express from 'express'
 import * as flatten from 'flat'
+import { chain } from 'lodash'
+import * as path from 'path'
 import * as winston from 'winston'
-import { jsonLoggerFormat, labelFormatter } from './logger'
-import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared'
-import { VideoComment } from '../../shared/models/videos/video-comment.model'
+import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
+import { Abuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
 import { CustomConfig } from '../../shared/models/server/custom-config.model'
+import { VideoComment } from '../../shared/models/videos/video-comment.model'
 import { CONFIG } from '../initializers/config'
-import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
+import { jsonLoggerFormat, labelFormatter } from './logger'
 
 function getAuditIdFromRes (res: express.Response) {
   return res.locals.oauth.token.User.username
@@ -212,18 +212,15 @@ class VideoChannelAuditView extends EntityAuditView {
   }
 }
 
-const videoAbuseKeysToKeep = [
+const abuseKeysToKeep = [
   'id',
   'reason',
   'reporterAccount',
-  'video-id',
-  'video-name',
-  'video-uuid',
   'createdAt'
 ]
-class VideoAbuseAuditView extends EntityAuditView {
-  constructor (private readonly videoAbuse: VideoAbuse) {
-    super(videoAbuseKeysToKeep, 'abuse', videoAbuse)
+class AbuseAuditView extends EntityAuditView {
+  constructor (private readonly abuse: Abuse) {
+    super(abuseKeysToKeep, 'abuse', abuse)
   }
 }
 
@@ -274,6 +271,6 @@ export {
   CommentAuditView,
   UserAuditView,
   VideoAuditView,
-  VideoAbuseAuditView,
+  AbuseAuditView,
   CustomConfigAuditView
 }
diff --git a/server/helpers/custom-validators/abuses.ts b/server/helpers/custom-validators/abuses.ts
new file mode 100644 (file)
index 0000000..a6a895c
--- /dev/null
@@ -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
+}
index 6452e297cb9322af39d48f2fdfa39f25827a119d..dc90b366702ddc755f9658d726075ea9c6ddbee1 100644 (file)
@@ -1,9 +1,9 @@
 import { isActivityPubUrlValid } from './misc'
-import { isVideoAbuseReasonValid } from '../video-abuses'
+import { isAbuseReasonValid } from '../abuses'
 
 function isFlagActivityValid (activity: any) {
   return activity.type === 'Flag' &&
-    isVideoAbuseReasonValid(activity.content) &&
+    isAbuseReasonValid(activity.content) &&
     isActivityPubUrlValid(activity.object)
 }
 
diff --git a/server/helpers/custom-validators/video-abuses.ts b/server/helpers/custom-validators/video-abuses.ts
deleted file mode 100644 (file)
index 0c2c342..0000000
+++ /dev/null
@@ -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
-}
similarity index 56%
rename from server/helpers/middlewares/video-abuses.ts
rename to server/helpers/middlewares/abuses.ts
index 97a5724b6dd69afd69ad3c57051896ff1af310a9..3906f67604893ad76119ef972e82d472730dd1e4 100644 (file)
@@ -1,19 +1,20 @@
 import { Response } from 'express'
-import { VideoAbuseModel } from '../../models/video/video-abuse'
+import { AbuseModel } from '../../models/abuse/abuse'
 import { fetchVideo } from '../video'
 
+// FIXME: deprecated in 2.3. Remove this function
 async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
   const abuseId = parseInt(abuseIdArg + '', 10)
-  let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
+  let abuse = await AbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
 
-  if (!videoAbuse) {
+  if (!abuse) {
     const userId = res.locals.oauth?.token.User.id
     const video = await fetchVideo(videoUUID, 'all', userId)
 
-    if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id)
+    if (video) abuse = await AbuseModel.loadByIdAndVideoId(abuseId, video.id)
   }
 
-  if (videoAbuse === null) {
+  if (abuse === null) {
     res.status(404)
        .json({ error: 'Video abuse not found' })
        .end()
@@ -21,12 +22,17 @@ async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: stri
     return false
   }
 
-  res.locals.videoAbuse = videoAbuse
+  res.locals.abuse = abuse
   return true
 }
 
+async function doesAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
+
+}
+
 // ---------------------------------------------------------------------------
 
 export {
+  doesAbuseExist,
   doesVideoAbuseExist
 }
index f91aeaa1259417dd3ec2c3cd28dc6199a06fb277..f57f3ad310f797d2d314e1c16fa2625da893ca81 100644 (file)
@@ -1,5 +1,5 @@
+export * from './abuses'
 export * from './accounts'
-export * from './video-abuses'
 export * from './video-blacklists'
 export * from './video-captions'
 export * from './video-channels'
index e730e3c84080ddff3ef105e93892d2d773ae83dd..8f86bbbef62a9999e01311587d874d1b9e4b293b 100644 (file)
@@ -1,9 +1,17 @@
 import { join } from 'path'
 import { randomBytes } from 'crypto'
-import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models'
 import { ActivityPubActorType } from '../../shared/models/activitypub'
 import { FollowState } from '../../shared/models/actors'
-import { VideoAbuseState, VideoImportState, VideoPrivacy, VideoTranscodingFPS } from '../../shared/models/videos'
+import {
+  AbuseState,
+  VideoImportState,
+  VideoPrivacy,
+  VideoTranscodingFPS,
+  JobType,
+  VideoRateType,
+  VideoResolution,
+  VideoState
+} from '../../shared/models'
 // Do not use barrels, remain constants as independent as possible
 import { isTestInstance, sanitizeHost, sanitizeUrl, root } from '../helpers/core-utils'
 import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
@@ -51,7 +59,6 @@ const SORTABLE_COLUMNS = {
   USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
   ACCOUNTS: [ 'createdAt' ],
   JOBS: [ 'createdAt' ],
-  VIDEO_ABUSES: [ 'id', 'createdAt', 'state' ],
   VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
   VIDEO_IMPORTS: [ 'createdAt' ],
   VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ],
@@ -66,6 +73,8 @@ const SORTABLE_COLUMNS = {
   VIDEOS_SEARCH: [ 'name', 'duration', 'createdAt', 'publishedAt', 'originallyPublishedAt', 'views', 'likes', 'match' ],
   VIDEO_CHANNELS_SEARCH: [ 'match', 'displayName', 'createdAt' ],
 
+  ABUSES: [ 'id', 'createdAt', 'state' ],
+
   ACCOUNTS_BLOCKLIST: [ 'createdAt' ],
   SERVERS_BLOCKLIST: [ 'createdAt' ],
 
@@ -193,7 +202,7 @@ const CONSTRAINTS_FIELDS = {
     VIDEO_LANGUAGES: { max: 500 }, // Array length
     BLOCKED_REASON: { min: 3, max: 250 } // Length
   },
-  VIDEO_ABUSES: {
+  ABUSES: {
     REASON: { min: 2, max: 3000 }, // Length
     MODERATION_COMMENT: { min: 2, max: 3000 } // Length
   },
@@ -378,10 +387,10 @@ const VIDEO_IMPORT_STATES = {
   [VideoImportState.REJECTED]: 'Rejected'
 }
 
-const VIDEO_ABUSE_STATES = {
-  [VideoAbuseState.PENDING]: 'Pending',
-  [VideoAbuseState.REJECTED]: 'Rejected',
-  [VideoAbuseState.ACCEPTED]: 'Accepted'
+const ABUSE_STATES = {
+  [AbuseState.PENDING]: 'Pending',
+  [AbuseState.REJECTED]: 'Rejected',
+  [AbuseState.ACCEPTED]: 'Accepted'
 }
 
 const VIDEO_PLAYLIST_PRIVACIES = {
@@ -778,7 +787,7 @@ export {
   VIDEO_RATE_TYPES,
   VIDEO_TRANSCODING_FPS,
   FFMPEG_NICE,
-  VIDEO_ABUSE_STATES,
+  ABUSE_STATES,
   VIDEO_CHANNELS,
   LRU_CACHE,
   JOB_REQUEST_TIMEOUT,
index 633d4f95645eac497265a5ccb32cbd06e8448caf..0775f1fadc75ed56ede1826f62ee7cc440821c00 100644 (file)
@@ -1,44 +1,45 @@
+import { QueryTypes, Transaction } from 'sequelize'
 import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
+import { AbuseModel } from '@server/models/abuse/abuse'
+import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
+import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
 import { isTestInstance } from '../helpers/core-utils'
 import { logger } from '../helpers/logger'
-
 import { AccountModel } from '../models/account/account'
+import { AccountBlocklistModel } from '../models/account/account-blocklist'
 import { AccountVideoRateModel } from '../models/account/account-video-rate'
 import { UserModel } from '../models/account/user'
+import { UserNotificationModel } from '../models/account/user-notification'
+import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
+import { UserVideoHistoryModel } from '../models/account/user-video-history'
 import { ActorModel } from '../models/activitypub/actor'
 import { ActorFollowModel } from '../models/activitypub/actor-follow'
 import { ApplicationModel } from '../models/application/application'
 import { AvatarModel } from '../models/avatar/avatar'
 import { OAuthClientModel } from '../models/oauth/oauth-client'
 import { OAuthTokenModel } from '../models/oauth/oauth-token'
+import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
+import { PluginModel } from '../models/server/plugin'
 import { ServerModel } from '../models/server/server'
+import { ServerBlocklistModel } from '../models/server/server-blocklist'
+import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
 import { TagModel } from '../models/video/tag'
+import { ThumbnailModel } from '../models/video/thumbnail'
 import { VideoModel } from '../models/video/video'
-import { VideoAbuseModel } from '../models/video/video-abuse'
 import { VideoBlacklistModel } from '../models/video/video-blacklist'
+import { VideoCaptionModel } from '../models/video/video-caption'
+import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
 import { VideoChannelModel } from '../models/video/video-channel'
 import { VideoCommentModel } from '../models/video/video-comment'
 import { VideoFileModel } from '../models/video/video-file'
-import { VideoShareModel } from '../models/video/video-share'
-import { VideoTagModel } from '../models/video/video-tag'
-import { CONFIG } from './config'
-import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
-import { VideoCaptionModel } from '../models/video/video-caption'
 import { VideoImportModel } from '../models/video/video-import'
-import { VideoViewModel } from '../models/video/video-view'
-import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
-import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
-import { UserVideoHistoryModel } from '../models/account/user-video-history'
-import { AccountBlocklistModel } from '../models/account/account-blocklist'
-import { ServerBlocklistModel } from '../models/server/server-blocklist'
-import { UserNotificationModel } from '../models/account/user-notification'
-import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
-import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
 import { VideoPlaylistModel } from '../models/video/video-playlist'
 import { VideoPlaylistElementModel } from '../models/video/video-playlist-element'
-import { ThumbnailModel } from '../models/video/thumbnail'
-import { PluginModel } from '../models/server/plugin'
-import { QueryTypes, Transaction } from 'sequelize'
+import { VideoShareModel } from '../models/video/video-share'
+import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
+import { VideoTagModel } from '../models/video/video-tag'
+import { VideoViewModel } from '../models/video/video-view'
+import { CONFIG } from './config'
 
 require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
 
@@ -86,6 +87,8 @@ async function initDatabaseModels (silent: boolean) {
     TagModel,
     AccountVideoRateModel,
     UserModel,
+    AbuseModel,
+    VideoCommentAbuseModel,
     VideoAbuseModel,
     VideoModel,
     VideoChangeOwnershipModel,
index 50de25182499bce398ab1b7bde221ec62a3a153d..e4993c393b978ccbca2e2a76b163be71fc3a7baf 100644 (file)
@@ -1,5 +1,5 @@
 import * as Sequelize from 'sequelize'
-import { VideoAbuseState } from '../../../shared/models/videos'
+import { AbuseState } from '../../../shared/models'
 
 async function up (utils: {
   transaction: Sequelize.Transaction
@@ -16,7 +16,7 @@ async function up (utils: {
   }
 
   {
-    const query = 'UPDATE "videoAbuse" SET "state" = ' + VideoAbuseState.PENDING
+    const query = 'UPDATE "videoAbuse" SET "state" = ' + AbuseState.PENDING
     await utils.sequelize.query(query)
   }
 
index 1d7132a3a7010f8a04ac94747ae12af42e20070f..6350cee12816e92f8277f168b40d10e9222d4375 100644 (file)
@@ -1,24 +1,19 @@
-import {
-  ActivityCreate,
-  ActivityFlag,
-  VideoAbuseState,
-  videoAbusePredefinedReasonsMap
-} from '../../../../shared'
-import { VideoAbuseObject } from '../../../../shared/models/activitypub/objects'
+import { createAccountAbuse, createVideoAbuse, createVideoCommentAbuse } from '@server/lib/moderation'
+import { AccountModel } from '@server/models/account/account'
+import { VideoModel } from '@server/models/video/video'
+import { VideoCommentModel } from '@server/models/video/video-comment'
+import { AbuseObject, abusePredefinedReasonsMap, AbuseState, ActivityCreate, ActivityFlag } from '../../../../shared'
+import { getAPId } from '../../../helpers/activitypub'
 import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { logger } from '../../../helpers/logger'
 import { sequelizeTypescript } from '../../../initializers/database'
-import { VideoAbuseModel } from '../../../models/video/video-abuse'
-import { getOrCreateVideoAndAccountAndChannel } from '../videos'
-import { Notifier } from '../../notifier'
-import { getAPId } from '../../../helpers/activitypub'
 import { APProcessorOptions } from '../../../types/activitypub-processor.model'
-import { MActorSignature, MVideoAbuseAccountVideo } from '../../../types/models'
-import { AccountModel } from '@server/models/account/account'
+import { MAccountDefault, MActorSignature, MCommentOwnerVideo } from '../../../types/models'
 
 async function processFlagActivity (options: APProcessorOptions<ActivityCreate | ActivityFlag>) {
   const { activity, byActor } = options
-  return retryTransactionWrapper(processCreateVideoAbuse, activity, byActor)
+
+  return retryTransactionWrapper(processCreateAbuse, activity, byActor)
 }
 
 // ---------------------------------------------------------------------------
@@ -29,55 +24,79 @@ export {
 
 // ---------------------------------------------------------------------------
 
-async function processCreateVideoAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
-  const flag = activity.type === 'Flag' ? activity : (activity.object as VideoAbuseObject)
+async function processCreateAbuse (activity: ActivityCreate | ActivityFlag, byActor: MActorSignature) {
+  const flag = activity.type === 'Flag' ? activity : (activity.object as AbuseObject)
 
   const account = byActor.Account
-  if (!account) throw new Error('Cannot create video abuse with the non account actor ' + byActor.url)
+  if (!account) throw new Error('Cannot create abuse with the non account actor ' + byActor.url)
+
+  const reporterAccount = await AccountModel.load(account.id)
 
   const objects = Array.isArray(flag.object) ? flag.object : [ flag.object ]
 
+  const tags = Array.isArray(flag.tag) ? flag.tag : []
+  const predefinedReasons = tags.map(tag => abusePredefinedReasonsMap[tag.name])
+                                .filter(v => !isNaN(v))
+
+  const startAt = flag.startAt
+  const endAt = flag.endAt
+
   for (const object of objects) {
     try {
-      logger.debug('Reporting remote abuse for video %s.', getAPId(object))
-
-      const { video } = await getOrCreateVideoAndAccountAndChannel({ videoObject: object })
-      const reporterAccount = await sequelizeTypescript.transaction(async t => AccountModel.load(account.id, t))
-      const tags = Array.isArray(flag.tag) ? flag.tag : []
-      const predefinedReasons = tags.map(tag => videoAbusePredefinedReasonsMap[tag.name])
-                                    .filter(v => !isNaN(v))
-      const startAt = flag.startAt
-      const endAt = flag.endAt
-
-      const videoAbuseInstance = await sequelizeTypescript.transaction(async t => {
-        const videoAbuseData = {
-          reporterAccountId: account.id,
-          reason: flag.content,
-          videoId: video.id,
-          state: VideoAbuseState.PENDING,
-          predefinedReasons,
-          startAt,
-          endAt
-        }
+      const uri = getAPId(object)
 
-        const videoAbuseInstance: MVideoAbuseAccountVideo = await VideoAbuseModel.create(videoAbuseData, { transaction: t })
-        videoAbuseInstance.Video = video
-        videoAbuseInstance.Account = reporterAccount
+      logger.debug('Reporting remote abuse for object %s.', uri)
 
-        logger.info('Remote abuse for video uuid %s created', flag.object)
+      await sequelizeTypescript.transaction(async t => {
 
-        return videoAbuseInstance
-      })
+        const video = await VideoModel.loadByUrlAndPopulateAccount(uri)
+        let videoComment: MCommentOwnerVideo
+        let flaggedAccount: MAccountDefault
+
+        if (!video) videoComment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(uri)
+        if (!videoComment) flaggedAccount = await AccountModel.loadByUrl(uri)
+
+        if (!video && !videoComment && !flaggedAccount) {
+          logger.warn('Cannot flag unknown entity %s.', object)
+          return
+        }
+
+        const baseAbuse = {
+          reporterAccountId: reporterAccount.id,
+          reason: flag.content,
+          state: AbuseState.PENDING,
+          predefinedReasons
+        }
 
-      const videoAbuseJSON = videoAbuseInstance.toFormattedJSON()
+        if (video) {
+          return createVideoAbuse({
+            baseAbuse,
+            startAt,
+            endAt,
+            reporterAccount,
+            transaction: t,
+            videoInstance: video
+          })
+        }
+
+        if (videoComment) {
+          return createVideoCommentAbuse({
+            baseAbuse,
+            reporterAccount,
+            transaction: t,
+            commentInstance: videoComment
+          })
+        }
 
-      Notifier.Instance.notifyOnNewVideoAbuse({
-        videoAbuse: videoAbuseJSON,
-        videoAbuseInstance,
-        reporter: reporterAccount.Actor.getIdentifier()
+        return await createAccountAbuse({
+          baseAbuse,
+          reporterAccount,
+          transaction: t,
+          accountInstance: flaggedAccount
+        })
       })
     } catch (err) {
-      logger.debug('Cannot process report of %s. (Maybe not a video abuse).', getAPId(object), { err })
+      logger.debug('Cannot process report of %s', getAPId(object), { err })
     }
   }
 }
index 3a1fe08122e95a5547eafffdc36b81bd290b9f19..821637ec84b871858b2d9f72a8c6971f44abef7f 100644 (file)
@@ -1,32 +1,31 @@
-import { getVideoAbuseActivityPubUrl } from '../url'
-import { unicastTo } from './utils'
-import { logger } from '../../../helpers/logger'
+import { Transaction } from 'sequelize'
 import { ActivityAudience, ActivityFlag } from '../../../../shared/models/activitypub'
+import { logger } from '../../../helpers/logger'
+import { MAbuseAP, MAccountLight, MActor } from '../../../types/models'
 import { audiencify, getAudience } from '../audience'
-import { Transaction } from 'sequelize'
-import { MActor, MVideoFullLight } from '../../../types/models'
-import { MVideoAbuseVideo } from '../../../types/models/video'
+import { getAbuseActivityPubUrl } from '../url'
+import { unicastTo } from './utils'
 
-function sendVideoAbuse (byActor: MActor, videoAbuse: MVideoAbuseVideo, video: MVideoFullLight, t: Transaction) {
-  if (!video.VideoChannel.Account.Actor.serverId) return // Local user
+function sendAbuse (byActor: MActor, abuse: MAbuseAP, flaggedAccount: MAccountLight, t: Transaction) {
+  if (!flaggedAccount.Actor.serverId) return // Local user
 
-  const url = getVideoAbuseActivityPubUrl(videoAbuse)
+  const url = getAbuseActivityPubUrl(abuse)
 
-  logger.info('Creating job to send video abuse %s.', url)
+  logger.info('Creating job to send abuse %s.', url)
 
   // Custom audience, we only send the abuse to the origin instance
-  const audience = { to: [ video.VideoChannel.Account.Actor.url ], cc: [] }
-  const flagActivity = buildFlagActivity(url, byActor, videoAbuse, audience)
+  const audience = { to: [ flaggedAccount.Actor.url ], cc: [] }
+  const flagActivity = buildFlagActivity(url, byActor, abuse, audience)
 
-  t.afterCommit(() => unicastTo(flagActivity, byActor, video.VideoChannel.Account.Actor.getSharedInbox()))
+  t.afterCommit(() => unicastTo(flagActivity, byActor, flaggedAccount.Actor.getSharedInbox()))
 }
 
-function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbuseVideo, audience: ActivityAudience): ActivityFlag {
+function buildFlagActivity (url: string, byActor: MActor, abuse: MAbuseAP, audience: ActivityAudience): ActivityFlag {
   if (!audience) audience = getAudience(byActor)
 
   const activity = Object.assign(
     { id: url, actor: byActor.url },
-    videoAbuse.toActivityPubObject()
+    abuse.toActivityPubObject()
   )
 
   return audiencify(activity, audience)
@@ -35,5 +34,5 @@ function buildFlagActivity (url: string, byActor: MActor, videoAbuse: MVideoAbus
 // ---------------------------------------------------------------------------
 
 export {
-  sendVideoAbuse
+  sendAbuse
 }
index 7f98751a1263814abc42a7af7e9649f04b92252b..b54e038a43f5684f6080c62cf0e2ba69505b8a06 100644 (file)
@@ -5,10 +5,10 @@ import {
   MActorId,
   MActorUrl,
   MCommentId,
-  MVideoAbuseId,
   MVideoId,
   MVideoUrl,
-  MVideoUUID
+  MVideoUUID,
+  MAbuseId
 } from '../../types/models'
 import { MVideoPlaylist, MVideoPlaylistUUID } from '../../types/models/video/video-playlist'
 import { MVideoFileVideoUUID } from '../../types/models/video/video-file'
@@ -48,8 +48,8 @@ function getAccountActivityPubUrl (accountName: string) {
   return WEBSERVER.URL + '/accounts/' + accountName
 }
 
-function getVideoAbuseActivityPubUrl (videoAbuse: MVideoAbuseId) {
-  return WEBSERVER.URL + '/admin/video-abuses/' + videoAbuse.id
+function getAbuseActivityPubUrl (abuse: MAbuseId) {
+  return WEBSERVER.URL + '/admin/abuses/' + abuse.id
 }
 
 function getVideoViewActivityPubUrl (byActor: MActorUrl, video: MVideoId) {
@@ -118,7 +118,7 @@ export {
   getVideoCacheStreamingPlaylistActivityPubUrl,
   getVideoChannelActivityPubUrl,
   getAccountActivityPubUrl,
-  getVideoAbuseActivityPubUrl,
+  getAbuseActivityPubUrl,
   getActorFollowActivityPubUrl,
   getActorFollowAcceptActivityPubUrl,
   getVideoAnnounceActivityPubUrl,
index c08732b4833de362798a3afeedb9a7dbeebea050..e821aea5fba3a8a73f639b5aa0b750f174ad374c 100644 (file)
@@ -1,26 +1,20 @@
+import { readFileSync } from 'fs-extra'
+import { merge } from 'lodash'
 import { createTransport, Transporter } from 'nodemailer'
+import { join } from 'path'
+import { VideoChannelModel } from '@server/models/video/video-channel'
+import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
+import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
+import { Abuse, EmailPayload } from '@shared/models'
+import { SendEmailOptions } from '../../shared/models/server/emailer.model'
 import { isTestInstance, root } from '../helpers/core-utils'
 import { bunyanLogger, logger } from '../helpers/logger'
 import { CONFIG, isEmailEnabled } from '../initializers/config'
-import { JobQueue } from './job-queue'
-import { readFileSync } from 'fs-extra'
 import { WEBSERVER } from '../initializers/constants'
-import {
-  MCommentOwnerVideo,
-  MVideo,
-  MVideoAbuseVideo,
-  MVideoAccountLight,
-  MVideoBlacklistLightVideo,
-  MVideoBlacklistVideo
-} from '../types/models/video'
-import { MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
-import { MVideoImport, MVideoImportVideo } from '@server/types/models/video/video-import'
-import { EmailPayload } from '@shared/models'
-import { join } from 'path'
-import { VideoAbuse } from '../../shared/models/videos'
-import { SendEmailOptions } from '../../shared/models/server/emailer.model'
-import { merge } from 'lodash'
-import { VideoChannelModel } from '@server/models/video/video-channel'
+import { MAbuseFull, MActorFollowActors, MActorFollowFull, MUser } from '../types/models'
+import { MCommentOwnerVideo, MVideo, MVideoAccountLight } from '../types/models/video'
+import { JobQueue } from './job-queue'
+
 const Email = require('email-templates')
 
 class Emailer {
@@ -288,28 +282,70 @@ class Emailer {
     return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
   }
 
-  addVideoAbuseModeratorsNotification (to: string[], parameters: {
-    videoAbuse: VideoAbuse
-    videoAbuseInstance: MVideoAbuseVideo
+  addAbuseModeratorsNotification (to: string[], parameters: {
+    abuse: Abuse
+    abuseInstance: MAbuseFull
     reporter: string
   }) {
-    const videoAbuseUrl = WEBSERVER.URL + '/admin/moderation/video-abuses/list?search=%23' + parameters.videoAbuse.id
-    const videoUrl = WEBSERVER.URL + parameters.videoAbuseInstance.Video.getWatchStaticPath()
+    const { abuse, abuseInstance, reporter } = parameters
 
-    const emailPayload: EmailPayload = {
-      template: 'video-abuse-new',
-      to,
-      subject: `New video abuse report from ${parameters.reporter}`,
-      locals: {
-        videoUrl,
-        videoAbuseUrl,
-        videoCreatedAt: new Date(parameters.videoAbuseInstance.Video.createdAt).toLocaleString(),
-        videoPublishedAt: new Date(parameters.videoAbuseInstance.Video.publishedAt).toLocaleString(),
-        videoAbuse: parameters.videoAbuse,
-        reporter: parameters.reporter,
-        action: {
-          text: 'View report #' + parameters.videoAbuse.id,
-          url: videoAbuseUrl
+    const action = {
+      text: 'View report #' + abuse.id,
+      url: WEBSERVER.URL + '/admin/moderation/abuses/list?search=%23' + abuse.id
+    }
+
+    let emailPayload: EmailPayload
+
+    if (abuseInstance.VideoAbuse) {
+      const video = abuseInstance.VideoAbuse.Video
+      const videoUrl = WEBSERVER.URL + video.getWatchStaticPath()
+
+      emailPayload = {
+        template: 'video-abuse-new',
+        to,
+        subject: `New video abuse report from ${reporter}`,
+        locals: {
+          videoUrl,
+          isLocal: video.remote === false,
+          videoCreatedAt: new Date(video.createdAt).toLocaleString(),
+          videoPublishedAt: new Date(video.publishedAt).toLocaleString(),
+          videoName: video.name,
+          reason: abuse.reason,
+          videoChannel: video.VideoChannel,
+          action
+        }
+      }
+    } else if (abuseInstance.VideoCommentAbuse) {
+      const comment = abuseInstance.VideoCommentAbuse.VideoComment
+      const commentUrl = WEBSERVER.URL + comment.Video.getWatchStaticPath() + ';threadId=' + comment.getThreadId()
+
+      emailPayload = {
+        template: 'comment-abuse-new',
+        to,
+        subject: `New comment abuse report from ${reporter}`,
+        locals: {
+          commentUrl,
+          isLocal: comment.isOwned(),
+          commentCreatedAt: new Date(comment.createdAt).toLocaleString(),
+          reason: abuse.reason,
+          flaggedAccount: abuseInstance.FlaggedAccount.getDisplayName(),
+          action
+        }
+      }
+    } else {
+      const account = abuseInstance.FlaggedAccount
+      const accountUrl = account.getClientUrl()
+
+      emailPayload = {
+        template: 'account-abuse-new',
+        to,
+        subject: `New account abuse report from ${reporter}`,
+        locals: {
+          accountUrl,
+          accountDisplayName: account.getDisplayName(),
+          isLocal: account.isOwned(),
+          reason: abuse.reason,
+          action
         }
       }
     }
diff --git a/server/lib/emails/account-abuse-new/html.pug b/server/lib/emails/account-abuse-new/html.pug
new file mode 100644 (file)
index 0000000..06be802
--- /dev/null
@@ -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;")
index 76b805a24f2d9ca0737b09083d43b426ce286dd1..8312118643d0f8426076fb2bacb3d62f992314a1 100644 (file)
@@ -1,3 +1,7 @@
 mixin channel(channel)
   - var handle = `${channel.name}@${channel.host}`
-  | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
\ No newline at end of file
+  | #[a(href=`${WEBSERVER.URL}/video-channels/${handle}` title=handle) #{channel.displayName}]
+
+mixin account(account)
+  - var handle = `${account.name}@${account.host}`
+  | #[a(href=`${WEBSERVER.URL}/accounts/${handle}` title=handle) #{account.displayName}]
index 999c89d26e07e14684a6ac71ad8f72c7f5c26129..a1acdabdccdacdb569743897dc1c55513ae2eac0 100644 (file)
@@ -6,13 +6,13 @@ block title
 
 block content
   p
-    | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{videoAbuse.video.channel.isLocal ? '' : 'remote '}video "
-    a(href=videoUrl) #{videoAbuse.video.name}
-    | " by #[+channel(videoAbuse.video.channel)]
+    | #[a(href=WEBSERVER.URL) #{WEBSERVER.HOST}] received an abuse report for the #{isLocal ? '' : 'remote '}video "
+    a(href=videoUrl) #{videoName}
+    | " by #[+channel(videoChannel)]
     if videoPublishedAt
       | , published the #{videoPublishedAt}.
     else
       | , uploaded the #{videoCreatedAt} but not yet published.
   p The reporter, #{reporter}, cited the following reason(s):
-  blockquote #{videoAbuse.reason}
+  blockquote #{reason}
   br(style="display: none;")
diff --git a/server/lib/emails/video-comment-abuse-new/html.pug b/server/lib/emails/video-comment-abuse-new/html.pug
new file mode 100644 (file)
index 0000000..170b795
--- /dev/null
@@ -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;")
index 60d1b40537f3a88f53efe8e1922f059dced1f06f..4fc9cd747609a22d0d612caafc8e6c1fb944b566 100644 (file)
@@ -1,15 +1,33 @@
-import { VideoModel } from '../models/video/video'
-import { VideoCommentModel } from '../models/video/video-comment'
-import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
+import { PathLike } from 'fs-extra'
+import { Transaction } from 'sequelize/types'
+import { AbuseAuditView, auditLoggerFactory } from '@server/helpers/audit-logger'
+import { logger } from '@server/helpers/logger'
+import { AbuseModel } from '@server/models/abuse/abuse'
+import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
+import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
+import { VideoFileModel } from '@server/models/video/video-file'
+import { FilteredModelAttributes } from '@server/types'
+import {
+  MAbuseFull,
+  MAccountDefault,
+  MAccountLight,
+  MCommentAbuseAccountVideo,
+  MCommentOwnerVideo,
+  MUser,
+  MVideoAbuseVideoFull,
+  MVideoAccountLightBlacklistAllFiles
+} from '@server/types/models'
+import { ActivityCreate } from '../../shared/models/activitypub'
+import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
+import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
 import { VideoCreate, VideoImportCreate } from '../../shared/models/videos'
+import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
 import { UserModel } from '../models/account/user'
-import { VideoTorrentObject } from '../../shared/models/activitypub/objects'
-import { ActivityCreate } from '../../shared/models/activitypub'
 import { ActorModel } from '../models/activitypub/actor'
-import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
-import { VideoFileModel } from '@server/models/video/video-file'
-import { PathLike } from 'fs-extra'
-import { MUser } from '@server/types/models'
+import { VideoModel } from '../models/video/video'
+import { VideoCommentModel } from '../models/video/video-comment'
+import { sendAbuse } from './activitypub/send/send-flag'
+import { Notifier } from './notifier'
 
 export type AcceptResult = {
   accepted: boolean
@@ -73,6 +91,89 @@ function isPostImportVideoAccepted (object: {
   return { accepted: true }
 }
 
+async function createVideoAbuse (options: {
+  baseAbuse: FilteredModelAttributes<AbuseModel>
+  videoInstance: MVideoAccountLightBlacklistAllFiles
+  startAt: number
+  endAt: number
+  transaction: Transaction
+  reporterAccount: MAccountDefault
+}) {
+  const { baseAbuse, videoInstance, startAt, endAt, transaction, reporterAccount } = options
+
+  const associateFun = async (abuseInstance: MAbuseFull) => {
+    const videoAbuseInstance: MVideoAbuseVideoFull = await VideoAbuseModel.create({
+      abuseId: abuseInstance.id,
+      videoId: videoInstance.id,
+      startAt: startAt,
+      endAt: endAt
+    }, { transaction })
+
+    videoAbuseInstance.Video = videoInstance
+    abuseInstance.VideoAbuse = videoAbuseInstance
+
+    return { isOwned: videoInstance.isOwned() }
+  }
+
+  return createAbuse({
+    base: baseAbuse,
+    reporterAccount,
+    flaggedAccount: videoInstance.VideoChannel.Account,
+    transaction,
+    associateFun
+  })
+}
+
+function createVideoCommentAbuse (options: {
+  baseAbuse: FilteredModelAttributes<AbuseModel>
+  commentInstance: MCommentOwnerVideo
+  transaction: Transaction
+  reporterAccount: MAccountDefault
+}) {
+  const { baseAbuse, commentInstance, transaction, reporterAccount } = options
+
+  const associateFun = async (abuseInstance: MAbuseFull) => {
+    const commentAbuseInstance: MCommentAbuseAccountVideo = await VideoCommentAbuseModel.create({
+      abuseId: abuseInstance.id,
+      videoCommentId: commentInstance.id
+    }, { transaction })
+
+    commentAbuseInstance.VideoComment = commentInstance
+    abuseInstance.VideoCommentAbuse = commentAbuseInstance
+
+    return { isOwned: commentInstance.isOwned() }
+  }
+
+  return createAbuse({
+    base: baseAbuse,
+    reporterAccount,
+    flaggedAccount: commentInstance.Account,
+    transaction,
+    associateFun
+  })
+}
+
+function createAccountAbuse (options: {
+  baseAbuse: FilteredModelAttributes<AbuseModel>
+  accountInstance: MAccountDefault
+  transaction: Transaction
+  reporterAccount: MAccountDefault
+}) {
+  const { baseAbuse, accountInstance, transaction, reporterAccount } = options
+
+  const associateFun = async () => {
+    return { isOwned: accountInstance.isOwned() }
+  }
+
+  return createAbuse({
+    base: baseAbuse,
+    reporterAccount,
+    flaggedAccount: accountInstance,
+    transaction,
+    associateFun
+  })
+}
+
 export {
   isLocalVideoAccepted,
   isLocalVideoThreadAccepted,
@@ -80,5 +181,48 @@ export {
   isRemoteVideoCommentAccepted,
   isLocalVideoCommentReplyAccepted,
   isPreImportVideoAccepted,
-  isPostImportVideoAccepted
+  isPostImportVideoAccepted,
+
+  createAbuse,
+  createVideoAbuse,
+  createVideoCommentAbuse,
+  createAccountAbuse
+}
+
+// ---------------------------------------------------------------------------
+
+async function createAbuse (options: {
+  base: FilteredModelAttributes<AbuseModel>
+  reporterAccount: MAccountDefault
+  flaggedAccount: MAccountLight
+  associateFun: (abuseInstance: MAbuseFull) => Promise<{ isOwned: boolean} >
+  transaction: Transaction
+}) {
+  const { base, reporterAccount, flaggedAccount, associateFun, transaction } = options
+  const auditLogger = auditLoggerFactory('abuse')
+
+  const abuseAttributes = Object.assign({}, base, { flaggedAccountId: flaggedAccount.id })
+  const abuseInstance: MAbuseFull = await AbuseModel.create(abuseAttributes, { transaction })
+
+  abuseInstance.ReporterAccount = reporterAccount
+  abuseInstance.FlaggedAccount = flaggedAccount
+
+  const { isOwned } = await associateFun(abuseInstance)
+
+  if (isOwned === false) {
+    await sendAbuse(reporterAccount.Actor, abuseInstance, abuseInstance.FlaggedAccount, transaction)
+  }
+
+  const abuseJSON = abuseInstance.toFormattedJSON()
+  auditLogger.create(reporterAccount.Actor.getIdentifier(), new AbuseAuditView(abuseJSON))
+
+  Notifier.Instance.notifyOnNewAbuse({
+    abuse: abuseJSON,
+    abuseInstance,
+    reporter: reporterAccount.Actor.getIdentifier()
+  })
+
+  logger.info('Abuse report %d created.', abuseInstance.id)
+
+  return abuseJSON
 }
index 943a087d2e8315bb575b29803aa5b03403c3a813..40cff66d224d4798b0feedf5ce9f6bc86232d434 100644 (file)
@@ -8,23 +8,18 @@ import {
   MUserWithNotificationSetting,
   UserNotificationModelForApi
 } from '@server/types/models/user'
+import { MVideoBlacklistLightVideo, MVideoBlacklistVideo } from '@server/types/models/video/video-blacklist'
 import { MVideoImportVideo } from '@server/types/models/video/video-import'
+import { Abuse } from '@shared/models'
 import { UserNotificationSettingValue, UserNotificationType, UserRight } from '../../shared/models/users'
-import { VideoAbuse, VideoPrivacy, VideoState } from '../../shared/models/videos'
+import { VideoPrivacy, VideoState } from '../../shared/models/videos'
 import { logger } from '../helpers/logger'
 import { CONFIG } from '../initializers/config'
 import { AccountBlocklistModel } from '../models/account/account-blocklist'
 import { UserModel } from '../models/account/user'
 import { UserNotificationModel } from '../models/account/user-notification'
-import { MAccountServer, MActorFollowFull } from '../types/models'
-import {
-  MCommentOwnerVideo,
-  MVideoAbuseVideo,
-  MVideoAccountLight,
-  MVideoBlacklistLightVideo,
-  MVideoBlacklistVideo,
-  MVideoFullLight
-} from '../types/models/video'
+import { MAbuseFull, MAbuseVideo, MAccountServer, MActorFollowFull } from '../types/models'
+import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
 import { isBlockedByServerOrAccount } from './blocklist'
 import { Emailer } from './emailer'
 import { PeerTubeSocket } from './peertube-socket'
@@ -78,9 +73,9 @@ class Notifier {
         .catch(err => logger.error('Cannot notify mentions of comment %s.', comment.url, { err }))
   }
 
-  notifyOnNewVideoAbuse (parameters: { videoAbuse: VideoAbuse, videoAbuseInstance: MVideoAbuseVideo, reporter: string }): void {
-    this.notifyModeratorsOfNewVideoAbuse(parameters)
-        .catch(err => logger.error('Cannot notify of new video abuse of video %s.', parameters.videoAbuseInstance.Video.url, { err }))
+  notifyOnNewAbuse (parameters: { abuse: Abuse, abuseInstance: MAbuseFull, reporter: string }): void {
+    this.notifyModeratorsOfNewAbuse(parameters)
+        .catch(err => logger.error('Cannot notify of new abuse %d.', parameters.abuseInstance.id, { err }))
   }
 
   notifyOnVideoAutoBlacklist (videoBlacklist: MVideoBlacklistLightVideo): void {
@@ -354,33 +349,37 @@ class Notifier {
     return this.notify({ users: admins, settingGetter, notificationCreator, emailSender })
   }
 
-  private async notifyModeratorsOfNewVideoAbuse (parameters: {
-    videoAbuse: VideoAbuse
-    videoAbuseInstance: MVideoAbuseVideo
+  private async notifyModeratorsOfNewAbuse (parameters: {
+    abuse: Abuse
+    abuseInstance: MAbuseFull
     reporter: string
   }) {
-    const moderators = await UserModel.listWithRight(UserRight.MANAGE_VIDEO_ABUSES)
+    const { abuse, abuseInstance } = parameters
+
+    const moderators = await UserModel.listWithRight(UserRight.MANAGE_ABUSES)
     if (moderators.length === 0) return
 
-    logger.info('Notifying %s user/moderators of new video abuse %s.', moderators.length, parameters.videoAbuseInstance.Video.url)
+    const url = abuseInstance.VideoAbuse?.Video?.url || abuseInstance.VideoCommentAbuse?.VideoComment?.url
+
+    logger.info('Notifying %s user/moderators of new abuse %s.', moderators.length, url)
 
     function settingGetter (user: MUserWithNotificationSetting) {
       return user.NotificationSetting.videoAbuseAsModerator
     }
 
     async function notificationCreator (user: MUserWithNotificationSetting) {
-      const notification: UserNotificationModelForApi = await UserNotificationModel.create<UserNotificationModelForApi>({
+      const notification = await UserNotificationModel.create<UserNotificationModelForApi>({
         type: UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS,
         userId: user.id,
-        videoAbuseId: parameters.videoAbuse.id
+        abuseId: abuse.id
       })
-      notification.VideoAbuse = parameters.videoAbuseInstance
+      notification.Abuse = abuseInstance
 
       return notification
     }
 
     function emailSender (emails: string[]) {
-      return Emailer.Instance.addVideoAbuseModeratorsNotification(emails, parameters)
+      return Emailer.Instance.addAbuseModeratorsNotification(emails, parameters)
     }
 
     return this.notify({ users: moderators, settingGetter, notificationCreator, emailSender })
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts
new file mode 100644 (file)
index 0000000..f098e2f
--- /dev/null
@@ -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
+}
index 65dd00335ed7d6762c9ba28131fdd7e790e93526..4086d77aa70a035d73a82e95cff96bdc9fe15553 100644 (file)
@@ -1,3 +1,4 @@
+export * from './abuse'
 export * from './account'
 export * from './blocklist'
 export * from './oembed'
index b76dab722d34d610430efa549f0c6333bb191ed5..29aba04367fe659b2ae87b5ff0baf7eb887a959b 100644 (file)
@@ -5,7 +5,7 @@ import { checkSort, createSortableColumns } from './utils'
 const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS)
 const SORTABLE_ACCOUNTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ACCOUNTS)
 const SORTABLE_JOBS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.JOBS)
-const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES)
+const SORTABLE_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.ABUSES)
 const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
 const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
 const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
@@ -28,7 +28,7 @@ const SORTABLE_VIDEO_REDUNDANCIES_COLUMNS = createSortableColumns(SORTABLE_COLUM
 const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS)
 const accountsSortValidator = checkSort(SORTABLE_ACCOUNTS_COLUMNS)
 const jobsSortValidator = checkSort(SORTABLE_JOBS_COLUMNS)
-const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS)
+const abusesSortValidator = checkSort(SORTABLE_ABUSES_COLUMNS)
 const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
 const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
 const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
@@ -52,7 +52,7 @@ const videoRedundanciesSortValidator = checkSort(SORTABLE_VIDEO_REDUNDANCIES_COL
 
 export {
   usersSortValidator,
-  videoAbusesSortValidator,
+  abusesSortValidator,
   videoChannelsSortValidator,
   videoImportsSortValidator,
   videosSearchSortValidator,
index a0d585b938f12102e76c94202b45559c8cbf14d0..1eabada0a2ce43fe7b0f25291dafbe67b2c7c669 100644 (file)
@@ -1,4 +1,3 @@
-export * from './video-abuses'
 export * from './video-blacklist'
 export * from './video-captions'
 export * from './video-channels'
diff --git a/server/middlewares/validators/videos/video-abuses.ts b/server/middlewares/validators/videos/video-abuses.ts
deleted file mode 100644 (file)
index 5bbd1e3..0000000
+++ /dev/null
@@ -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
-}
similarity index 52%
rename from server/models/video/video-abuse.ts
rename to server/models/abuse/abuse.ts
index 1319332f0738fa80b645d67f42d71c8fd33b20cc..4f99f9c9b836e7a6955fa0f2229995f4fc8096b6 100644 (file)
@@ -1,5 +1,6 @@
 import * as Bluebird from 'bluebird'
-import { literal, Op } from 'sequelize'
+import { invert } from 'lodash'
+import { literal, Op, WhereOptions } from 'sequelize'
 import {
   AllowNull,
   BelongsTo,
@@ -8,36 +9,35 @@ import {
   DataType,
   Default,
   ForeignKey,
+  HasOne,
   Is,
   Model,
   Scopes,
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
-import {
-  VideoAbuseState,
-  VideoDetails,
-  VideoAbusePredefinedReasons,
-  VideoAbusePredefinedReasonsString,
-  videoAbusePredefinedReasonsMap
-} from '../../../shared'
-import { VideoAbuseObject } from '../../../shared/models/activitypub/objects'
-import { VideoAbuse } from '../../../shared/models/videos'
+import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
 import {
-  isVideoAbuseModerationCommentValid,
-  isVideoAbuseReasonValid,
-  isVideoAbuseStateValid
-} from '../../helpers/custom-validators/video-abuses'
-import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
-import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../types/models'
-import { AccountModel } from '../account/account'
+  Abuse,
+  AbuseObject,
+  AbusePredefinedReasons,
+  abusePredefinedReasonsMap,
+  AbusePredefinedReasonsString,
+  AbuseState,
+  AbuseVideoIs,
+  VideoAbuse
+} from '@shared/models'
+import { AbuseFilter } from '@shared/models/moderation/abuse/abuse-filter'
+import { CONSTRAINTS_FIELDS, ABUSE_STATES } from '../../initializers/constants'
+import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models'
+import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
 import { buildBlockedAccountSQL, getSort, searchAttribute, throwIfNotValid } from '../utils'
-import { ThumbnailModel } from './thumbnail'
-import { VideoModel } from './video'
-import { VideoBlacklistModel } from './video-blacklist'
-import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
-import { invert } from 'lodash'
+import { ThumbnailModel } from '../video/thumbnail'
+import { VideoModel } from '../video/video'
+import { VideoBlacklistModel } from '../video/video-blacklist'
+import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
+import { VideoAbuseModel } from './video-abuse'
+import { VideoCommentAbuseModel } from './video-comment-abuse'
 
 export enum ScopeNames {
   FOR_API = 'FOR_API'
@@ -49,20 +49,26 @@ export enum ScopeNames {
     search?: string
     searchReporter?: string
     searchReportee?: string
+
+    // video releated
     searchVideo?: string
     searchVideoChannel?: string
+    videoIs?: AbuseVideoIs
 
     // filters
     id?: number
     predefinedReasonId?: number
+    filter?: AbuseFilter
 
-    state?: VideoAbuseState
-    videoIs?: VideoAbuseVideoIs
+    state?: AbuseState
 
     // accountIds
     serverAccountId: number
     userAccountId: number
   }) => {
+    const onlyBlacklisted = options.videoIs === 'blacklisted'
+    const videoRequired = !!(onlyBlacklisted || options.searchVideo || options.searchVideoChannel)
+
     const where = {
       reporterAccountId: {
         [Op.notIn]: literal('(' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')')
@@ -70,33 +76,36 @@ export enum ScopeNames {
     }
 
     if (options.search) {
+      const escapedSearch = AbuseModel.sequelize.escape('%' + options.search + '%')
+
       Object.assign(where, {
         [Op.or]: [
           {
             [Op.and]: [
-              { videoId: { [Op.not]: null } },
-              searchAttribute(options.search, '$Video.name$')
+              { '$VideoAbuse.videoId$': { [Op.not]: null } },
+              searchAttribute(options.search, '$VideoAbuse.Video.name$')
             ]
           },
           {
             [Op.and]: [
-              { videoId: { [Op.not]: null } },
-              searchAttribute(options.search, '$Video.VideoChannel.name$')
+              { '$VideoAbuse.videoId$': { [Op.not]: null } },
+              searchAttribute(options.search, '$VideoAbuse.Video.VideoChannel.name$')
             ]
           },
           {
             [Op.and]: [
-              { deletedVideo: { [Op.not]: null } },
-              { deletedVideo: searchAttribute(options.search, 'name') }
+              { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
+              literal(`"VideoAbuse"."deletedVideo"->>'name' ILIKE ${escapedSearch}`)
             ]
           },
           {
             [Op.and]: [
-              { deletedVideo: { [Op.not]: null } },
-              { deletedVideo: { channel: searchAttribute(options.search, 'displayName') } }
+              { '$VideoAbuse.deletedVideo$': { [Op.not]: null } },
+              literal(`"VideoAbuse"."deletedVideo"->'channel'->>'displayName' ILIKE ${escapedSearch}`)
             ]
           },
-          searchAttribute(options.search, '$Account.name$')
+          searchAttribute(options.search, '$ReporterAccount.name$'),
+          searchAttribute(options.search, '$FlaggedAccount.name$')
         ]
       })
     }
@@ -106,7 +115,7 @@ export enum ScopeNames {
 
     if (options.videoIs === 'deleted') {
       Object.assign(where, {
-        deletedVideo: {
+        '$VideoAbuse.deletedVideo$': {
           [Op.not]: null
         }
       })
@@ -120,8 +129,6 @@ export enum ScopeNames {
       })
     }
 
-    const onlyBlacklisted = options.videoIs === 'blacklisted'
-
     return {
       attributes: {
         include: [
@@ -131,7 +138,7 @@ export enum ScopeNames {
               '(' +
                 'SELECT count(*) ' +
                 'FROM "videoAbuse" ' +
-                'WHERE "videoId" = "VideoAbuseModel"."videoId" ' +
+                'WHERE "videoId" = "VideoAbuse"."videoId" ' +
               ')'
             ),
             'countReportsForVideo'
@@ -146,7 +153,7 @@ export enum ScopeNames {
                          'row_number() OVER (PARTITION BY "videoId" ORDER BY "createdAt") AS nth ' +
                   'FROM "videoAbuse" ' +
                 ') t ' +
-                'WHERE t.id = "VideoAbuseModel".id ' +
+                'WHERE t.id = "VideoAbuse".id' +
               ')'
             ),
             'nthReportForVideo'
@@ -159,7 +166,7 @@ export enum ScopeNames {
                 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
                 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
                 'INNER JOIN "account" ON "videoChannel"."accountId" = "account"."id" ' +
-                'WHERE "account"."id" = "VideoAbuseModel"."reporterAccountId" ' +
+                'WHERE "account"."id" = "AbuseModel"."reporterAccountId" ' +
               ')'
             ),
             'countReportsForReporter__video'
@@ -169,7 +176,7 @@ export enum ScopeNames {
               '(' +
                 'SELECT count(DISTINCT "videoAbuse"."id") ' +
                 'FROM "videoAbuse" ' +
-                `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuseModel"."reporterAccountId" ` +
+                `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "AbuseModel"."reporterAccountId" ` +
               ')'
             ),
             'countReportsForReporter__deletedVideo'
@@ -182,8 +189,8 @@ export enum ScopeNames {
                 'INNER JOIN "video" ON "video"."id" = "videoAbuse"."videoId" ' +
                 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
                 'INNER JOIN "account" ON ' +
-                      '"videoChannel"."accountId" = "Video->VideoChannel"."accountId" ' +
-                   `OR "videoChannel"."accountId" = CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
+                      '"videoChannel"."accountId" = "VideoAbuse->Video->VideoChannel"."accountId" ' +
+                   `OR "videoChannel"."accountId" = CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
               ')'
             ),
             'countReportsForReportee__video'
@@ -193,9 +200,9 @@ export enum ScopeNames {
               '(' +
                 'SELECT count(DISTINCT "videoAbuse"."id") ' +
                 'FROM "videoAbuse" ' +
-                `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "Video->VideoChannel"."accountId" ` +
+                `WHERE CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = "VideoAbuse->Video->VideoChannel"."accountId" ` +
                    `OR CAST("deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) = ` +
-                      `CAST("VideoAbuseModel"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
+                      `CAST("VideoAbuse"."deletedVideo"->'channel'->'ownerAccount'->>'id' AS INTEGER) ` +
               ')'
             ),
             'countReportsForReportee__deletedVideo'
@@ -204,32 +211,47 @@ export enum ScopeNames {
       },
       include: [
         {
-          model: AccountModel,
+          model: AccountModel.scope(AccountScopeNames.SUMMARY),
+          as: 'ReporterAccount',
           required: true,
           where: searchAttribute(options.searchReporter, 'name')
         },
         {
-          model: VideoModel,
-          required: !!(onlyBlacklisted || options.searchVideo || options.searchReportee || options.searchVideoChannel),
-          where: searchAttribute(options.searchVideo, 'name'),
+          model: AccountModel.scope(AccountScopeNames.SUMMARY),
+          as: 'FlaggedAccount',
+          required: true,
+          where: searchAttribute(options.searchReportee, 'name')
+        },
+        {
+          model: VideoAbuseModel,
+          required: options.filter === 'video' || !!options.videoIs || videoRequired,
           include: [
             {
-              model: ThumbnailModel
-            },
-            {
-              model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
-              where: searchAttribute(options.searchVideoChannel, 'name'),
+              model: VideoModel,
+              required: videoRequired,
+              where: searchAttribute(options.searchVideo, 'name'),
               include: [
                 {
-                  model: AccountModel,
-                  where: searchAttribute(options.searchReportee, 'name')
+                  model: ThumbnailModel
+                },
+                {
+                  model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: false } as SummaryOptions ] }),
+                  where: searchAttribute(options.searchVideoChannel, 'name'),
+                  required: true,
+                  include: [
+                    {
+                      model: AccountModel.scope(AccountScopeNames.SUMMARY),
+                      required: true,
+                      where: searchAttribute(options.searchReportee, 'name')
+                    }
+                  ]
+                },
+                {
+                  attributes: [ 'id', 'reason', 'unfederated' ],
+                  model: VideoBlacklistModel,
+                  required: onlyBlacklisted
                 }
               ]
-            },
-            {
-              attributes: [ 'id', 'reason', 'unfederated' ],
-              model: VideoBlacklistModel,
-              required: onlyBlacklisted
             }
           ]
         }
@@ -239,55 +261,40 @@ export enum ScopeNames {
   }
 }))
 @Table({
-  tableName: 'videoAbuse',
+  tableName: 'abuse',
   indexes: [
     {
-      fields: [ 'videoId' ]
+      fields: [ 'reporterAccountId' ]
     },
     {
-      fields: [ 'reporterAccountId' ]
+      fields: [ 'flaggedAccountId' ]
     }
   ]
 })
-export class VideoAbuseModel extends Model<VideoAbuseModel> {
+export class AbuseModel extends Model<AbuseModel> {
 
   @AllowNull(false)
   @Default(null)
-  @Is('VideoAbuseReason', value => throwIfNotValid(value, isVideoAbuseReasonValid, 'reason'))
-  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.REASON.max))
+  @Is('VideoAbuseReason', value => throwIfNotValid(value, isAbuseReasonValid, 'reason'))
+  @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.REASON.max))
   reason: string
 
   @AllowNull(false)
   @Default(null)
-  @Is('VideoAbuseState', value => throwIfNotValid(value, isVideoAbuseStateValid, 'state'))
+  @Is('VideoAbuseState', value => throwIfNotValid(value, isAbuseStateValid, 'state'))
   @Column
-  state: VideoAbuseState
+  state: AbuseState
 
   @AllowNull(true)
   @Default(null)
-  @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isVideoAbuseModerationCommentValid, 'moderationComment', true))
-  @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
+  @Is('VideoAbuseModerationComment', value => throwIfNotValid(value, isAbuseModerationCommentValid, 'moderationComment', true))
+  @Column(DataType.STRING(CONSTRAINTS_FIELDS.ABUSES.MODERATION_COMMENT.max))
   moderationComment: string
 
-  @AllowNull(true)
-  @Default(null)
-  @Column(DataType.JSONB)
-  deletedVideo: VideoDetails
-
   @AllowNull(true)
   @Default(null)
   @Column(DataType.ARRAY(DataType.INTEGER))
-  predefinedReasons: VideoAbusePredefinedReasons[]
-
-  @AllowNull(true)
-  @Default(null)
-  @Column
-  startAt: number
-
-  @AllowNull(true)
-  @Default(null)
-  @Column
-  endAt: number
+  predefinedReasons: AbusePredefinedReasons[]
 
   @CreatedAt
   createdAt: Date
@@ -301,36 +308,65 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
 
   @BelongsTo(() => AccountModel, {
     foreignKey: {
+      name: 'reporterAccountId',
       allowNull: true
     },
+    as: 'ReporterAccount',
     onDelete: 'set null'
   })
-  Account: AccountModel
+  ReporterAccount: AccountModel
 
-  @ForeignKey(() => VideoModel)
+  @ForeignKey(() => AccountModel)
   @Column
-  videoId: number
+  flaggedAccountId: number
 
-  @BelongsTo(() => VideoModel, {
+  @BelongsTo(() => AccountModel, {
     foreignKey: {
+      name: 'flaggedAccountId',
       allowNull: true
     },
+    as: 'FlaggedAccount',
     onDelete: 'set null'
   })
-  Video: VideoModel
+  FlaggedAccount: AccountModel
+
+  @HasOne(() => VideoCommentAbuseModel, {
+    foreignKey: {
+      name: 'abuseId',
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
+  VideoCommentAbuse: VideoCommentAbuseModel
 
-  static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
-    const videoAttributes = {}
-    if (videoId) videoAttributes['videoId'] = videoId
-    if (uuid) videoAttributes['deletedVideo'] = { uuid }
+  @HasOne(() => VideoAbuseModel, {
+    foreignKey: {
+      name: 'abuseId',
+      allowNull: false
+    },
+    onDelete: 'cascade'
+  })
+  VideoAbuse: VideoAbuseModel
+
+  static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MAbuse> {
+    const videoWhere: WhereOptions = {}
+
+    if (videoId) videoWhere.videoId = videoId
+    if (uuid) videoWhere.deletedVideo = { uuid }
 
     const query = {
+      include: [
+        {
+          model: VideoAbuseModel,
+          required: true,
+          where: videoWhere
+        }
+      ],
       where: {
-        id,
-        ...videoAttributes
+        id
       }
     }
-    return VideoAbuseModel.findOne(query)
+    return AbuseModel.findOne(query)
   }
 
   static listForApi (parameters: {
@@ -338,13 +374,15 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
     count: number
     sort: string
 
+    filter?: AbuseFilter
+
     serverAccountId: number
     user?: MUserAccountId
 
     id?: number
-    predefinedReason?: VideoAbusePredefinedReasonsString
-    state?: VideoAbuseState
-    videoIs?: VideoAbuseVideoIs
+    predefinedReason?: AbusePredefinedReasonsString
+    state?: AbuseState
+    videoIs?: AbuseVideoIs
 
     search?: string
     searchReporter?: string
@@ -364,24 +402,26 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
       predefinedReason,
       searchReportee,
       searchVideo,
+      filter,
       searchVideoChannel,
       searchReporter,
       id
     } = parameters
 
     const userAccountId = user ? user.Account.id : undefined
-    const predefinedReasonId = predefinedReason ? videoAbusePredefinedReasonsMap[predefinedReason] : undefined
+    const predefinedReasonId = predefinedReason ? abusePredefinedReasonsMap[predefinedReason] : undefined
 
     const query = {
       offset: start,
       limit: count,
       order: getSort(sort),
-      col: 'VideoAbuseModel.id',
+      col: 'AbuseModel.id',
       distinct: true
     }
 
     const filters = {
       id,
+      filter,
       predefinedReasonId,
       search,
       state,
@@ -394,7 +434,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
       userAccountId
     }
 
-    return VideoAbuseModel
+    return AbuseModel
       .scope([
         { method: [ ScopeNames.FOR_API, filters ] }
       ])
@@ -404,8 +444,8 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
       })
   }
 
-  toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
-    const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+  toFormattedJSON (this: MAbuseFormattable): Abuse {
+    const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
     const countReportsForVideo = this.get('countReportsForVideo') as number
     const nthReportForVideo = this.get('nthReportForVideo') as number
     const countReportsForReporterVideo = this.get('countReportsForReporter__video') as number
@@ -413,51 +453,70 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
     const countReportsForReporteeVideo = this.get('countReportsForReportee__video') as number
     const countReportsForReporteeDeletedVideo = this.get('countReportsForReportee__deletedVideo') as number
 
-    const video = this.Video
-      ? this.Video
-      : this.deletedVideo
+    let video: VideoAbuse
+
+    if (this.VideoAbuse) {
+      const abuseModel = this.VideoAbuse
+      const entity = abuseModel.Video || abuseModel.deletedVideo
+
+      video = {
+        id: entity.id,
+        uuid: entity.uuid,
+        name: entity.name,
+        nsfw: entity.nsfw,
+
+        startAt: abuseModel.startAt,
+        endAt: abuseModel.endAt,
+
+        deleted: !abuseModel.Video,
+        blacklisted: abuseModel.Video?.isBlacklisted() || false,
+        thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(),
+        channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel
+      }
+    }
 
     return {
       id: this.id,
       reason: this.reason,
       predefinedReasons,
-      reporterAccount: this.Account.toFormattedJSON(),
+
+      reporterAccount: this.ReporterAccount.toFormattedJSON(),
+
       state: {
         id: this.state,
-        label: VideoAbuseModel.getStateLabel(this.state)
+        label: AbuseModel.getStateLabel(this.state)
       },
+
       moderationComment: this.moderationComment,
-      video: {
-        id: video.id,
-        uuid: video.uuid,
-        name: video.name,
-        nsfw: video.nsfw,
-        deleted: !this.Video,
-        blacklisted: this.Video?.isBlacklisted() || false,
-        thumbnailPath: this.Video?.getMiniatureStaticPath(),
-        channel: this.Video?.VideoChannel.toFormattedJSON() || this.deletedVideo?.channel
-      },
+
+      video,
+      comment: null,
+
       createdAt: this.createdAt,
       updatedAt: this.updatedAt,
-      startAt: this.startAt,
-      endAt: this.endAt,
       count: countReportsForVideo || 0,
       nth: nthReportForVideo || 0,
       countReportsForReporter: (countReportsForReporterVideo || 0) + (countReportsForReporterDeletedVideo || 0),
-      countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0)
+      countReportsForReportee: (countReportsForReporteeVideo || 0) + (countReportsForReporteeDeletedVideo || 0),
+
+      // FIXME: deprecated in 2.3, remove this
+      startAt: null,
+      endAt: null
     }
   }
 
-  toActivityPubObject (this: MVideoAbuseVideo): VideoAbuseObject {
-    const predefinedReasons = VideoAbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+  toActivityPubObject (this: MAbuseAP): AbuseObject {
+    const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons)
+
+    const object = this.VideoAbuse?.Video?.url || this.VideoCommentAbuse?.VideoComment?.url || this.FlaggedAccount.Actor.url
 
-    const startAt = this.startAt
-    const endAt = this.endAt
+    const startAt = this.VideoAbuse?.startAt
+    const endAt = this.VideoAbuse?.endAt
 
     return {
       type: 'Flag' as 'Flag',
       content: this.reason,
-      object: this.Video.url,
+      object,
       tag: predefinedReasons.map(r => ({
         type: 'Hashtag' as 'Hashtag',
         name: r
@@ -468,12 +527,12 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
   }
 
   private static getStateLabel (id: number) {
-    return VIDEO_ABUSE_STATES[id] || 'Unknown'
+    return ABUSE_STATES[id] || 'Unknown'
   }
 
-  private static getPredefinedReasonsStrings (predefinedReasons: VideoAbusePredefinedReasons[]): VideoAbusePredefinedReasonsString[] {
+  private static getPredefinedReasonsStrings (predefinedReasons: AbusePredefinedReasons[]): AbusePredefinedReasonsString[] {
     return (predefinedReasons || [])
-      .filter(r => r in VideoAbusePredefinedReasons)
-      .map(r => invert(videoAbusePredefinedReasonsMap)[r] as VideoAbusePredefinedReasonsString)
+      .filter(r => r in AbusePredefinedReasons)
+      .map(r => invert(abusePredefinedReasonsMap)[r] as AbusePredefinedReasonsString)
   }
 }
diff --git a/server/models/abuse/video-abuse.ts b/server/models/abuse/video-abuse.ts
new file mode 100644 (file)
index 0000000..d92bcf1
--- /dev/null
@@ -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
+}
diff --git a/server/models/abuse/video-comment-abuse.ts b/server/models/abuse/video-comment-abuse.ts
new file mode 100644 (file)
index 0000000..b4cc276
--- /dev/null
@@ -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
+}
index cf8872fd5f9153976fb7beca3fd79557d26ce121..577b7dc192fb88175a16c7b1949d17908d84673c 100644 (file)
@@ -1,12 +1,12 @@
-import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
-import { AccountModel } from './account'
-import { getSort, searchAttribute } from '../utils'
-import { AccountBlock } from '../../../shared/models/blocklist'
-import { Op } from 'sequelize'
 import * as Bluebird from 'bluebird'
+import { Op } from 'sequelize'
+import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
 import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
+import { AccountBlock } from '../../../shared/models'
 import { ActorModel } from '../activitypub/actor'
 import { ServerModel } from '../server/server'
+import { getSort, searchAttribute } from '../utils'
+import { AccountModel } from './account'
 
 enum ScopeNames {
   WITH_ACCOUNTS = 'WITH_ACCOUNTS'
index 4395d179ae471845bb08d66be5794e0de50551d9..466d6258e1cdafce825adab256c83c38bb92aa6f 100644 (file)
@@ -388,6 +388,10 @@ export class AccountModel extends Model<AccountModel> {
       .findAll(query)
   }
 
+  getClientUrl () {
+    return WEBSERVER.URL + '/accounts/' + this.Actor.getIdentifier()
+  }
+
   toFormattedJSON (this: MAccountFormattable): Account {
     const actor = this.Actor.toFormattedJSON()
     const account = {
index 30985bb0f19cb318f485578353bc250c8eb15a8b..07db5a2dbea704b12f259160b74f81fc5050b7ff 100644 (file)
@@ -1,22 +1,24 @@
+import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
 import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
 import { UserNotification, UserNotificationType } from '../../../shared'
-import { getSort, throwIfNotValid } from '../utils'
 import { isBooleanValid } from '../../helpers/custom-validators/misc'
 import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
-import { UserModel } from './user'
-import { VideoModel } from '../video/video'
-import { VideoCommentModel } from '../video/video-comment'
-import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
-import { VideoChannelModel } from '../video/video-channel'
-import { AccountModel } from './account'
-import { VideoAbuseModel } from '../video/video-abuse'
-import { VideoBlacklistModel } from '../video/video-blacklist'
-import { VideoImportModel } from '../video/video-import'
+import { AbuseModel } from '../abuse/abuse'
+import { VideoAbuseModel } from '../abuse/video-abuse'
+import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
 import { ActorModel } from '../activitypub/actor'
 import { ActorFollowModel } from '../activitypub/actor-follow'
 import { AvatarModel } from '../avatar/avatar'
 import { ServerModel } from '../server/server'
-import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
+import { getSort, throwIfNotValid } from '../utils'
+import { VideoModel } from '../video/video'
+import { VideoBlacklistModel } from '../video/video-blacklist'
+import { VideoChannelModel } from '../video/video-channel'
+import { VideoCommentModel } from '../video/video-comment'
+import { VideoImportModel } from '../video/video-import'
+import { AccountModel } from './account'
+import { UserModel } from './user'
 
 enum ScopeNames {
   WITH_ALL = 'WITH_ALL'
@@ -87,9 +89,41 @@ function buildAccountInclude (required: boolean, withActor = false) {
 
       {
         attributes: [ 'id' ],
-        model: VideoAbuseModel.unscoped(),
+        model: AbuseModel.unscoped(),
         required: false,
-        include: [ buildVideoInclude(true) ]
+        include: [
+          {
+            attributes: [ 'id' ],
+            model: VideoAbuseModel.unscoped(),
+            required: false,
+            include: [ buildVideoInclude(true) ]
+          },
+          {
+            attributes: [ 'id' ],
+            model: VideoCommentAbuseModel.unscoped(),
+            required: false,
+            include: [
+              {
+                attributes: [ 'id', 'originCommentId' ],
+                model: VideoCommentModel,
+                required: true,
+                include: [
+                  {
+                    attributes: [ 'uuid' ],
+                    model: VideoModel.unscoped(),
+                    required: true
+                  }
+                ]
+              }
+            ]
+          },
+          {
+            model: AccountModel,
+            as: 'FlaggedAccount',
+            required: true,
+            include: [ buildActorWithAvatarInclude() ]
+          }
+        ]
       },
 
       {
@@ -179,9 +213,9 @@ function buildAccountInclude (required: boolean, withActor = false) {
       }
     },
     {
-      fields: [ 'videoAbuseId' ],
+      fields: [ 'abuseId' ],
       where: {
-        videoAbuseId: {
+        abuseId: {
           [Op.ne]: null
         }
       }
@@ -276,17 +310,17 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
   })
   Comment: VideoCommentModel
 
-  @ForeignKey(() => VideoAbuseModel)
+  @ForeignKey(() => AbuseModel)
   @Column
-  videoAbuseId: number
+  abuseId: number
 
-  @BelongsTo(() => VideoAbuseModel, {
+  @BelongsTo(() => AbuseModel, {
     foreignKey: {
       allowNull: true
     },
     onDelete: 'cascade'
   })
-  VideoAbuse: VideoAbuseModel
+  Abuse: AbuseModel
 
   @ForeignKey(() => VideoBlacklistModel)
   @Column
@@ -397,10 +431,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
       video: this.formatVideo(this.Comment.Video)
     } : undefined
 
-    const videoAbuse = this.VideoAbuse ? {
-      id: this.VideoAbuse.id,
-      video: this.formatVideo(this.VideoAbuse.Video)
-    } : undefined
+    const abuse = this.Abuse ? this.formatAbuse(this.Abuse) : undefined
 
     const videoBlacklist = this.VideoBlacklist ? {
       id: this.VideoBlacklist.id,
@@ -439,7 +470,7 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
       video,
       videoImport,
       comment,
-      videoAbuse,
+      abuse,
       videoBlacklist,
       account,
       actorFollow,
@@ -456,6 +487,27 @@ export class UserNotificationModel extends Model<UserNotificationModel> {
     }
   }
 
+  formatAbuse (this: UserNotificationModelForApi, abuse: UserNotificationIncludes.AbuseInclude) {
+    const commentAbuse = abuse.VideoCommentAbuse?.VideoComment ? {
+      threadId: abuse.VideoCommentAbuse.VideoComment.getThreadId(),
+
+      video: {
+        uuid: abuse.VideoCommentAbuse.VideoComment.Video.uuid
+      }
+    } : undefined
+
+    const videoAbuse = abuse.VideoAbuse?.Video ? this.formatVideo(abuse.VideoAbuse.Video) : undefined
+
+    const accountAbuse = (!commentAbuse && !videoAbuse) ? this.formatActor(abuse.FlaggedAccount) : undefined
+
+    return {
+      id: abuse.id,
+      video: videoAbuse,
+      comment: commentAbuse,
+      account: accountAbuse
+    }
+  }
+
   formatActor (
     this: UserNotificationModelForApi,
     accountOrChannel: UserNotificationIncludes.AccountIncludeActor | UserNotificationIncludes.VideoChannelIncludeActor
index de193131a27815d8ee31168528beae93eeaa7575..f21eff04b53912a427cc12a98a9c900768b9b4a2 100644 (file)
@@ -19,7 +19,7 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, VideoAbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
+import { hasUserRight, MyUser, USER_ROLE_LABELS, UserRight, AbuseState, VideoPlaylistType, VideoPrivacy } from '../../../shared'
 import { User, UserRole } from '../../../shared/models/users'
 import {
   isNoInstanceConfigWarningModal,
@@ -169,7 +169,7 @@ enum ScopeNames {
               `SELECT concat_ws(':', "abuses", "acceptedAbuses") ` +
               'FROM (' +
                 'SELECT COUNT("videoAbuse"."id") AS "abuses", ' +
-                       `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${VideoAbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
+                       `COUNT("videoAbuse"."id") FILTER (WHERE "videoAbuse"."state" = ${AbuseState.ACCEPTED}) AS "acceptedAbuses" ` +
                 'FROM "videoAbuse" ' +
                 'INNER JOIN "video" ON "videoAbuse"."videoId" = "video"."id" ' +
                 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
index 30f0525e54a4d941f21b47a29f74187b0767a513..68cd72ee791c1dd3e6b575c3994569779d12589d 100644 (file)
@@ -1,11 +1,11 @@
+import * as Bluebird from 'bluebird'
+import { Op } from 'sequelize'
 import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
+import { ServerBlock } from '@shared/models'
 import { AccountModel } from '../account/account'
-import { ServerModel } from './server'
-import { ServerBlock } from '../../../shared/models/blocklist'
 import { getSort, searchAttribute } from '../utils'
-import * as Bluebird from 'bluebird'
-import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
-import { Op } from 'sequelize'
+import { ServerModel } from './server'
 
 enum ScopeNames {
   WITH_ACCOUNT = 'WITH_ACCOUNT',
index e2718300e4f1d86b1cd95e231ab0954a7aa6b6bf..272bba0e1c6ee08aa187a457765d47521c966cdf 100644 (file)
@@ -1,4 +1,5 @@
 import * as Bluebird from 'bluebird'
+import { remove } from 'fs-extra'
 import { maxBy, minBy, pick } from 'lodash'
 import { join } from 'path'
 import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
@@ -23,10 +24,18 @@ import {
   Table,
   UpdatedAt
 } from 'sequelize-typescript'
-import { UserRight, VideoPrivacy, VideoState, ResultList } from '../../../shared'
+import { buildNSFWFilter } from '@server/helpers/express-utils'
+import { getPrivaciesForFederation, isPrivacyForFederation } from '@server/helpers/video'
+import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { getServerActor } from '@server/models/application/application'
+import { ModelCache } from '@server/models/model-cache'
+import { VideoFile } from '@shared/models/videos/video-file.model'
+import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
 import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
 import { Video, VideoDetails } from '../../../shared/models/videos'
+import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
 import { VideoFilter } from '../../../shared/models/videos/video-query.type'
+import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
 import { peertubeTruncate } from '../../helpers/core-utils'
 import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
 import { isBooleanValid } from '../../helpers/custom-validators/misc'
@@ -43,6 +52,7 @@ import {
 } from '../../helpers/custom-validators/videos'
 import { getVideoFileResolution } from '../../helpers/ffmpeg-utils'
 import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
 import {
   ACTIVITY_PUB,
   API_VERSION,
@@ -59,40 +69,6 @@ import {
   WEBSERVER
 } from '../../initializers/constants'
 import { sendDeleteVideo } from '../../lib/activitypub/send'
-import { AccountModel } from '../account/account'
-import { AccountVideoRateModel } from '../account/account-video-rate'
-import { ActorModel } from '../activitypub/actor'
-import { AvatarModel } from '../avatar/avatar'
-import { ServerModel } from '../server/server'
-import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
-import { TagModel } from './tag'
-import { VideoAbuseModel } from './video-abuse'
-import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
-import { VideoCommentModel } from './video-comment'
-import { VideoFileModel } from './video-file'
-import { VideoShareModel } from './video-share'
-import { VideoTagModel } from './video-tag'
-import { ScheduleVideoUpdateModel } from './schedule-video-update'
-import { VideoCaptionModel } from './video-caption'
-import { VideoBlacklistModel } from './video-blacklist'
-import { remove } from 'fs-extra'
-import { VideoViewModel } from './video-view'
-import { VideoRedundancyModel } from '../redundancy/video-redundancy'
-import {
-  videoFilesModelToFormattedJSON,
-  VideoFormattingJSONOptions,
-  videoModelToActivityPubObject,
-  videoModelToFormattedDetailsJSON,
-  videoModelToFormattedJSON
-} from './video-format-utils'
-import { UserVideoHistoryModel } from '../account/user-video-history'
-import { VideoImportModel } from './video-import'
-import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
-import { VideoPlaylistElementModel } from './video-playlist-element'
-import { CONFIG } from '../../initializers/config'
-import { ThumbnailModel } from './thumbnail'
-import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
-import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
 import {
   MChannel,
   MChannelAccountDefault,
@@ -118,15 +94,39 @@ import {
   MVideoWithFile,
   MVideoWithRights
 } from '../../types/models'
-import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
 import { MThumbnail } from '../../types/models/video/thumbnail'
-import { VideoFile } from '@shared/models/videos/video-file.model'
-import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
-import { ModelCache } from '@server/models/model-cache'
+import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
+import { VideoAbuseModel } from '../abuse/video-abuse'
+import { AccountModel } from '../account/account'
+import { AccountVideoRateModel } from '../account/account-video-rate'
+import { UserVideoHistoryModel } from '../account/user-video-history'
+import { ActorModel } from '../activitypub/actor'
+import { AvatarModel } from '../avatar/avatar'
+import { VideoRedundancyModel } from '../redundancy/video-redundancy'
+import { ServerModel } from '../server/server'
+import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
+import { ScheduleVideoUpdateModel } from './schedule-video-update'
+import { TagModel } from './tag'
+import { ThumbnailModel } from './thumbnail'
+import { VideoBlacklistModel } from './video-blacklist'
+import { VideoCaptionModel } from './video-caption'
+import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
+import { VideoCommentModel } from './video-comment'
+import { VideoFileModel } from './video-file'
+import {
+  videoFilesModelToFormattedJSON,
+  VideoFormattingJSONOptions,
+  videoModelToActivityPubObject,
+  videoModelToFormattedDetailsJSON,
+  videoModelToFormattedJSON
+} from './video-format-utils'
+import { VideoImportModel } from './video-import'
+import { VideoPlaylistElementModel } from './video-playlist-element'
 import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
-import { buildNSFWFilter } from '@server/helpers/express-utils'
-import { getServerActor } from '@server/models/application/application'
-import { getPrivaciesForFederation, isPrivacyForFederation } from "@server/helpers/video"
+import { VideoShareModel } from './video-share'
+import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
+import { VideoTagModel } from './video-tag'
+import { VideoViewModel } from './video-view'
 
 export enum ScopeNames {
   AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
index 557bf20eb41f79e0699d46fb672d79ed9574fb0f..f122baef4417c3d953be37e44ae40287344aef86 100644 (file)
@@ -1,7 +1,7 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
 import 'mocha'
-
+import { AbuseState, VideoAbuseCreate } from '@shared/models'
 import {
   cleanupTests,
   createUser,
@@ -20,7 +20,8 @@ import {
   checkBadSortPagination,
   checkBadStartPagination
 } from '../../../../shared/extra-utils/requests/check-api-params'
-import { VideoAbuseState, VideoAbuseCreate } from '../../../../shared/models/videos'
+
+// FIXME: deprecated in 2.3. Remove this controller
 
 describe('Test video abuses API validators', function () {
   let server: ServerInfo
@@ -136,7 +137,7 @@ describe('Test video abuses API validators', function () {
       const fields = { reason: 'my super reason' }
 
       const res = await makePostBodyRequest({ url: server.url, path, token: server.accessToken, fields, statusCodeExpected: 200 })
-      videoAbuseId = res.body.videoAbuse.id
+      videoAbuseId = res.body.abuse.id
     })
 
     it('Should fail with a wrong predefined reason', async function () {
@@ -190,7 +191,7 @@ describe('Test video abuses API validators', function () {
     })
 
     it('Should succeed with the correct params', async function () {
-      const body = { state: VideoAbuseState.ACCEPTED }
+      const body = { state: AbuseState.ACCEPTED }
       await updateVideoAbuse(server.url, server.accessToken, server.video.uuid, videoAbuseId, body)
     })
   })
index 0a66bd1ce5f89f20c497bac4be0e4e52bf7bc998..88b68d977399c591b883a3b1a9b1ca6f9a6d249d 100644 (file)
@@ -2,7 +2,7 @@
 
 import * as chai from 'chai'
 import 'mocha'
-import { MyUser, User, UserRole, Video, VideoAbuseState, VideoAbuseUpdate, VideoPlaylistType } from '../../../../shared/index'
+import { MyUser, User, UserRole, Video, AbuseState, AbuseUpdate, VideoPlaylistType } from '@shared/models'
 import {
   addVideoCommentThread,
   blockUser,
@@ -937,7 +937,7 @@ describe('Test users', function () {
       expect(user2.videoAbusesCount).to.equal(1) // number of incriminations
       expect(user2.videoAbusesCreatedCount).to.equal(1) // number of reports created
 
-      const body: VideoAbuseUpdate = { state: VideoAbuseState.ACCEPTED }
+      const body: AbuseUpdate = { state: AbuseState.ACCEPTED }
       await updateVideoAbuse(server.url, server.accessToken, videoId, abuseId, body)
 
       const res3 = await getUserInformation(server.url, server.accessToken, user17Id, true)
index 7383bd991c51207a4c1e3e46b8dfc1c76e033f3c..20975aa4ac4f8acb31b9e5775dae86c90beadfc3 100644 (file)
@@ -1,21 +1,21 @@
 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
 
-import * as chai from 'chai'
 import 'mocha'
-import { VideoAbuse, VideoAbuseState, VideoAbusePredefinedReasonsString } from '../../../../shared/models/videos'
+import * as chai from 'chai'
+import { Abuse, AbusePredefinedReasonsString, AbuseState } from '@shared/models'
 import {
   cleanupTests,
+  createUser,
   deleteVideoAbuse,
   flushAndRunMultipleServers,
   getVideoAbusesList,
   getVideosList,
+  removeVideo,
   reportVideoAbuse,
   ServerInfo,
   setAccessTokensToServers,
   updateVideoAbuse,
   uploadVideo,
-  removeVideo,
-  createUser,
   userLogin
 } from '../../../../shared/extra-utils/index'
 import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
@@ -29,9 +29,11 @@ import {
 
 const expect = chai.expect
 
+// FIXME: deprecated in 2.3. Remove this controller
+
 describe('Test video abuses', function () {
   let servers: ServerInfo[] = []
-  let abuseServer2: VideoAbuse
+  let abuseServer2: Abuse
 
   before(async function () {
     this.timeout(50000)
@@ -95,7 +97,7 @@ describe('Test video abuses', function () {
     expect(res1.body.data).to.be.an('array')
     expect(res1.body.data.length).to.equal(1)
 
-    const abuse: VideoAbuse = res1.body.data[0]
+    const abuse: Abuse = res1.body.data[0]
     expect(abuse.reason).to.equal('my super bad reason')
     expect(abuse.reporterAccount.name).to.equal('root')
     expect(abuse.reporterAccount.host).to.equal('localhost:' + servers[0].port)
@@ -128,23 +130,23 @@ describe('Test video abuses', function () {
     expect(res1.body.data).to.be.an('array')
     expect(res1.body.data.length).to.equal(2)
 
-    const abuse1: VideoAbuse = res1.body.data[0]
+    const abuse1: Abuse = res1.body.data[0]
     expect(abuse1.reason).to.equal('my super bad reason')
     expect(abuse1.reporterAccount.name).to.equal('root')
     expect(abuse1.reporterAccount.host).to.equal('localhost:' + servers[0].port)
     expect(abuse1.video.id).to.equal(servers[0].video.id)
-    expect(abuse1.state.id).to.equal(VideoAbuseState.PENDING)
+    expect(abuse1.state.id).to.equal(AbuseState.PENDING)
     expect(abuse1.state.label).to.equal('Pending')
     expect(abuse1.moderationComment).to.be.null
     expect(abuse1.count).to.equal(1)
     expect(abuse1.nth).to.equal(1)
 
-    const abuse2: VideoAbuse = res1.body.data[1]
+    const abuse2: Abuse = res1.body.data[1]
     expect(abuse2.reason).to.equal('my super bad reason 2')
     expect(abuse2.reporterAccount.name).to.equal('root')
     expect(abuse2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
     expect(abuse2.video.id).to.equal(servers[1].video.id)
-    expect(abuse2.state.id).to.equal(VideoAbuseState.PENDING)
+    expect(abuse2.state.id).to.equal(AbuseState.PENDING)
     expect(abuse2.state.label).to.equal('Pending')
     expect(abuse2.moderationComment).to.be.null
 
@@ -157,25 +159,25 @@ describe('Test video abuses', function () {
     expect(abuseServer2.reason).to.equal('my super bad reason 2')
     expect(abuseServer2.reporterAccount.name).to.equal('root')
     expect(abuseServer2.reporterAccount.host).to.equal('localhost:' + servers[0].port)
-    expect(abuseServer2.state.id).to.equal(VideoAbuseState.PENDING)
+    expect(abuseServer2.state.id).to.equal(AbuseState.PENDING)
     expect(abuseServer2.state.label).to.equal('Pending')
     expect(abuseServer2.moderationComment).to.be.null
   })
 
   it('Should update the state of a video abuse', async function () {
-    const body = { state: VideoAbuseState.REJECTED }
+    const body = { state: AbuseState.REJECTED }
     await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
 
     const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
-    expect(res.body.data[0].state.id).to.equal(VideoAbuseState.REJECTED)
+    expect(res.body.data[0].state.id).to.equal(AbuseState.REJECTED)
   })
 
   it('Should add a moderation comment', async function () {
-    const body = { state: VideoAbuseState.ACCEPTED, moderationComment: 'It is valid' }
+    const body = { state: AbuseState.ACCEPTED, moderationComment: 'It is valid' }
     await updateVideoAbuse(servers[1].url, servers[1].accessToken, abuseServer2.video.uuid, abuseServer2.id, body)
 
     const res = await getVideoAbusesList({ url: servers[1].url, token: servers[1].accessToken })
-    expect(res.body.data[0].state.id).to.equal(VideoAbuseState.ACCEPTED)
+    expect(res.body.data[0].state.id).to.equal(AbuseState.ACCEPTED)
     expect(res.body.data[0].moderationComment).to.equal('It is valid')
   })
 
@@ -243,7 +245,7 @@ describe('Test video abuses', function () {
     expect(res.body.data.length).to.equal(2, "wrong number of videos returned")
     expect(res.body.data[0].id).to.equal(abuseServer2.id, "wrong origin server id for first video")
 
-    const abuse: VideoAbuse = res.body.data[0]
+    const abuse: Abuse = res.body.data[0]
     expect(abuse.video.id).to.equal(abuseServer2.video.id, "wrong video id")
     expect(abuse.video.channel).to.exist
     expect(abuse.video.deleted).to.be.true
@@ -277,7 +279,7 @@ describe('Test video abuses', function () {
     const res2 = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
 
     {
-      for (const abuse of res2.body.data as VideoAbuse[]) {
+      for (const abuse of res2.body.data as Abuse[]) {
         if (abuse.video.id === video3.id) {
           expect(abuse.count).to.equal(1, "wrong reports count for video 3")
           expect(abuse.nth).to.equal(1, "wrong report position in report list for video 3")
@@ -295,7 +297,7 @@ describe('Test video abuses', function () {
     this.timeout(10000)
 
     const reason5 = 'my super bad reason 5'
-    const predefinedReasons5: VideoAbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
+    const predefinedReasons5: AbusePredefinedReasonsString[] = [ 'violentOrRepulsive', 'captions' ]
     const createdAbuse = (await reportVideoAbuse(
       servers[0].url,
       servers[0].accessToken,
@@ -304,16 +306,16 @@ describe('Test video abuses', function () {
       predefinedReasons5,
       1,
       5
-    )).body.videoAbuse as VideoAbuse
+    )).body.abuse
 
     const res = await getVideoAbusesList({ url: servers[0].url, token: servers[0].accessToken })
 
     {
-      const abuse = (res.body.data as VideoAbuse[]).find(a => a.id === createdAbuse.id)
+      const abuse = (res.body.data as Abuse[]).find(a => a.id === createdAbuse.id)
       expect(abuse.reason).to.equals(reason5)
       expect(abuse.predefinedReasons).to.deep.equals(predefinedReasons5, "predefined reasons do not match the one reported")
-      expect(abuse.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
-      expect(abuse.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
+      expect(abuse.video.startAt).to.equal(1, "starting timestamp doesn't match the one reported")
+      expect(abuse.video.endAt).to.equal(5, "ending timestamp doesn't match the one reported")
     }
   })
 
@@ -348,7 +350,7 @@ describe('Test video abuses', function () {
 
       const res = await getVideoAbusesList(options)
 
-      return res.body.data as VideoAbuse[]
+      return res.body.data as Abuse[]
     }
 
     expect(await list({ id: 56 })).to.have.lengthOf(0)
@@ -365,14 +367,14 @@ describe('Test video abuses', function () {
     expect(await list({ searchReporter: 'user2' })).to.have.lengthOf(1)
     expect(await list({ searchReporter: 'root' })).to.have.lengthOf(5)
 
-    expect(await list({ searchReportee: 'root' })).to.have.lengthOf(4)
+    expect(await list({ searchReportee: 'root' })).to.have.lengthOf(5)
     expect(await list({ searchReportee: 'aaaa' })).to.have.lengthOf(0)
 
     expect(await list({ videoIs: 'deleted' })).to.have.lengthOf(1)
     expect(await list({ videoIs: 'blacklisted' })).to.have.lengthOf(0)
 
-    expect(await list({ state: VideoAbuseState.ACCEPTED })).to.have.lengthOf(0)
-    expect(await list({ state: VideoAbuseState.PENDING })).to.have.lengthOf(6)
+    expect(await list({ state: AbuseState.ACCEPTED })).to.have.lengthOf(0)
+    expect(await list({ state: AbuseState.PENDING })).to.have.lengthOf(6)
 
     expect(await list({ predefinedReason: 'violentOrRepulsive' })).to.have.lengthOf(1)
     expect(await list({ predefinedReason: 'serverRules' })).to.have.lengthOf(0)
index 78b4948ce85f263f84f1fed28f4002fcfd5f7c88..affa17425d0afe4d182003b18fb8db3fd90da0da 100644 (file)
@@ -1,4 +1,5 @@
 export * from './account'
+export * from './moderation'
 export * from './oauth'
 export * from './server'
 export * from './user'
diff --git a/server/types/models/moderation/abuse.ts b/server/types/models/moderation/abuse.ts
new file mode 100644 (file)
index 0000000..abbc93d
--- /dev/null
@@ -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>
diff --git a/server/types/models/moderation/index.ts b/server/types/models/moderation/index.ts
new file mode 100644 (file)
index 0000000..8bea170
--- /dev/null
@@ -0,0 +1 @@
+export * from './abuse'
index dd3de423b978d5d89084a1581552bae241a99a6d..92ea16768c581b3b6294e66912576cde8f48fe75 100644 (file)
@@ -1,16 +1,18 @@
-import { UserNotificationModel } from '../../../models/account/user-notification'
+import { VideoAbuseModel } from '@server/models/abuse/video-abuse'
+import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
 import { PickWith, PickWithOpt } from '@shared/core-utils'
-import { VideoModel } from '../../../models/video/video'
+import { AbuseModel } from '../../../models/abuse/abuse'
+import { AccountModel } from '../../../models/account/account'
+import { UserNotificationModel } from '../../../models/account/user-notification'
 import { ActorModel } from '../../../models/activitypub/actor'
-import { ServerModel } from '../../../models/server/server'
+import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 import { AvatarModel } from '../../../models/avatar/avatar'
+import { ServerModel } from '../../../models/server/server'
+import { VideoModel } from '../../../models/video/video'
+import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
 import { VideoChannelModel } from '../../../models/video/video-channel'
-import { AccountModel } from '../../../models/account/account'
 import { VideoCommentModel } from '../../../models/video/video-comment'
-import { VideoAbuseModel } from '../../../models/video/video-abuse'
-import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
 import { VideoImportModel } from '../../../models/video/video-import'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
 
 type Use<K extends keyof UserNotificationModel, M> = PickWith<UserNotificationModel, K, M>
 
@@ -47,6 +49,18 @@ export module UserNotificationIncludes {
     Pick<VideoAbuseModel, 'id'> &
     PickWith<VideoAbuseModel, 'Video', VideoInclude>
 
+  export type VideoCommentAbuseInclude =
+    Pick<VideoCommentAbuseModel, 'id'> &
+    PickWith<VideoCommentAbuseModel, 'VideoComment',
+    Pick<VideoCommentModel, 'id' | 'originCommentId' | 'getThreadId'> &
+    PickWith<VideoCommentModel, 'Video', Pick<VideoModel, 'uuid'>>>
+
+  export type AbuseInclude =
+    Pick<AbuseModel, 'id'> &
+    PickWith<AbuseModel, 'VideoAbuse', VideoAbuseInclude> &
+    PickWith<AbuseModel, 'VideoCommentAbuse', VideoCommentAbuseInclude> &
+    PickWith<AbuseModel, 'FlaggedAccount', AccountIncludeActor>
+
   export type VideoBlacklistInclude =
     Pick<VideoBlacklistModel, 'id'> &
     PickWith<VideoAbuseModel, 'Video', VideoInclude>
@@ -76,7 +90,7 @@ export module UserNotificationIncludes {
 // ############################################################################
 
 export type MUserNotification =
-  Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'VideoAbuse' | 'VideoBlacklist' |
+  Omit<UserNotificationModel, 'User' | 'Video' | 'Comment' | 'Abuse' | 'VideoBlacklist' |
   'VideoImport' | 'Account' | 'ActorFollow'>
 
 // ############################################################################
@@ -85,7 +99,7 @@ export type UserNotificationModelForApi =
   MUserNotification &
   Use<'Video', UserNotificationIncludes.VideoIncludeChannel> &
   Use<'Comment', UserNotificationIncludes.VideoCommentInclude> &
-  Use<'VideoAbuse', UserNotificationIncludes.VideoAbuseInclude> &
+  Use<'Abuse', UserNotificationIncludes.AbuseInclude> &
   Use<'VideoBlacklist', UserNotificationIncludes.VideoBlacklistInclude> &
   Use<'VideoImport', UserNotificationIncludes.VideoImportInclude> &
   Use<'ActorFollow', UserNotificationIncludes.ActorFollowInclude> &
index bd69c8a4b273e73c52a1910bd4cf0ebfc3587d37..25db23898425675c8215270f2acfb4289be1f566 100644 (file)
@@ -2,7 +2,6 @@ export * from './schedule-video-update'
 export * from './tag'
 export * from './thumbnail'
 export * from './video'
-export * from './video-abuse'
 export * from './video-blacklist'
 export * from './video-caption'
 export * from './video-change-ownership'
diff --git a/server/types/models/video/video-abuse.ts b/server/types/models/video/video-abuse.ts
deleted file mode 100644 (file)
index 279a87c..0000000
+++ /dev/null
@@ -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'>>
index cac801e55229c8a39f104f574116185fe5073bbc..7595e6d86c22ceaafd3bef731f3fd74e1dcc7788 100644 (file)
@@ -1,5 +1,6 @@
 import { RegisterServerAuthExternalOptions } from '@server/types'
 import {
+  MAbuse,
   MAccountBlocklist,
   MActorUrl,
   MStreamingPlaylist,
@@ -26,7 +27,6 @@ import {
   MComment,
   MCommentOwnerVideoReply,
   MUserDefault,
-  MVideoAbuse,
   MVideoBlacklist,
   MVideoCaptionVideo,
   MVideoFullLight,
@@ -77,7 +77,7 @@ declare module 'express' {
 
       videoCaption?: MVideoCaptionVideo
 
-      videoAbuse?: MVideoAbuse
+      abuse?: MAbuse
 
       videoStreamingPlaylist?: MStreamingPlaylist
 
index 2ac0c6338500e04014311fd3e524621f65a45840..af4d23856607fb9f611b60549c33d02a5d12f63c 100644 (file)
@@ -17,6 +17,7 @@ export * from './videos/services'
 export * from './videos/video-playlists'
 export * from './users/users'
 export * from './users/accounts'
+export * from './moderation/abuses'
 export * from './videos/video-abuses'
 export * from './videos/video-blacklist'
 export * from './videos/video-captions'
diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts
new file mode 100644 (file)
index 0000000..48a51e2
--- /dev/null
@@ -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
+}
index a17a39de9080bbf8dd1bb9b0fbdfa5d45faa8f01..62f3418c5db3ba2a01a5d90437e7dbdd8534b488 100644 (file)
@@ -443,11 +443,11 @@ async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUU
       expect(notification).to.not.be.undefined
       expect(notification.type).to.equal(notificationType)
 
-      expect(notification.videoAbuse.id).to.be.a('number')
-      checkVideo(notification.videoAbuse.video, videoName, videoUUID)
+      expect(notification.abuse.id).to.be.a('number')
+      checkVideo(notification.abuse.video, videoName, videoUUID)
     } else {
       expect(notification).to.satisfy((n: UserNotification) => {
-        return n === undefined || n.videoAbuse === undefined || n.videoAbuse.video.uuid !== videoUUID
+        return n === undefined || n.abuse === undefined || n.abuse.video.uuid !== videoUUID
       })
     }
   }
index ff006672ad99a42a5e44867d09a71771d81a42b3..8827b8196ccad80f15b5df1000330ec8a59d3dc6 100644 (file)
@@ -1,15 +1,15 @@
 import * as request from 'supertest'
-import { VideoAbuseUpdate } from '../../models/videos/abuse/video-abuse-update.model'
-import { makeDeleteRequest, makePutBodyRequest, makeGetRequest } from '../requests/requests'
-import { VideoAbuseState, VideoAbusePredefinedReasonsString } from '@shared/models'
-import { VideoAbuseVideoIs } from '@shared/models/videos/abuse/video-abuse-video-is.type'
+import { AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
+import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
+
+// FIXME: deprecated in 2.3. Remove this file
 
 function reportVideoAbuse (
   url: string,
   token: string,
   videoId: number | string,
   reason: string,
-  predefinedReasons?: VideoAbusePredefinedReasonsString[],
+  predefinedReasons?: AbusePredefinedReasonsString[],
   startAt?: number,
   endAt?: number,
   specialStatus = 200
@@ -28,10 +28,10 @@ function getVideoAbusesList (options: {
   url: string
   token: string
   id?: number
-  predefinedReason?: VideoAbusePredefinedReasonsString
+  predefinedReason?: AbusePredefinedReasonsString
   search?: string
-  state?: VideoAbuseState
-  videoIs?: VideoAbuseVideoIs
+  state?: AbuseState
+  videoIs?: AbuseVideoIs
   searchReporter?: string
   searchReportee?: string
   searchVideo?: string
@@ -79,7 +79,7 @@ function updateVideoAbuse (
   token: string,
   videoId: string | number,
   videoAbuseId: number,
-  body: VideoAbuseUpdate,
+  body: AbuseUpdate,
   statusCodeExpected = 204
 ) {
   const path = '/api/v1/videos/' + videoId + '/abuse/' + videoAbuseId
index 31b9e46739718ad82479637ec9ee2a5398e9a52a..5b4ce214a22b98ee5198f9aeb7d683f1a3ff3750 100644 (file)
@@ -1,12 +1,12 @@
 import { ActivityPubActor } from './activitypub-actor'
 import { ActivityPubSignature } from './activitypub-signature'
-import { CacheFileObject, VideoTorrentObject, ActivityFlagReasonObject } from './objects'
+import { ActivityFlagReasonObject, CacheFileObject, VideoTorrentObject } from './objects'
+import { AbuseObject } from './objects/abuse-object'
 import { DislikeObject } from './objects/dislike-object'
-import { VideoAbuseObject } from './objects/video-abuse-object'
-import { VideoCommentObject } from './objects/video-comment-object'
-import { ViewObject } from './objects/view-object'
 import { APObject } from './objects/object.model'
 import { PlaylistObject } from './objects/playlist-object'
+import { VideoCommentObject } from './objects/video-comment-object'
+import { ViewObject } from './objects/view-object'
 
 export type Activity =
   ActivityCreate |
@@ -53,7 +53,7 @@ export interface BaseActivity {
 
 export interface ActivityCreate extends BaseActivity {
   type: 'Create'
-  object: VideoTorrentObject | VideoAbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject
+  object: VideoTorrentObject | AbuseObject | ViewObject | DislikeObject | VideoCommentObject | CacheFileObject | PlaylistObject
 }
 
 export interface ActivityUpdate extends BaseActivity {
similarity index 84%
rename from shared/models/activitypub/objects/video-abuse-object.ts
rename to shared/models/activitypub/objects/abuse-object.ts
index 73add8ef4479aa99922b2a87d8ec3bce6bb7e005..ad45cc064e7a04255278e640b21d8a89499cbfb2 100644 (file)
@@ -1,10 +1,12 @@
 import { ActivityFlagReasonObject } from './common-objects'
 
-export interface VideoAbuseObject {
+export interface AbuseObject {
   type: 'Flag'
   content: string
   object: string | string[]
+
   tag?: ActivityFlagReasonObject[]
+
   startAt?: number
   endAt?: number
 }
index 096d422eab117a4aaa3f12c30ff75bf64246b2a7..711ce45f45b925a361198df304ae68350eae6533 100644 (file)
@@ -1,4 +1,4 @@
-import { VideoAbusePredefinedReasonsString } from '@shared/models/videos'
+import { AbusePredefinedReasonsString } from '@shared/models'
 
 export interface ActivityIdentifierObject {
   identifier: string
@@ -85,7 +85,7 @@ export interface ActivityMentionObject {
 
 export interface ActivityFlagReasonObject {
   type: 'Hashtag'
-  name: VideoAbusePredefinedReasonsString
+  name: AbusePredefinedReasonsString
 }
 
 export type ActivityTagObject =
index fba61e12fcfb103ba247eff3a2d89da6f175de5e..a6a20e87a01a8572fb64a507707d4d3c46ea6d6e 100644 (file)
@@ -1,6 +1,6 @@
+export * from './abuse-object'
 export * from './cache-file-object'
 export * from './common-objects'
-export * from './video-abuse-object'
+export * from './dislike-object'
 export * from './video-torrent-object'
 export * from './view-object'
-export * from './dislike-object'
index 3d4bdedde40ab4780f8d3212e74a11599f9b6b40..a68f57148d3274b08d4e16303e0a1693cf60f1fb 100644 (file)
@@ -1,7 +1,7 @@
 export * from './activitypub'
 export * from './actors'
 export * from './avatars'
-export * from './blocklist'
+export * from './moderation'
 export * from './bulk'
 export * from './redundancy'
 export * from './users'
@@ -14,4 +14,3 @@ export * from './search'
 export * from './server'
 export * from './oauth-client-local.model'
 export * from './result-list.model'
-export * from './server/server-config.model'
diff --git a/shared/models/moderation/abuse/abuse-create.model.ts b/shared/models/moderation/abuse/abuse-create.model.ts
new file mode 100644 (file)
index 0000000..c0d04e4
--- /dev/null
@@ -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
+}
diff --git a/shared/models/moderation/abuse/abuse-filter.ts b/shared/models/moderation/abuse/abuse-filter.ts
new file mode 100644 (file)
index 0000000..03303bb
--- /dev/null
@@ -0,0 +1 @@
+export type AbuseFilter = 'video' | 'comment'
diff --git a/shared/models/moderation/abuse/abuse-reason.model.ts b/shared/models/moderation/abuse/abuse-reason.model.ts
new file mode 100644 (file)
index 0000000..3687596
--- /dev/null
@@ -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
+}
similarity index 61%
rename from shared/models/videos/abuse/video-abuse-state.model.ts
rename to shared/models/moderation/abuse/abuse-state.model.ts
index 529f034bdd048e62c445d82ad5e397bf9d2bdcc8..b00cccad85e777fbeadf5c6f2665277e3a77b604 100644 (file)
@@ -1,4 +1,4 @@
-export enum VideoAbuseState {
+export enum AbuseState {
   PENDING = 1,
   REJECTED = 2,
   ACCEPTED = 3
diff --git a/shared/models/moderation/abuse/abuse-update.model.ts b/shared/models/moderation/abuse/abuse-update.model.ts
new file mode 100644 (file)
index 0000000..4360fe7
--- /dev/null
@@ -0,0 +1,7 @@
+import { AbuseState } from './abuse-state.model'
+
+export interface AbuseUpdate {
+  moderationComment?: string
+
+  state?: AbuseState
+}
diff --git a/shared/models/moderation/abuse/abuse-video-is.type.ts b/shared/models/moderation/abuse/abuse-video-is.type.ts
new file mode 100644 (file)
index 0000000..74937f3
--- /dev/null
@@ -0,0 +1 @@
+export type AbuseVideoIs = 'deleted' | 'blacklisted'
diff --git a/shared/models/moderation/abuse/abuse.model.ts b/shared/models/moderation/abuse/abuse.model.ts
new file mode 100644 (file)
index 0000000..9ff150c
--- /dev/null
@@ -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
+}
diff --git a/shared/models/moderation/abuse/index.ts b/shared/models/moderation/abuse/index.ts
new file mode 100644 (file)
index 0000000..32a6b4e
--- /dev/null
@@ -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'
similarity index 75%
rename from shared/models/blocklist/index.ts
rename to shared/models/moderation/index.ts
index fc78732706b0e2d4cf1ac73e2234d0a57692b2f3..8b6042e9790b55fb986e42fcac258de1b8cb63d6 100644 (file)
@@ -1,2 +1,3 @@
+export * from './abuse'
 export * from './account-block.model'
 export * from './server-block.model'
index e9be1ca7fd76c6a7f844f6304dcb6f45c63ad7f8..39090f5a104f59af63673dccb24dffca77e4a5b3 100644 (file)
@@ -64,9 +64,20 @@ export interface UserNotification {
     video: VideoInfo
   }
 
-  videoAbuse?: {
+  abuse?: {
     id: number
-    video: VideoInfo
+
+    video?: VideoInfo
+
+    comment?: {
+      threadId: number
+
+      video: {
+        uuid: string
+      }
+    }
+
+    account?: ActorInfo
   }
 
   videoBlacklist?: {
index 2f88a65ded827a7c24be60119586bca7f2c9903b..4a7ae43738f54898cbe57410f7f4c27852afc7de 100644 (file)
@@ -11,7 +11,7 @@ export enum UserRight {
 
   MANAGE_SERVER_REDUNDANCY,
 
-  MANAGE_VIDEO_ABUSES,
+  MANAGE_ABUSES,
 
   MANAGE_JOBS,
 
index 2b08b585029901d17c0e417cba6d381f38c1c845..772988c0c5a2ce0471fad1415deb583fe566658b 100644 (file)
@@ -20,7 +20,7 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
 
   [UserRole.MODERATOR]: [
     UserRight.MANAGE_VIDEO_BLACKLIST,
-    UserRight.MANAGE_VIDEO_ABUSES,
+    UserRight.MANAGE_ABUSES,
     UserRight.REMOVE_ANY_VIDEO,
     UserRight.REMOVE_ANY_VIDEO_CHANNEL,
     UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
diff --git a/shared/models/videos/abuse/index.ts b/shared/models/videos/abuse/index.ts
deleted file mode 100644 (file)
index f70bc73..0000000
+++ /dev/null
@@ -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'
diff --git a/shared/models/videos/abuse/video-abuse-create.model.ts b/shared/models/videos/abuse/video-abuse-create.model.ts
deleted file mode 100644 (file)
index c93cb8b..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
-
-export interface VideoAbuseCreate {
-  reason: string
-  predefinedReasons?: VideoAbusePredefinedReasonsString[]
-  startAt?: number
-  endAt?: number
-}
diff --git a/shared/models/videos/abuse/video-abuse-reason.model.ts b/shared/models/videos/abuse/video-abuse-reason.model.ts
deleted file mode 100644 (file)
index 9064f0c..0000000
+++ /dev/null
@@ -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
-}
diff --git a/shared/models/videos/abuse/video-abuse-update.model.ts b/shared/models/videos/abuse/video-abuse-update.model.ts
deleted file mode 100644 (file)
index 9b32aae..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-import { VideoAbuseState } from './video-abuse-state.model'
-
-export interface VideoAbuseUpdate {
-  moderationComment?: string
-  state?: VideoAbuseState
-}
diff --git a/shared/models/videos/abuse/video-abuse-video-is.type.ts b/shared/models/videos/abuse/video-abuse-video-is.type.ts
deleted file mode 100644 (file)
index e860189..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export type VideoAbuseVideoIs = 'deleted' | 'blacklisted'
diff --git a/shared/models/videos/abuse/video-abuse.model.ts b/shared/models/videos/abuse/video-abuse.model.ts
deleted file mode 100644 (file)
index 38605dc..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Account } from '../../actors/index'
-import { VideoConstant } from '../video-constant.model'
-import { VideoAbuseState } from './video-abuse-state.model'
-import { VideoChannel } from '../channel/video-channel.model'
-import { VideoAbusePredefinedReasonsString } from './video-abuse-reason.model'
-
-export interface VideoAbuse {
-  id: number
-  reason: string
-  predefinedReasons?: VideoAbusePredefinedReasonsString[]
-  reporterAccount: Account
-
-  state: VideoConstant<VideoAbuseState>
-  moderationComment?: string
-
-  video: {
-    id: number
-    name: string
-    uuid: string
-    nsfw: boolean
-    deleted: boolean
-    blacklisted: boolean
-    thumbnailPath?: string
-    channel?: VideoChannel
-  }
-
-  createdAt: Date
-  updatedAt: Date
-
-  startAt: number
-  endAt: number
-
-  count?: number
-  nth?: number
-
-  countReportsForReporter?: number
-  countReportsForReportee?: number
-}
index e1d96b40ad22699db24a25045005b29e24ad6515..20b9638abfcee542a07093bc9691d24282751c40 100644 (file)
@@ -1,4 +1,3 @@
-export * from './abuse'
 export * from './blacklist'
 export * from './caption'
 export * from './channel'