Give moderators access to edit channels (#4608)

* give admins access to edit all channels

closes #4598

* test(channels): +admin update another users channel

* Fix tests

* fix(server): delete another users channel

Since the channel owner isn't necessary the auth user we need to check
the right account whether it's the last video or not.

* REMOVE_ANY_VIDEO_CHANNEL > MANAGE_ANY_VIDEO_CHANNEL

Merge REMOVE_ANY_VIDEO_CHANNEL and MANY_VIDEO_CHANNELS to
MANAGE_ANY_VIDEO_CHANNEL.

* user-right: moderator can't manage admins channel

* client: MyVideoChannelCreateComponent > VideoChannelCreateComponent

* client: MyVideoChannelEdit > VideoChannelEdit

* Revert "user-right: moderator can't manage admins channel"

This reverts commit 2c627c154e.

* server: clean dupl validator functionality

* fix ensureUserCanManageChannel usage

It's not async anymore.

* server: merge channel validator middleares

ensureAuthUserOwnsChannelValidator & ensureUserCanManageChannel gets
merged into one middleware.

* client(VideoChannelEdit): redirect to prev route

* fix(VideoChannels): handle anon users

* client: new routes for create/update channel

* Refactor channel validators

Co-authored-by: Chocobozzz <me@florianbigard.com>
pull/4636/head
kontrollanten 2021-12-13 15:29:13 +01:00 committed by GitHub
parent 11e520b50d
commit a37e9e74ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 316 additions and 246 deletions

View File

@ -0,0 +1,31 @@
import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router'
import { VideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
const manageRoutes: Routes = [
{
path: 'create',
component: VideoChannelCreateComponent,
data: {
meta: {
title: $localize`Create a new video channel`
}
}
},
{
path: 'update/:videoChannelName',
component: VideoChannelUpdateComponent,
data: {
meta: {
title: $localize`Update video channel`
}
}
}
]
@NgModule({
imports: [ RouterModule.forChild(manageRoutes) ],
exports: [ RouterModule ]
})
export class ManageRoutingModule {}

View File

@ -0,0 +1,31 @@
import { NgModule } from '@angular/core'
import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main'
import { SharedActorImageModule } from '../shared/shared-actor-image/shared-actor-image.module'
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
import { VideoChannelCreateComponent } from './video-channel-edit/video-channel-create.component'
import { VideoChannelUpdateComponent } from './video-channel-edit/video-channel-update.component'
import { ManageRoutingModule } from './manage-routing.module'
@NgModule({
imports: [
ManageRoutingModule,
SharedMainModule,
SharedFormModule,
SharedGlobalIconModule,
SharedActorImageModule,
SharedActorImageEditModule
],
declarations: [
VideoChannelCreateComponent,
VideoChannelUpdateComponent
],
exports: [
],
providers: []
})
export class ManageModule { }

View File

@ -12,13 +12,13 @@ import {
import { FormValidatorService } from '@app/shared/shared-forms' import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { HttpStatusCode, VideoChannelCreate } from '@shared/models' import { HttpStatusCode, VideoChannelCreate } from '@shared/models'
import { MyVideoChannelEdit } from './my-video-channel-edit' import { VideoChannelEdit } from './video-channel-edit'
@Component({ @Component({
templateUrl: './my-video-channel-edit.component.html', templateUrl: './video-channel-edit.component.html',
styleUrls: [ './my-video-channel-edit.component.scss' ] styleUrls: [ './video-channel-edit.component.scss' ]
}) })
export class MyVideoChannelCreateComponent extends MyVideoChannelEdit implements OnInit { export class VideoChannelCreateComponent extends VideoChannelEdit implements OnInit {
error: string error: string
videoChannel = new VideoChannel({}) videoChannel = new VideoChannel({})

View File

@ -0,0 +1,96 @@
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<div class="margin-content">
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="form-row"> <!-- channel grid -->
<div class="form-group col-12 col-lg-4 col-xl-3">
<div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
<div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
</div>
<div class="form-group col-12 col-lg-8 col-xl-9">
<h6 i18n>Banner image of the channel</h6>
<my-actor-banner-edit
*ngIf="videoChannel" [previewImage]="isCreation()"
[actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
></my-actor-banner-edit>
<my-actor-avatar-edit
*ngIf="videoChannel" [previewImage]="isCreation()"
[actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
[displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
></my-actor-avatar-edit>
<div class="form-group" *ngIf="isCreation()">
<label i18n for="name">Name</label>
<div class="input-group">
<input
type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
>
<div class="input-group-append">
<span class="input-group-text">@{{ instanceHost }}</span>
</div>
</div>
<div *ngIf="formErrors['name']" class="form-error">
{{ formErrors['name'] }}
</div>
</div>
<div class="form-group">
<label i18n for="display-name">Display name</label>
<input
type="text" id="display-name" class="form-control"
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
>
<div *ngIf="formErrors['display-name']" class="form-error">
{{ formErrors['display-name'] }}
</div>
</div>
<div class="form-group">
<label i18n for="description">Description</label>
<textarea
id="description" formControlName="description" class="form-control"
[ngClass]="{ 'input-error': formErrors['description'] }"
></textarea>
<div *ngIf="formErrors.description" class="form-error">
{{ formErrors.description }}
</div>
</div>
<div class="form-group">
<label for="support">Support</label>
<my-help
helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support the channel (membership platform...).<br /><br />
When a video is uploaded in this channel, the video support field will be automatically filled by this text."
></my-help>
<my-markdown-textarea
id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced"
[classes]="{ 'input-error': formErrors['support'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.support" class="form-error">
{{ formErrors.support }}
</div>
</div>
<div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
<my-peertube-checkbox
inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
i18n-labelText labelText="Overwrite support field of all videos of this channel"
></my-peertube-checkbox>
</div>
</div>
</div>
<div class="form-row"> <!-- submit placement block -->
<div class="col-md-7 col-xl-5"></div>
<div class="col-md-5 col-xl-5 d-inline-flex">
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</div>
</div>
</form>
</div>

View File

@ -1,6 +1,10 @@
@use '_variables' as *; @use '_variables' as *;
@use '_mixins' as *; @use '_mixins' as *;
.margin-content {
padding-top: 20px;
}
label { label {
font-weight: $font-regular; font-weight: $font-regular;
font-size: 100%; font-size: 100%;

View File

@ -1,7 +1,7 @@
import { FormReactive } from '@app/shared/shared-forms' import { FormReactive } from '@app/shared/shared-forms'
import { VideoChannel } from '@app/shared/shared-main' import { VideoChannel } from '@app/shared/shared-main'
export abstract class MyVideoChannelEdit extends FormReactive { export abstract class VideoChannelEdit extends FormReactive {
videoChannel: VideoChannel videoChannel: VideoChannel
abstract isCreation (): boolean abstract isCreation (): boolean

View File

@ -2,7 +2,7 @@ import { Subscription } from 'rxjs'
import { HttpErrorResponse } from '@angular/common/http' import { HttpErrorResponse } from '@angular/common/http'
import { Component, OnDestroy, OnInit } from '@angular/core' import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router' import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, Notifier, ServerService } from '@app/core' import { AuthService, Notifier, RedirectService, ServerService } from '@app/core'
import { genericUploadErrorHandler } from '@app/helpers' import { genericUploadErrorHandler } from '@app/helpers'
import { import {
VIDEO_CHANNEL_DESCRIPTION_VALIDATOR, VIDEO_CHANNEL_DESCRIPTION_VALIDATOR,
@ -12,14 +12,14 @@ import {
import { FormValidatorService } from '@app/shared/shared-forms' import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models' import { HTMLServerConfig, VideoChannelUpdate } from '@shared/models'
import { MyVideoChannelEdit } from './my-video-channel-edit' import { VideoChannelEdit } from './video-channel-edit'
@Component({ @Component({
selector: 'my-video-channel-update', selector: 'my-video-channel-update',
templateUrl: './my-video-channel-edit.component.html', templateUrl: './video-channel-edit.component.html',
styleUrls: [ './my-video-channel-edit.component.scss' ] styleUrls: [ './video-channel-edit.component.scss' ]
}) })
export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements OnInit, OnDestroy { export class VideoChannelUpdateComponent extends VideoChannelEdit implements OnInit, OnDestroy {
error: string error: string
videoChannel: VideoChannel videoChannel: VideoChannel
@ -34,7 +34,8 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
private router: Router, private router: Router,
private route: ActivatedRoute, private route: ActivatedRoute,
private videoChannelService: VideoChannelService, private videoChannelService: VideoChannelService,
private serverService: ServerService private serverService: ServerService,
private redirectService: RedirectService
) { ) {
super() super()
} }
@ -50,9 +51,9 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
}) })
this.paramsSub = this.route.params.subscribe(routeParams => { this.paramsSub = this.route.params.subscribe(routeParams => {
const videoChannelId = routeParams['videoChannelId'] const videoChannelName = routeParams['videoChannelName']
this.videoChannelService.getVideoChannel(videoChannelId) this.videoChannelService.getVideoChannel(videoChannelName)
.subscribe({ .subscribe({
next: videoChannelToUpdate => { next: videoChannelToUpdate => {
this.videoChannel = videoChannelToUpdate this.videoChannel = videoChannelToUpdate
@ -95,7 +96,7 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`) this.notifier.success($localize`Video channel ${videoChannelUpdate.displayName} updated.`)
this.router.navigate([ '/my-library', 'video-channels' ]) this.redirectService.redirectToPreviousRoute([ '/c', this.videoChannel.name ])
}, },
error: err => { error: err => {

View File

@ -1,112 +0,0 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a routerLink="/my-library/video-channels" i18n>My Channels</a>
</li>
<ng-container *ngIf="isCreation()">
<li class="breadcrumb-item active" i18n>Create</li>
</ng-container>
<ng-container *ngIf="!isCreation()">
<li class="breadcrumb-item active" i18n>Edit</li>
<li class="breadcrumb-item active" aria-current="page">
<a *ngIf="videoChannel" [routerLink]="[ '/my-library/video-channels/update', videoChannel?.nameWithHost ]">{{ videoChannel?.displayName }}</a>
</li>
</ng-container>
</ol>
</nav>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="form-row"> <!-- channel grid -->
<div class="form-group col-12 col-lg-4 col-xl-3">
<div *ngIf="isCreation()" class="video-channel-title" i18n>NEW CHANNEL</div>
<div *ngIf="!isCreation() && videoChannel" class="video-channel-title" i18n>CHANNEL</div>
</div>
<div class="form-group col-12 col-lg-8 col-xl-9">
<h6 i18n>Banner image of your channel</h6>
<my-actor-banner-edit
*ngIf="videoChannel" [previewImage]="isCreation()"
[actor]="videoChannel" (bannerChange)="onBannerChange($event)" (bannerDelete)="onBannerDelete()"
></my-actor-banner-edit>
<my-actor-avatar-edit
*ngIf="videoChannel" [previewImage]="isCreation()"
[actor]="videoChannel" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
[displayUsername]="!isCreation()" [displaySubscribers]="!isCreation()"
></my-actor-avatar-edit>
<div class="form-group" *ngIf="isCreation()">
<label i18n for="name">Name</label>
<div class="input-group">
<input
type="text" id="name" i18n-placeholder placeholder="Example: my_channel"
formControlName="name" [ngClass]="{ 'input-error': formErrors['name'] }" class="form-control"
>
<div class="input-group-append">
<span class="input-group-text">@{{ instanceHost }}</span>
</div>
</div>
<div *ngIf="formErrors['name']" class="form-error">
{{ formErrors['name'] }}
</div>
</div>
<div class="form-group">
<label i18n for="display-name">Display name</label>
<input
type="text" id="display-name" class="form-control"
formControlName="display-name" [ngClass]="{ 'input-error': formErrors['display-name'] }"
>
<div *ngIf="formErrors['display-name']" class="form-error">
{{ formErrors['display-name'] }}
</div>
</div>
<div class="form-group">
<label i18n for="description">Description</label>
<textarea
id="description" formControlName="description" class="form-control"
[ngClass]="{ 'input-error': formErrors['description'] }"
></textarea>
<div *ngIf="formErrors.description" class="form-error">
{{ formErrors.description }}
</div>
</div>
<div class="form-group">
<label for="support">Support</label>
<my-help
helpType="markdownEnhanced" i18n-preHtml preHtml="Short text to tell people how they can support your channel (membership platform...).<br /><br />
When you will upload a video in this channel, the video support field will be automatically filled by this text."
></my-help>
<my-markdown-textarea
id="support" formControlName="support" textareaMaxWidth="500px" markdownType="enhanced"
[classes]="{ 'input-error': formErrors['support'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.support" class="form-error">
{{ formErrors.support }}
</div>
</div>
<div class="form-group" *ngIf="isBulkUpdateVideosDisplayed()">
<my-peertube-checkbox
inputName="bulkVideosSupportUpdate" formControlName="bulkVideosSupportUpdate"
i18n-labelText labelText="Overwrite support field of all videos of this channel"
></my-peertube-checkbox>
</div>
</div>
</div>
<div class="form-row"> <!-- submit placement block -->
<div class="col-md-7 col-xl-5"></div>
<div class="col-md-5 col-xl-5 d-inline-flex">
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</div>
</div>
</form>

View File

@ -1,7 +1,5 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { RouterModule, Routes } from '@angular/router' import { RouterModule, Routes } from '@angular/router'
import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
import { MyVideoChannelsComponent } from './my-video-channels.component' import { MyVideoChannelsComponent } from './my-video-channels.component'
const myVideoChannelsRoutes: Routes = [ const myVideoChannelsRoutes: Routes = [
@ -16,21 +14,11 @@ const myVideoChannelsRoutes: Routes = [
}, },
{ {
path: 'create', path: 'create',
component: MyVideoChannelCreateComponent, redirectTo: '/manage/create'
data: {
meta: {
title: $localize`Create a new video channel`
}
}
}, },
{ {
path: 'update/:videoChannelId', path: 'update/:videoChannelName',
component: MyVideoChannelUpdateComponent, redirectTo: '/manage/update/:videoChannelName'
data: {
meta: {
title: $localize`Update video channel`
}
}
} }
] ]

View File

@ -9,7 +9,7 @@
<div class="video-channels-header d-flex justify-content-between"> <div class="video-channels-header d-flex justify-content-between">
<my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter> <my-advanced-input-filter (search)="onSearch($event)"></my-advanced-input-filter>
<a class="create-button" routerLink="create"> <a class="create-button" routerLink="/manage/create">
<my-global-icon iconName="add" aria-hidden="true"></my-global-icon> <my-global-icon iconName="add" aria-hidden="true"></my-global-icon>
<ng-container i18n>Create video channel</ng-container> <ng-container i18n>Create video channel</ng-container>
</a> </a>
@ -37,7 +37,7 @@
<div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div> <div i18n class="video-channel-videos">{videoChannel.videosCount, plural, =0 {No videos} =1 {1 video} other {{{ videoChannel.videosCount }} videos}}</div>
<div class="video-channel-buttons"> <div class="video-channel-buttons">
<my-edit-button label [routerLink]="[ 'update', videoChannel.nameWithHost ]"></my-edit-button> <my-edit-button label [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]"></my-edit-button>
<my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button> <my-delete-button label (click)="deleteVideoChannel(videoChannel)"></my-delete-button>
</div> </div>

View File

@ -1,11 +1,8 @@
import { ChartModule } from 'primeng/chart' import { ChartModule } from 'primeng/chart'
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
import { SharedFormModule } from '@app/shared/shared-forms' import { SharedFormModule } from '@app/shared/shared-forms'
import { SharedGlobalIconModule } from '@app/shared/shared-icons' import { SharedGlobalIconModule } from '@app/shared/shared-icons'
import { SharedMainModule } from '@app/shared/shared-main' import { SharedMainModule } from '@app/shared/shared-main'
import { MyVideoChannelCreateComponent } from './my-video-channel-create.component'
import { MyVideoChannelUpdateComponent } from './my-video-channel-update.component'
import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module' import { MyVideoChannelsRoutingModule } from './my-video-channels-routing.module'
import { MyVideoChannelsComponent } from './my-video-channels.component' import { MyVideoChannelsComponent } from './my-video-channels.component'
import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module' import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-actor-image.module'
@ -19,14 +16,11 @@ import { SharedActorImageModule } from '@app/shared/shared-actor-image/shared-ac
SharedMainModule, SharedMainModule,
SharedFormModule, SharedFormModule,
SharedGlobalIconModule, SharedGlobalIconModule,
SharedActorImageEditModule,
SharedActorImageModule SharedActorImageModule
], ],
declarations: [ declarations: [
MyVideoChannelsComponent, MyVideoChannelsComponent
MyVideoChannelCreateComponent,
MyVideoChannelUpdateComponent
], ],
exports: [], exports: [],

View File

@ -6,11 +6,11 @@
<div class="channel-info"> <div class="channel-info">
<ng-template #buttonsTemplate> <ng-template #buttonsTemplate>
<a *ngIf="isManageable()" [routerLink]="[ '/my-library/video-channels/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n> <a *ngIf="isManageable()" [routerLink]="[ '/manage/update', videoChannel.nameWithHost ]" class="peertube-button-link orange-button" i18n>
Manage channel Manage channel
</a> </a>
<my-subscribe-button *ngIf="!isManageable()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button> <my-subscribe-button *ngIf="!isOwner()" #subscribeButton [videoChannels]="[videoChannel]"></my-subscribe-button>
<button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted"> <button *ngIf="videoChannel.support" (click)="showSupportModal()" class="support-button peertube-button orange-button-inverted">
<my-global-icon iconName="support" aria-hidden="true"></my-global-icon> <my-global-icon iconName="support" aria-hidden="true"></my-global-icon>

View File

@ -8,7 +8,7 @@ import { Account, ListOverflowItem, VideoChannel, VideoChannelService, VideoServ
import { BlocklistService } from '@app/shared/shared-moderation' import { BlocklistService } from '@app/shared/shared-moderation'
import { SupportModalComponent } from '@app/shared/shared-support-modal' import { SupportModalComponent } from '@app/shared/shared-support-modal'
import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription' import { SubscribeButtonComponent } from '@app/shared/shared-user-subscription'
import { HttpStatusCode } from '@shared/models' import { HttpStatusCode, UserRight } from '@shared/models'
@Component({ @Component({
templateUrl: './video-channels.component.html', templateUrl: './video-channels.component.html',
@ -98,12 +98,18 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
return this.authService.isLoggedIn() return this.authService.isLoggedIn()
} }
isManageable () { isOwner () {
if (!this.isUserLoggedIn()) return false if (!this.isUserLoggedIn()) return false
return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id return this.videoChannel?.ownerAccount.userId === this.authService.getUser().id
} }
isManageable () {
if (!this.isUserLoggedIn()) return false
return this.isOwner() || this.authService.getUser().hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL)
}
activateCopiedMessage () { activateCopiedMessage () {
this.notifier.success($localize`Username copied`) this.notifier.success($localize`Username copied`)
} }

View File

@ -12,6 +12,7 @@ import { VideoChannelPlaylistsComponent } from './video-channel-playlists/video-
import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component' import { VideoChannelVideosComponent } from './video-channel-videos/video-channel-videos.component'
import { VideoChannelsRoutingModule } from './video-channels-routing.module' import { VideoChannelsRoutingModule } from './video-channels-routing.module'
import { VideoChannelsComponent } from './video-channels.component' import { VideoChannelsComponent } from './video-channels.component'
import { SharedActorImageEditModule } from '@app/shared/shared-actor-image-edit'
@NgModule({ @NgModule({
imports: [ imports: [
@ -25,6 +26,7 @@ import { VideoChannelsComponent } from './video-channels.component'
SharedGlobalIconModule, SharedGlobalIconModule,
SharedSupportModal, SharedSupportModal,
SharedActorImageModule, SharedActorImageModule,
SharedActorImageEditModule,
SharedModerationModule SharedModerationModule
], ],

View File

@ -56,7 +56,11 @@ const routes: Routes = [
loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule), loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
canActivateChild: [ MetaGuard ] canActivateChild: [ MetaGuard ]
}, },
{
path: 'manage',
loadChildren: () => import('./+manage/manage.module').then(m => m.ManageModule),
canActivateChild: [ MetaGuard ]
},
{ {
path: 'p', path: 'p',
loadChildren: () => import('./+plugin-pages/plugin-pages.module').then(m => m.PluginPagesModule), loadChildren: () => import('./+plugin-pages/plugin-pages.module').then(m => m.PluginPagesModule),

View File

@ -46,7 +46,7 @@ export class RedirectService {
return this.defaultTrendingAlgorithm return this.defaultTrendingAlgorithm
} }
redirectToPreviousRoute () { redirectToPreviousRoute (fallbackRoute: string[] = null) {
const exceptions = [ const exceptions = [
'/verify-account', '/verify-account',
'/reset-password' '/reset-password'
@ -57,6 +57,10 @@ export class RedirectService {
if (!isException) return this.router.navigateByUrl(this.previousUrl) if (!isException) return this.router.navigateByUrl(this.previousUrl)
} }
if (fallbackRoute) {
return this.router.navigate(fallbackRoute)
}
return this.redirectToHomepage() return this.redirectToHomepage()
} }

View File

@ -20,7 +20,8 @@ import {
asyncMiddleware, asyncMiddleware,
executeIfActivityPub, executeIfActivityPub,
localAccountValidator, localAccountValidator,
localVideoChannelValidator, videoChannelsNameWithHostValidator,
ensureIsLocalChannel,
videosCustomGetValidator, videosCustomGetValidator,
videosShareValidator videosShareValidator
} from '../../middlewares' } from '../../middlewares'
@ -123,24 +124,28 @@ activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId/activity
) )
activityPubClientRouter.get( activityPubClientRouter.get(
[ '/video-channels/:name', '/video-channels/:name/videos', '/c/:name', '/c/:name/videos' ], [ '/video-channels/:nameWithHost', '/video-channels/:nameWithHost/videos', '/c/:nameWithHost', '/c/:nameWithHost/videos' ],
executeIfActivityPub, executeIfActivityPub,
asyncMiddleware(localVideoChannelValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
videoChannelController videoChannelController
) )
activityPubClientRouter.get('/video-channels/:name/followers', activityPubClientRouter.get('/video-channels/:nameWithHost/followers',
executeIfActivityPub, executeIfActivityPub,
asyncMiddleware(localVideoChannelValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(videoChannelFollowersController) asyncMiddleware(videoChannelFollowersController)
) )
activityPubClientRouter.get('/video-channels/:name/following', activityPubClientRouter.get('/video-channels/:nameWithHost/following',
executeIfActivityPub, executeIfActivityPub,
asyncMiddleware(localVideoChannelValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(videoChannelFollowingController) asyncMiddleware(videoChannelFollowingController)
) )
activityPubClientRouter.get('/video-channels/:name/playlists', activityPubClientRouter.get('/video-channels/:nameWithHost/playlists',
executeIfActivityPub, executeIfActivityPub,
asyncMiddleware(localVideoChannelValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(videoChannelPlaylistsController) asyncMiddleware(videoChannelPlaylistsController)
) )

View File

@ -4,7 +4,14 @@ import { Activity, ActivityPubCollection, ActivityPubOrderedCollection, RootActi
import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' import { HttpStatusCode } from '../../../shared/models/http/http-error-codes'
import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity' import { isActivityValid } from '../../helpers/custom-validators/activitypub/activity'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { asyncMiddleware, checkSignature, localAccountValidator, localVideoChannelValidator, signatureValidator } from '../../middlewares' import {
asyncMiddleware,
checkSignature,
ensureIsLocalChannel,
localAccountValidator,
signatureValidator,
videoChannelsNameWithHostValidator
} from '../../middlewares'
import { activityPubValidator } from '../../middlewares/validators/activitypub/activity' import { activityPubValidator } from '../../middlewares/validators/activitypub/activity'
const inboxRouter = express.Router() const inboxRouter = express.Router()
@ -23,10 +30,11 @@ inboxRouter.post('/accounts/:name/inbox',
asyncMiddleware(activityPubValidator), asyncMiddleware(activityPubValidator),
inboxController inboxController
) )
inboxRouter.post('/video-channels/:name/inbox', inboxRouter.post('/video-channels/:nameWithHost/inbox',
signatureValidator, signatureValidator,
asyncMiddleware(checkSignature), asyncMiddleware(checkSignature),
asyncMiddleware(localVideoChannelValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(activityPubValidator), asyncMiddleware(activityPubValidator),
inboxController inboxController
) )

View File

@ -1,15 +1,15 @@
import express from 'express' import express from 'express'
import { MActorLight } from '@server/types/models'
import { Activity } from '../../../shared/models/activitypub/activity' import { Activity } from '../../../shared/models/activitypub/activity'
import { VideoPrivacy } from '../../../shared/models/videos' import { VideoPrivacy } from '../../../shared/models/videos'
import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub' import { activityPubCollectionPagination, activityPubContextify } from '../../helpers/activitypub'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
import { buildAudience } from '../../lib/activitypub/audience' import { buildAudience } from '../../lib/activitypub/audience'
import { asyncMiddleware, localAccountValidator, localVideoChannelValidator } from '../../middlewares' import { buildAnnounceActivity, buildCreateActivity } from '../../lib/activitypub/send'
import { asyncMiddleware, ensureIsLocalChannel, localAccountValidator, videoChannelsNameWithHostValidator } from '../../middlewares'
import { apPaginationValidator } from '../../middlewares/validators/activitypub'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { activityPubResponse } from './utils' import { activityPubResponse } from './utils'
import { MActorLight } from '@server/types/models'
import { apPaginationValidator } from '../../middlewares/validators/activitypub'
const outboxRouter = express.Router() const outboxRouter = express.Router()
@ -19,9 +19,10 @@ outboxRouter.get('/accounts/:name/outbox',
asyncMiddleware(outboxController) asyncMiddleware(outboxController)
) )
outboxRouter.get('/video-channels/:name/outbox', outboxRouter.get('/video-channels/:nameWithHost/outbox',
apPaginationValidator, apPaginationValidator,
localVideoChannelValidator, asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
asyncMiddleware(outboxController) asyncMiddleware(outboxController)
) )

View File

@ -24,6 +24,7 @@ import {
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
commonVideosFiltersValidator, commonVideosFiltersValidator,
ensureCanManageChannel,
optionalAuthenticate, optionalAuthenticate,
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
@ -36,7 +37,7 @@ import {
videoPlaylistsSortValidator videoPlaylistsSortValidator
} from '../../middlewares' } from '../../middlewares'
import { import {
ensureAuthUserOwnsChannelValidator, ensureIsLocalChannel,
videoChannelsFollowersSortValidator, videoChannelsFollowersSortValidator,
videoChannelsListValidator, videoChannelsListValidator,
videoChannelsNameWithHostValidator, videoChannelsNameWithHostValidator,
@ -74,7 +75,8 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
authenticate, authenticate,
reqAvatarFile, reqAvatarFile,
asyncMiddleware(videoChannelsNameWithHostValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator, ensureIsLocalChannel,
ensureCanManageChannel,
updateAvatarValidator, updateAvatarValidator,
asyncMiddleware(updateVideoChannelAvatar) asyncMiddleware(updateVideoChannelAvatar)
) )
@ -83,7 +85,8 @@ videoChannelRouter.post('/:nameWithHost/banner/pick',
authenticate, authenticate,
reqBannerFile, reqBannerFile,
asyncMiddleware(videoChannelsNameWithHostValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator, ensureIsLocalChannel,
ensureCanManageChannel,
updateBannerValidator, updateBannerValidator,
asyncMiddleware(updateVideoChannelBanner) asyncMiddleware(updateVideoChannelBanner)
) )
@ -91,27 +94,33 @@ videoChannelRouter.post('/:nameWithHost/banner/pick',
videoChannelRouter.delete('/:nameWithHost/avatar', videoChannelRouter.delete('/:nameWithHost/avatar',
authenticate, authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator, ensureIsLocalChannel,
ensureCanManageChannel,
asyncMiddleware(deleteVideoChannelAvatar) asyncMiddleware(deleteVideoChannelAvatar)
) )
videoChannelRouter.delete('/:nameWithHost/banner', videoChannelRouter.delete('/:nameWithHost/banner',
authenticate, authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator, ensureIsLocalChannel,
ensureCanManageChannel,
asyncMiddleware(deleteVideoChannelBanner) asyncMiddleware(deleteVideoChannelBanner)
) )
videoChannelRouter.put('/:nameWithHost', videoChannelRouter.put('/:nameWithHost',
authenticate, authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator, ensureIsLocalChannel,
ensureCanManageChannel,
videoChannelsUpdateValidator, videoChannelsUpdateValidator,
asyncRetryTransactionMiddleware(updateVideoChannel) asyncRetryTransactionMiddleware(updateVideoChannel)
) )
videoChannelRouter.delete('/:nameWithHost', videoChannelRouter.delete('/:nameWithHost',
authenticate, authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator),
ensureIsLocalChannel,
ensureCanManageChannel,
asyncMiddleware(videoChannelsRemoveValidator), asyncMiddleware(videoChannelsRemoveValidator),
asyncRetryTransactionMiddleware(removeVideoChannel) asyncRetryTransactionMiddleware(removeVideoChannel)
) )
@ -145,7 +154,7 @@ videoChannelRouter.get('/:nameWithHost/videos',
videoChannelRouter.get('/:nameWithHost/followers', videoChannelRouter.get('/:nameWithHost/followers',
authenticate, authenticate,
asyncMiddleware(videoChannelsNameWithHostValidator), asyncMiddleware(videoChannelsNameWithHostValidator),
ensureAuthUserOwnsChannelValidator, ensureCanManageChannel,
paginationValidator, paginationValidator,
videoChannelsFollowersSortValidator, videoChannelsFollowersSortValidator,
setDefaultSort, setDefaultSort,

View File

@ -3,12 +3,6 @@ import { VideoChannelModel } from '@server/models/video/video-channel'
import { MChannelBannerAccountDefault } from '@server/types/models' import { MChannelBannerAccountDefault } from '@server/types/models'
import { HttpStatusCode } from '@shared/models' import { HttpStatusCode } from '@shared/models'
async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
return processVideoChannelExist(videoChannel, res)
}
async function doesVideoChannelIdExist (id: number, res: express.Response) { async function doesVideoChannelIdExist (id: number, res: express.Response) {
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id) const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
@ -24,7 +18,6 @@ async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: e
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
doesLocalVideoChannelNameExist,
doesVideoChannelIdExist, doesVideoChannelIdExist,
doesVideoChannelNameWithHostExist doesVideoChannelNameWithHostExist
} }

View File

@ -3,7 +3,7 @@ import { body, param, query } from 'express-validator'
import { omit } from 'lodash' import { omit } from 'lodash'
import { Hooks } from '@server/lib/plugins/hooks' import { Hooks } from '@server/lib/plugins/hooks'
import { MUserDefault } from '@server/types/models' import { MUserDefault } from '@server/types/models'
import { HttpStatusCode, UserRegister, UserRole } from '@shared/models' import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models'
import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
import { isThemeNameValid } from '../../helpers/custom-validators/plugins' import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import { import {
@ -490,14 +490,17 @@ const ensureAuthUserOwnsAccountValidator = [
} }
] ]
const ensureAuthUserOwnsChannelValidator = [ const ensureCanManageChannel = [
(req: express.Request, res: express.Response, next: express.NextFunction) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.user
const isUserOwner = res.locals.videoChannel.Account.userId === user.id
if (!isUserOwner && user.hasRight(UserRight.MANAGE_ANY_VIDEO_CHANNEL) === false) {
const message = `User ${user.username} does not have right to manage channel ${req.params.nameWithHost}.`
if (res.locals.videoChannel.Account.userId !== user.id) {
return res.fail({ return res.fail({
status: HttpStatusCode.FORBIDDEN_403, status: HttpStatusCode.FORBIDDEN_403,
message: 'Only owner of this video channel can access this ressource' message
}) })
} }
@ -542,8 +545,8 @@ export {
usersVerifyEmailValidator, usersVerifyEmailValidator,
userAutocompleteValidator, userAutocompleteValidator,
ensureAuthUserOwnsAccountValidator, ensureAuthUserOwnsAccountValidator,
ensureAuthUserOwnsChannelValidator, ensureCanManageUser,
ensureCanManageUser ensureCanManageChannel
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -1,7 +1,7 @@
import express from 'express' import express from 'express'
import { body, param, query } from 'express-validator' import { body, param, query } from 'express-validator'
import { MChannelAccountDefault, MUser } from '@server/types/models' import { CONFIG } from '@server/initializers/config'
import { UserRight } from '../../../../shared' import { MChannelAccountDefault } from '@server/types/models'
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc' import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
import { import {
@ -13,8 +13,7 @@ import {
import { logger } from '../../../helpers/logger' import { logger } from '../../../helpers/logger'
import { ActorModel } from '../../../models/actor/actor' import { ActorModel } from '../../../models/actor/actor'
import { VideoChannelModel } from '../../../models/video/video-channel' import { VideoChannelModel } from '../../../models/video/video-channel'
import { areValidationErrors, doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../shared' import { areValidationErrors, doesVideoChannelNameWithHostExist } from '../shared'
import { CONFIG } from '@server/initializers/config'
const videoChannelsAddValidator = [ const videoChannelsAddValidator = [
body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'), body('name').custom(isVideoChannelUsernameValid).withMessage('Should have a valid channel name'),
@ -71,16 +70,10 @@ const videoChannelsUpdateValidator = [
] ]
const videoChannelsRemoveValidator = [ const videoChannelsRemoveValidator = [
param('nameWithHost').exists().withMessage('Should have an video channel name with host'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params }) logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, res)) return
if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
if (!checkUserCanDeleteVideoChannel(res.locals.oauth.token.User, res.locals.videoChannel, res)) return
if (!await checkVideoChannelIsNotTheLastOne(res)) return
return next() return next()
} }
@ -100,14 +93,14 @@ const videoChannelsNameWithHostValidator = [
} }
] ]
const localVideoChannelValidator = [ const ensureIsLocalChannel = [
param('name').custom(isVideoChannelDisplayNameValid).withMessage('Should have a valid video channel name'), (req: express.Request, res: express.Response, next: express.NextFunction) => {
if (res.locals.videoChannel.Actor.isOwned() === false) {
async (req: express.Request, res: express.Response, next: express.NextFunction) => { return res.fail({
logger.debug('Checking localVideoChannelValidator parameters', { parameters: req.params }) status: HttpStatusCode.FORBIDDEN_403,
message: 'This channel is not owned.'
if (areValidationErrors(req, res)) return })
if (!await doesLocalVideoChannelNameExist(req.params.name, res)) return }
return next() return next()
} }
@ -144,38 +137,15 @@ export {
videoChannelsUpdateValidator, videoChannelsUpdateValidator,
videoChannelsRemoveValidator, videoChannelsRemoveValidator,
videoChannelsNameWithHostValidator, videoChannelsNameWithHostValidator,
ensureIsLocalChannel,
videoChannelsListValidator, videoChannelsListValidator,
localVideoChannelValidator,
videoChannelStatsValidator videoChannelStatsValidator
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, res: express.Response) {
if (videoChannel.Actor.isOwned() === false) { const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot remove video channel of another server.'
})
return false
}
// Check if the user can delete the video channel
// The user can delete it if s/he is an admin
// Or if s/he is the video channel's account
if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) {
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot remove video channel of another user'
})
return false
}
return true
}
async function checkVideoChannelIsNotTheLastOne (res: express.Response) {
const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
if (count <= 1) { if (count <= 1) {
res.fail({ res.fail({

View File

@ -33,6 +33,7 @@ describe('Test video channels', function () {
let totoChannel: number let totoChannel: number
let videoUUID: string let videoUUID: string
let accountName: string let accountName: string
let secondUserChannelName: string
const avatarPaths: { [ port: number ]: string } = {} const avatarPaths: { [ port: number ]: string } = {}
const bannerPaths: { [ port: number ]: string } = {} const bannerPaths: { [ port: number ]: string } = {}
@ -219,6 +220,35 @@ describe('Test video channels', function () {
} }
}) })
it('Should update another accounts video channel', async function () {
this.timeout(15000)
const result = await servers[0].users.generate('second_user')
secondUserChannelName = result.userChannelName
await servers[0].videos.quickUpload({ name: 'video', token: result.token })
const videoChannelAttributes = {
displayName: 'video channel updated',
description: 'video channel description updated',
support: 'support updated'
}
await servers[0].channels.update({ channelName: secondUserChannelName, attributes: videoChannelAttributes })
await waitJobs(servers)
})
it('Should have another accounts video channel updated', async function () {
for (const server of servers) {
const body = await server.channels.get({ channelName: `${secondUserChannelName}@${servers[0].host}` })
expect(body.displayName).to.equal('video channel updated')
expect(body.description).to.equal('video channel description updated')
expect(body.support).to.equal('support updated')
}
})
it('Should update the channel support field and update videos too', async function () { it('Should update the channel support field and update videos too', async function () {
this.timeout(35000) this.timeout(35000)
@ -368,12 +398,13 @@ describe('Test video channels', function () {
}) })
it('Should have video channel deleted', async function () { it('Should have video channel deleted', async function () {
const body = await servers[0].channels.list({ start: 0, count: 10 }) const body = await servers[0].channels.list({ start: 0, count: 10, sort: 'createdAt' })
expect(body.total).to.equal(1) expect(body.total).to.equal(2)
expect(body.data).to.be.an('array') expect(body.data).to.be.an('array')
expect(body.data).to.have.lengthOf(1) expect(body.data).to.have.lengthOf(2)
expect(body.data[0].displayName).to.equal('Main root channel') expect(body.data[0].displayName).to.equal('Main root channel')
expect(body.data[1].displayName).to.equal('video channel updated')
}) })
it('Should create the main channel with an uuid if there is a conflict', async function () { it('Should create the main channel with an uuid if there is a conflict', async function () {

View File

@ -14,8 +14,8 @@ const userRoleRights: { [ id in UserRole ]: UserRight[] } = {
[UserRole.MODERATOR]: [ [UserRole.MODERATOR]: [
UserRight.MANAGE_VIDEO_BLACKLIST, UserRight.MANAGE_VIDEO_BLACKLIST,
UserRight.MANAGE_ABUSES, UserRight.MANAGE_ABUSES,
UserRight.MANAGE_ANY_VIDEO_CHANNEL,
UserRight.REMOVE_ANY_VIDEO, UserRight.REMOVE_ANY_VIDEO,
UserRight.REMOVE_ANY_VIDEO_CHANNEL,
UserRight.REMOVE_ANY_VIDEO_PLAYLIST, UserRight.REMOVE_ANY_VIDEO_PLAYLIST,
UserRight.REMOVE_ANY_VIDEO_COMMENT, UserRight.REMOVE_ANY_VIDEO_COMMENT,
UserRight.UPDATE_ANY_VIDEO, UserRight.UPDATE_ANY_VIDEO,

View File

@ -202,7 +202,8 @@ export class UsersCommand extends AbstractCommand {
return { return {
token, token,
userId: user.id, userId: user.id,
userChannelId: me.videoChannels[0].id userChannelId: me.videoChannels[0].id,
userChannelName: me.videoChannels[0].name
} }
} }

View File

@ -22,9 +22,9 @@ export const enum UserRight {
MANAGE_SERVERS_BLOCKLIST, MANAGE_SERVERS_BLOCKLIST,
MANAGE_VIDEO_BLACKLIST, MANAGE_VIDEO_BLACKLIST,
MANAGE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO, REMOVE_ANY_VIDEO,
REMOVE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO_PLAYLIST, REMOVE_ANY_VIDEO_PLAYLIST,
REMOVE_ANY_VIDEO_COMMENT, REMOVE_ANY_VIDEO_COMMENT,