mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-11-28 03:23:57 -06:00
Implement video comment list in admin
This commit is contained in:
parent
dc13623baa
commit
0f8d00e314
@ -62,6 +62,13 @@ export class AdminComponent implements OnInit {
|
||||
iconName: 'cross'
|
||||
})
|
||||
}
|
||||
if (this.hasVideoCommentsRight()) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Video comments`,
|
||||
routerLink: '/admin/moderation/video-comments/list',
|
||||
iconName: 'message-circle'
|
||||
})
|
||||
}
|
||||
if (this.hasAccountsBlocklistRight()) {
|
||||
moderationItems.children.push({
|
||||
label: $localize`Muted accounts`,
|
||||
@ -140,4 +147,8 @@ export class AdminComponent implements OnInit {
|
||||
hasDebugRight () {
|
||||
return this.auth.getUser().hasRight(UserRight.MANAGE_DEBUG)
|
||||
}
|
||||
|
||||
hasVideoCommentsRight () {
|
||||
return this.auth.getUser().hasRight(UserRight.SEE_ALL_COMMENTS)
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import { SharedFormModule } from '@app/shared/shared-forms'
|
||||
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
|
||||
import { SharedMainModule } from '@app/shared/shared-main'
|
||||
import { SharedModerationModule } from '@app/shared/shared-moderation'
|
||||
import { SharedVideoCommentModule } from '@app/shared/shared-video-comment'
|
||||
import { AdminRoutingModule } from './admin-routing.module'
|
||||
import { AdminComponent } from './admin.component'
|
||||
import { ConfigComponent, EditCustomConfigComponent } from './config'
|
||||
@ -18,6 +19,7 @@ import { VideoRedundancyInformationComponent } from './follows/video-redundancie
|
||||
import { AbuseListComponent, VideoBlockListComponent } from './moderation'
|
||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist'
|
||||
import { ModerationComponent } from './moderation/moderation.component'
|
||||
import { VideoCommentListComponent } from './moderation/video-comment-list'
|
||||
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'
|
||||
@ -37,6 +39,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
||||
SharedModerationModule,
|
||||
SharedGlobalIconModule,
|
||||
SharedAbuseListModule,
|
||||
SharedVideoCommentModule,
|
||||
|
||||
TableModule,
|
||||
SelectButtonModule,
|
||||
@ -62,6 +65,7 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom
|
||||
ModerationComponent,
|
||||
VideoBlockListComponent,
|
||||
AbuseListComponent,
|
||||
VideoCommentListComponent,
|
||||
|
||||
InstanceServerBlocklistComponent,
|
||||
InstanceAccountBlocklistComponent,
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Routes } from '@angular/router'
|
||||
import { AbuseListComponent } from '@app/+admin/moderation/abuse-list'
|
||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
|
||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
||||
import { AbuseListComponent } from '@app/+admin/moderation/abuse-list'
|
||||
import { VideoBlockListComponent } from '@app/+admin/moderation/video-block-list'
|
||||
import { VideoCommentListComponent } from './video-comment-list'
|
||||
import { UserRightGuard } from '@app/core'
|
||||
import { UserRight } from '@shared/models'
|
||||
|
||||
@ -37,6 +38,7 @@ export const ModerationRoutes: Routes = [
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-blacklist',
|
||||
redirectTo: 'video-blocks/list',
|
||||
@ -64,10 +66,28 @@ export const ModerationRoutes: Routes = [
|
||||
data: {
|
||||
userRight: UserRight.MANAGE_VIDEO_BLACKLIST,
|
||||
meta: {
|
||||
title: $localize`Videos blocked`
|
||||
title: $localize`Blocked videos`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'video-comments',
|
||||
redirectTo: 'video-comments/list',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'video-comments/list',
|
||||
component: VideoCommentListComponent,
|
||||
canActivate: [ UserRightGuard ],
|
||||
data: {
|
||||
userRight: UserRight.SEE_ALL_COMMENTS,
|
||||
meta: {
|
||||
title: $localize`Video comments`
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'blocklist/accounts',
|
||||
component: InstanceAccountBlocklistComponent,
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './video-comment-list.component'
|
@ -0,0 +1,102 @@
|
||||
<h1>
|
||||
<my-global-icon iconName="cross" aria-hidden="true"></my-global-icon>
|
||||
<ng-container i18n>Video comments</ng-container>
|
||||
</h1>
|
||||
|
||||
this view does show comments from muted accounts so you can delete them
|
||||
|
||||
<p-table
|
||||
[value]="comments" [lazy]="true" [paginator]="totalRecords > 0" [totalRecords]="totalRecords" [rows]="rowsPerPage" [rowsPerPageOptions]="rowsPerPageOptions"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
|
||||
[showCurrentPageReport]="true" i18n-currentPageReportTemplate
|
||||
currentPageReportTemplate="Showing {{'{first}'}} to {{'{last}'}} of {{'{totalRecords}'}} comments"
|
||||
(onPage)="onPage($event)" [expandedRowKeys]="expandedRows"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div class="ml-auto">
|
||||
<div class="input-group has-feedback has-clear">
|
||||
<div class="input-group-prepend c-hand" ngbDropdown placement="bottom-left auto" container="body">
|
||||
<div class="input-group-text" ngbDropdownToggle>
|
||||
<span class="caret" aria-haspopup="menu" role="button"></span>
|
||||
</div>
|
||||
|
||||
<div role="menu" ngbDropdownMenu>
|
||||
<h6 class="dropdown-header" i18n>Advanced comments filters</h6>
|
||||
<a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:true' }" class="dropdown-item" i18n>Local comments</a>
|
||||
<a [routerLink]="[ '/admin/moderation/video-comments/list' ]" [queryParams]="{ 'search': 'local:false' }" class="dropdown-item" i18n>Remote comments</a>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||
(keyup)="onSearch($event)"
|
||||
>
|
||||
<a class="glyphicon glyphicon-remove-sign form-control-feedback form-control-clear" (click)="resetTableFilter()"></a>
|
||||
<span class="sr-only" i18n>Clear filters</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 40px"></th>
|
||||
<th style="width: 100px;" i18n>Account</th>
|
||||
<th style="width: 100px;" i18n>Video</th>
|
||||
<th style="width: 100px;" i18n>Comment</th>
|
||||
<th style="width: 150px;" i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th style="width: 150px;"></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-videoComment let-expanded="expanded">
|
||||
<tr>
|
||||
<td class="expand-cell c-hand" [pRowToggler]="videoComment" 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>
|
||||
{{ videoComment.by }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ videoComment.video.name }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div [innerHTML]="videoComment.textHtml"></div>
|
||||
</td>
|
||||
|
||||
<td>{{ videoComment.createdAt | date: 'short' }}</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown
|
||||
[ngClass]="{ 'show': expanded }" placement="bottom-right" container="body"
|
||||
i18n-label label="Actions" [actions]="videoCommentActions" [entry]="videoComment"
|
||||
></my-action-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="rowexpansion" let-videoComment>
|
||||
<tr>
|
||||
<td class="expand-cell" colspan="5">
|
||||
<div [innerHTML]="videoComment.textHtml"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="emptymessage">
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="no-results">
|
||||
<ng-container *ngIf="search" i18n>No comments found matching current filters.</ng-container>
|
||||
<ng-container *ngIf="!search" i18n>No comments found.</ng-container>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
@ -0,0 +1,27 @@
|
||||
@import 'mixins';
|
||||
|
||||
my-global-icon {
|
||||
@include apply-svg-color(#7d7d7d);
|
||||
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
@include peertube-input-group(300px);
|
||||
|
||||
.dropdown-toggle::after {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.caption {
|
||||
justify-content: flex-end;
|
||||
|
||||
input {
|
||||
@include peertube-input-text(250px);
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
import { SortMeta } from 'primeng/api'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { AfterViewInit, Component, OnInit } from '@angular/core'
|
||||
import { DomSanitizer } from '@angular/platform-browser'
|
||||
import { ActivatedRoute, Params, Router } from '@angular/router'
|
||||
import { ConfirmService, MarkdownService, Notifier, RestPagination, RestTable, ServerService } from '@app/core'
|
||||
import { DropdownAction, VideoService } from '@app/shared/shared-main'
|
||||
import { VideoCommentAdmin, VideoCommentService } from '@app/shared/shared-video-comment'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-comment-list',
|
||||
templateUrl: './video-comment-list.component.html',
|
||||
styleUrls: [ './video-comment-list.component.scss' ]
|
||||
})
|
||||
export class VideoCommentListComponent extends RestTable implements OnInit, AfterViewInit {
|
||||
comments: VideoCommentAdmin[]
|
||||
totalRecords = 0
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
videoCommentActions: DropdownAction<VideoCommentAdmin>[][] = []
|
||||
|
||||
constructor (
|
||||
private notifier: Notifier,
|
||||
private serverService: ServerService,
|
||||
private confirmService: ConfirmService,
|
||||
private videoCommentService: VideoCommentService,
|
||||
private markdownRenderer: MarkdownService,
|
||||
private sanitizer: DomSanitizer,
|
||||
private videoService: VideoService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {
|
||||
super()
|
||||
|
||||
this.videoCommentActions = [
|
||||
[
|
||||
|
||||
// remove this comment,
|
||||
|
||||
// remove all comments of this account
|
||||
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.initialize()
|
||||
|
||||
this.route.queryParams
|
||||
.pipe(filter(params => params.search !== undefined && params.search !== null))
|
||||
.subscribe(params => {
|
||||
this.search = params.search
|
||||
this.setTableFilter(params.search)
|
||||
this.loadData()
|
||||
})
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
if (this.search) this.setTableFilter(this.search)
|
||||
}
|
||||
|
||||
onSearch (event: Event) {
|
||||
this.onSearch(event)
|
||||
this.setQueryParams((event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
setQueryParams (search: string) {
|
||||
const queryParams: Params = {}
|
||||
|
||||
if (search) Object.assign(queryParams, { search })
|
||||
this.router.navigate([ '/admin/moderation/video-comments/list' ], { queryParams })
|
||||
}
|
||||
|
||||
resetTableFilter () {
|
||||
this.setTableFilter('')
|
||||
this.setQueryParams('')
|
||||
this.resetSearch()
|
||||
}
|
||||
/* END Table filter functions */
|
||||
|
||||
getIdentifier () {
|
||||
return 'VideoCommentListComponent'
|
||||
}
|
||||
|
||||
toHtml (text: string) {
|
||||
return this.markdownRenderer.textMarkdownToHTML(text)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
this.videoCommentService.getAdminVideoComments({
|
||||
pagination: this.pagination,
|
||||
sort: this.sort,
|
||||
search: this.search
|
||||
}).subscribe(
|
||||
async resultList => {
|
||||
this.totalRecords = resultList.total
|
||||
|
||||
this.comments = []
|
||||
|
||||
for (const c of resultList.data) {
|
||||
this.comments.push(
|
||||
new VideoCommentAdmin(c, await this.toHtml(c.text))
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { getAbsoluteAPIUrl } from '@app/helpers'
|
||||
import { Actor } from '@app/shared/shared-main'
|
||||
import { Account as AccountInterface, VideoComment as VideoCommentServerModel } from '@shared/models'
|
||||
import { Account as AccountInterface, VideoComment as VideoCommentServerModel, VideoCommentAdmin as VideoCommentAdminServerModel } from '@shared/models'
|
||||
|
||||
export class VideoComment implements VideoCommentServerModel {
|
||||
id: number
|
||||
@ -46,3 +46,53 @@ export class VideoComment implements VideoCommentServerModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class VideoCommentAdmin implements VideoCommentAdminServerModel {
|
||||
id: number
|
||||
url: string
|
||||
text: string
|
||||
textHtml: string
|
||||
|
||||
threadId: number
|
||||
inReplyToCommentId: number
|
||||
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
|
||||
account: AccountInterface
|
||||
|
||||
video: {
|
||||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
}
|
||||
|
||||
by: string
|
||||
accountAvatarUrl: string
|
||||
|
||||
constructor (hash: VideoCommentAdminServerModel, textHtml: string) {
|
||||
this.id = hash.id
|
||||
this.url = hash.url
|
||||
this.text = hash.text
|
||||
this.textHtml = textHtml
|
||||
|
||||
this.threadId = hash.threadId
|
||||
this.inReplyToCommentId = hash.inReplyToCommentId
|
||||
|
||||
this.createdAt = new Date(hash.createdAt.toString())
|
||||
this.updatedAt = new Date(hash.updatedAt.toString())
|
||||
|
||||
this.video = {
|
||||
id: hash.video.id,
|
||||
uuid: hash.video.uuid,
|
||||
name: hash.video.name
|
||||
}
|
||||
|
||||
this.account = hash.account
|
||||
|
||||
if (this.account) {
|
||||
this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
|
||||
this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,18 +2,20 @@ import { Observable } from 'rxjs'
|
||||
import { catchError, map } from 'rxjs/operators'
|
||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ComponentPaginationLight, RestExtractor, RestService } from '@app/core'
|
||||
import { ComponentPaginationLight, RestExtractor, RestPagination, RestService } from '@app/core'
|
||||
import { objectLineFeedToHtml } from '@app/helpers'
|
||||
import {
|
||||
FeedFormat,
|
||||
ResultList,
|
||||
VideoComment as VideoCommentServerModel,
|
||||
VideoCommentAdmin,
|
||||
VideoCommentCreate,
|
||||
VideoCommentThreadTree as VideoCommentThreadTreeServerModel
|
||||
} from '@shared/models'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { VideoCommentThreadTree } from './video-comment-thread-tree.model'
|
||||
import { VideoComment } from './video-comment.model'
|
||||
import { SortMeta } from 'primeng/api'
|
||||
|
||||
@Injectable()
|
||||
export class VideoCommentService {
|
||||
@ -48,6 +50,27 @@ export class VideoCommentService {
|
||||
)
|
||||
}
|
||||
|
||||
getAdminVideoComments (options: {
|
||||
pagination: RestPagination,
|
||||
sort: SortMeta,
|
||||
search?: string
|
||||
}): Observable<ResultList<VideoCommentAdmin>> {
|
||||
const { pagination, sort, search } = options
|
||||
const url = VideoCommentService.BASE_VIDEO_URL + '/comments'
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
if (search) {
|
||||
params = this.buildParamsFromSearch(search, params)
|
||||
}
|
||||
|
||||
return this.authHttp.get<ResultList<VideoCommentAdmin>>(url, { params })
|
||||
.pipe(
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
}
|
||||
|
||||
getVideoCommentThreads (parameters: {
|
||||
videoId: number | string,
|
||||
componentPagination: ComponentPaginationLight,
|
||||
@ -146,4 +169,24 @@ export class VideoCommentService {
|
||||
|
||||
return tree as VideoCommentThreadTree
|
||||
}
|
||||
|
||||
private buildParamsFromSearch (search: string, params: HttpParams) {
|
||||
const filters = this.restService.parseQueryStringFilter(search, {
|
||||
state: {
|
||||
prefix: 'local:',
|
||||
isBoolean: true,
|
||||
handler: v => {
|
||||
if (v === 'true') return v
|
||||
if (v === 'false') return v
|
||||
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
|
||||
searchAccount: { prefix: 'account:' },
|
||||
searchVideo: { prefix: 'video:' }
|
||||
})
|
||||
|
||||
return this.restService.addObjectParams(params, filters)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import * as express from 'express'
|
||||
import { ResultList } from '../../../../shared/models'
|
||||
import { ResultList, UserRight } from '../../../../shared/models'
|
||||
import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
|
||||
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
|
||||
import { getFormattedObjects } from '../../../helpers/utils'
|
||||
@ -11,6 +11,7 @@ import {
|
||||
asyncMiddleware,
|
||||
asyncRetryTransactionMiddleware,
|
||||
authenticate,
|
||||
ensureUserHasRight,
|
||||
optionalAuthenticate,
|
||||
paginationValidator,
|
||||
setDefaultPagination,
|
||||
@ -19,9 +20,11 @@ import {
|
||||
import {
|
||||
addVideoCommentReplyValidator,
|
||||
addVideoCommentThreadValidator,
|
||||
listVideoCommentsValidator,
|
||||
listVideoCommentThreadsValidator,
|
||||
listVideoThreadCommentsValidator,
|
||||
removeVideoCommentValidator,
|
||||
videoCommentsValidator,
|
||||
videoCommentThreadsSortValidator
|
||||
} from '../../../middlewares/validators'
|
||||
import { AccountModel } from '../../../models/account/account'
|
||||
@ -61,6 +64,17 @@ videoCommentRouter.delete('/:videoId/comments/:commentId',
|
||||
asyncRetryTransactionMiddleware(removeVideoComment)
|
||||
)
|
||||
|
||||
videoCommentRouter.get('/comments',
|
||||
authenticate,
|
||||
ensureUserHasRight(UserRight.SEE_ALL_COMMENTS),
|
||||
paginationValidator,
|
||||
videoCommentsValidator,
|
||||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
listVideoCommentsValidator,
|
||||
asyncMiddleware(listComments)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
@ -69,6 +83,26 @@ export {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function listComments (req: express.Request, res: express.Response) {
|
||||
const options = {
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort,
|
||||
|
||||
isLocal: req.query.isLocal,
|
||||
search: req.query.search,
|
||||
searchAccount: req.query.searchAccount,
|
||||
searchVideo: req.query.searchVideo
|
||||
}
|
||||
|
||||
const resultList = await VideoCommentModel.listCommentsForApi(options)
|
||||
|
||||
return res.json({
|
||||
total: resultList.total,
|
||||
data: resultList.data.map(c => c.toFormattedAdminJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async function listVideoThreads (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||
|
@ -63,7 +63,10 @@ const SORTABLE_COLUMNS = {
|
||||
JOBS: [ 'createdAt' ],
|
||||
VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ],
|
||||
VIDEO_IMPORTS: [ 'createdAt' ],
|
||||
|
||||
VIDEO_COMMENT_THREADS: [ 'createdAt', 'totalReplies' ],
|
||||
VIDEO_COMMENTS: [ 'createdAt' ],
|
||||
|
||||
VIDEO_RATES: [ 'createdAt' ],
|
||||
BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ],
|
||||
FOLLOWERS: [ 'createdAt', 'state', 'score' ],
|
||||
|
@ -10,6 +10,7 @@ const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS)
|
||||
const SORTABLE_VIDEOS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS_SEARCH)
|
||||
const SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
|
||||
const SORTABLE_VIDEO_IMPORTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_IMPORTS)
|
||||
const SORTABLE_VIDEO_COMMENTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
|
||||
const SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
|
||||
const SORTABLE_VIDEO_RATES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_RATES)
|
||||
const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS)
|
||||
@ -33,6 +34,7 @@ const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS)
|
||||
const videoImportsSortValidator = checkSort(SORTABLE_VIDEO_IMPORTS_COLUMNS)
|
||||
const videosSearchSortValidator = checkSort(SORTABLE_VIDEOS_SEARCH_COLUMNS)
|
||||
const videoChannelsSearchSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_SEARCH_COLUMNS)
|
||||
const videoCommentsValidator = checkSort(SORTABLE_VIDEO_COMMENTS_COLUMNS)
|
||||
const videoCommentThreadsSortValidator = checkSort(SORTABLE_VIDEO_COMMENT_THREADS_COLUMNS)
|
||||
const videoRatesSortValidator = checkSort(SORTABLE_VIDEO_RATES_COLUMNS)
|
||||
const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS)
|
||||
@ -55,6 +57,7 @@ export {
|
||||
abusesSortValidator,
|
||||
videoChannelsSortValidator,
|
||||
videoImportsSortValidator,
|
||||
videoCommentsValidator,
|
||||
videosSearchSortValidator,
|
||||
videosSortValidator,
|
||||
blacklistSortValidator,
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { MUserAccountUrl } from '@server/types/models'
|
||||
import { UserRight } from '../../../../shared'
|
||||
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
|
||||
import { exists, isBooleanValid, isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
|
||||
import {
|
||||
doesVideoCommentExist,
|
||||
doesVideoCommentThreadExist,
|
||||
@ -15,6 +15,33 @@ import { Hooks } from '../../../lib/plugins/hooks'
|
||||
import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video'
|
||||
import { areValidationErrors } from '../utils'
|
||||
|
||||
const listVideoCommentsValidator = [
|
||||
query('isLocal')
|
||||
.optional()
|
||||
.custom(isBooleanValid)
|
||||
.withMessage('Should have a valid is local boolean'),
|
||||
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists).withMessage('Should have a valid search'),
|
||||
|
||||
query('searchAccount')
|
||||
.optional()
|
||||
.custom(exists).withMessage('Should have a valid account search'),
|
||||
|
||||
query('searchVideo')
|
||||
.optional()
|
||||
.custom(exists).withMessage('Should have a valid video search'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking listVideoCommentsValidator parameters.', { parameters: req.query })
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const listVideoCommentThreadsValidator = [
|
||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
||||
|
||||
@ -116,6 +143,7 @@ export {
|
||||
listVideoCommentThreadsValidator,
|
||||
listVideoThreadCommentsValidator,
|
||||
addVideoCommentThreadValidator,
|
||||
listVideoCommentsValidator,
|
||||
addVideoCommentReplyValidator,
|
||||
videoCommentGetValidator,
|
||||
removeVideoCommentValidator
|
||||
|
@ -1,6 +1,6 @@
|
||||
import * as Bluebird from 'bluebird'
|
||||
import { uniq } from 'lodash'
|
||||
import { FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
|
||||
import { FindAndCountOptions, FindOptions, Op, Order, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
|
||||
import {
|
||||
AllowNull,
|
||||
BelongsTo,
|
||||
@ -20,13 +20,14 @@ import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
|
||||
import { VideoPrivacy } from '@shared/models'
|
||||
import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
|
||||
import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
|
||||
import { VideoComment } from '../../../shared/models/videos/video-comment.model'
|
||||
import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/video-comment.model'
|
||||
import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { regexpCapture } from '../../helpers/regexp'
|
||||
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
||||
import {
|
||||
MComment,
|
||||
MCommentAdminFormattable,
|
||||
MCommentAP,
|
||||
MCommentFormattable,
|
||||
MCommentId,
|
||||
@ -40,7 +41,14 @@ import {
|
||||
import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
|
||||
import { AccountModel } from '../account/account'
|
||||
import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
|
||||
import { buildBlockedAccountSQL, buildBlockedAccountSQLOptimized, buildLocalAccountIdsIn, getCommentSort, throwIfNotValid } from '../utils'
|
||||
import {
|
||||
buildBlockedAccountSQL,
|
||||
buildBlockedAccountSQLOptimized,
|
||||
buildLocalAccountIdsIn,
|
||||
getCommentSort,
|
||||
searchAttribute,
|
||||
throwIfNotValid
|
||||
} from '../utils'
|
||||
import { VideoModel } from './video'
|
||||
import { VideoChannelModel } from './video-channel'
|
||||
|
||||
@ -303,6 +311,90 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||
return VideoCommentModel.scope([ ScopeNames.WITH_IN_REPLY_TO, ScopeNames.WITH_ACCOUNT ]).findOne(query)
|
||||
}
|
||||
|
||||
static listCommentsForApi (parameters: {
|
||||
start: number
|
||||
count: number
|
||||
sort: string
|
||||
|
||||
isLocal?: boolean
|
||||
search?: string
|
||||
searchAccount?: string
|
||||
searchVideo?: string
|
||||
}) {
|
||||
const { start, count, sort, isLocal, search, searchAccount, searchVideo } = parameters
|
||||
|
||||
const query: FindAndCountOptions = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: getCommentSort(sort)
|
||||
}
|
||||
|
||||
const where: WhereOptions = {
|
||||
isDeleted: false
|
||||
}
|
||||
|
||||
const whereAccount: WhereOptions = {}
|
||||
const whereActor: WhereOptions = {}
|
||||
const whereVideo: WhereOptions = {}
|
||||
|
||||
if (isLocal === true) {
|
||||
Object.assign(where, {
|
||||
serverId: null
|
||||
})
|
||||
} else if (isLocal === false) {
|
||||
Object.assign(where, {
|
||||
serverId: {
|
||||
[Op.ne]: null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (search) {
|
||||
Object.assign(where, searchAttribute(search, 'text'))
|
||||
Object.assign(whereActor, searchAttribute(search, 'preferredUsername'))
|
||||
Object.assign(whereAccount, searchAttribute(search, 'name'))
|
||||
Object.assign(whereVideo, searchAttribute(search, 'name'))
|
||||
}
|
||||
|
||||
if (searchAccount) {
|
||||
Object.assign(whereActor, searchAttribute(search, 'preferredUsername'))
|
||||
Object.assign(whereAccount, searchAttribute(search, 'name'))
|
||||
}
|
||||
|
||||
if (searchVideo) {
|
||||
Object.assign(whereVideo, searchAttribute(search, 'name'))
|
||||
}
|
||||
|
||||
query.include = [
|
||||
{
|
||||
model: AccountModel.unscoped(),
|
||||
required: !!searchAccount,
|
||||
where: whereAccount,
|
||||
include: [
|
||||
{
|
||||
attributes: {
|
||||
exclude: unusedActorAttributesForAPI
|
||||
},
|
||||
model: ActorModel, // Default scope includes avatar and server
|
||||
required: true,
|
||||
where: whereActor
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
model: VideoModel.unscoped(),
|
||||
required: true,
|
||||
where: whereVideo
|
||||
}
|
||||
]
|
||||
|
||||
return VideoCommentModel
|
||||
.findAndCountAll(query)
|
||||
.then(({ rows, count }) => {
|
||||
return { total: count, data: rows }
|
||||
})
|
||||
}
|
||||
|
||||
static async listThreadsForApi (parameters: {
|
||||
videoId: number
|
||||
isVideoOwned: boolean
|
||||
@ -656,19 +748,51 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||
id: this.id,
|
||||
url: this.url,
|
||||
text: this.text,
|
||||
|
||||
threadId: this.getThreadId(),
|
||||
inReplyToCommentId: this.inReplyToCommentId || null,
|
||||
videoId: this.videoId,
|
||||
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
deletedAt: this.deletedAt,
|
||||
|
||||
isDeleted: this.isDeleted(),
|
||||
|
||||
totalRepliesFromVideoAuthor: this.get('totalRepliesFromVideoAuthor') || 0,
|
||||
totalReplies: this.get('totalReplies') || 0,
|
||||
account: this.Account ? this.Account.toFormattedJSON() : null
|
||||
|
||||
account: this.Account
|
||||
? this.Account.toFormattedJSON()
|
||||
: null
|
||||
} as VideoComment
|
||||
}
|
||||
|
||||
toFormattedAdminJSON (this: MCommentAdminFormattable) {
|
||||
return {
|
||||
id: this.id,
|
||||
url: this.url,
|
||||
text: this.text,
|
||||
|
||||
threadId: this.getThreadId(),
|
||||
inReplyToCommentId: this.inReplyToCommentId || null,
|
||||
videoId: this.videoId,
|
||||
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
|
||||
video: {
|
||||
id: this.Video.id,
|
||||
uuid: this.Video.uuid,
|
||||
name: this.Video.name
|
||||
},
|
||||
|
||||
account: this.Account
|
||||
? this.Account.toFormattedJSON()
|
||||
: null
|
||||
} as VideoCommentAdmin
|
||||
}
|
||||
|
||||
toActivityPubObject (this: MCommentAP, threadParentComments: MCommentOwner[]): VideoCommentObject | ActivityTombstoneObject {
|
||||
let inReplyTo: string
|
||||
// New thread, so in AS we reply to the video
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||
import { PickWith, PickWithOpt } from '@shared/core-utils'
|
||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||
import { MAccountDefault, MAccountFormattable, MAccountUrl } from '../account'
|
||||
import { MVideoAccountLight, MVideoFeed, MVideoIdUrl, MVideoUrl } from './video'
|
||||
import { MVideo, MVideoAccountLight, MVideoFeed, MVideoIdUrl, MVideoUrl } from './video'
|
||||
|
||||
type Use<K extends keyof VideoCommentModel, M> = PickWith<VideoCommentModel, K, M>
|
||||
|
||||
@ -59,6 +59,11 @@ export type MCommentFormattable =
|
||||
MCommentTotalReplies &
|
||||
Use<'Account', MAccountFormattable>
|
||||
|
||||
export type MCommentAdminFormattable =
|
||||
MComment &
|
||||
Use<'Account', MAccountFormattable> &
|
||||
Use<'Video', MVideo>
|
||||
|
||||
export type MCommentAP =
|
||||
MComment &
|
||||
Use<'Account', MAccountUrl> &
|
||||
|
@ -22,7 +22,8 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
|
||||
UserRight.SEE_ALL_VIDEOS,
|
||||
UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
|
||||
UserRight.MANAGE_SERVERS_BLOCKLIST,
|
||||
UserRight.MANAGE_USERS
|
||||
UserRight.MANAGE_USERS,
|
||||
UserRight.SEE_ALL_COMMENTS
|
||||
],
|
||||
|
||||
[UserRole.USER]: []
|
||||
|
@ -32,6 +32,7 @@ export const enum UserRight {
|
||||
|
||||
GET_ANY_LIVE,
|
||||
SEE_ALL_VIDEOS,
|
||||
SEE_ALL_COMMENTS,
|
||||
CHANGE_VIDEO_OWNERSHIP,
|
||||
|
||||
MANAGE_PLUGINS,
|
||||
|
@ -16,6 +16,26 @@ export interface VideoComment {
|
||||
account: Account
|
||||
}
|
||||
|
||||
export interface VideoCommentAdmin {
|
||||
id: number
|
||||
url: string
|
||||
text: string
|
||||
|
||||
threadId: number
|
||||
inReplyToCommentId: number
|
||||
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
|
||||
account: Account
|
||||
|
||||
video: {
|
||||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface VideoCommentThreadTree {
|
||||
comment: VideoComment
|
||||
children: VideoCommentThreadTree[]
|
||||
|
Loading…
Reference in New Issue
Block a user