}
getVideoChannelLink (videoChannel: VideoChannel) {
- return [ '/video-channels', videoChannel.nameWithHost ]
+ return [ '/c', videoChannel.nameWithHost ]
}
}
<ng-container *ngIf="!isCreation()">
<li class="breadcrumb-item active" i18n>Edit</li>
<li class="breadcrumb-item active" aria-current="page">
- <a *ngIf="user" [routerLink]="[ '/accounts', user?.username ]">{{ user?.username }}</a>
+ <a *ngIf="user" [routerLink]="[ '/a', user?.username ]">{{ user?.username }}</a>
</li>
</ng-container>
</ol>
</td>
<td *ngIf="isSelected('username')">
- <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
+ <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]">
<div class="chip two-lines">
<my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar>
<div>
<div class="video-channels">
<div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
- <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/video-channels', videoChannel.nameWithHost ]"></my-actor-avatar>
+ <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
<div class="video-channel-info">
- <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
+ <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
<div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
<div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
</a>
<div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
<div *ngFor="let videoChannel of videoChannels" class="video-channel">
- <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/video-channels', videoChannel.nameWithHost ]"></my-actor-avatar>
+ <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
<div class="video-channel-info">
- <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
+ <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
<div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
<div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
</a>
<div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
- <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner">
+ <a [routerLink]="[ '/a', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner">
<span i18n>Created by {{ videoChannel.ownerBy }}</span>
<my-actor-avatar [account]="videoChannel.ownerAccount" size="18"></my-actor-avatar>
} else if (channelResult.data.length !== 0) {
const channel = new VideoChannel(channelResult.data[0])
- redirectUrl = '/video-channels/' + channel.nameWithHost
+ redirectUrl = '/c/' + channel.nameWithHost
} else {
this.error = $localize`Cannot access to the remote resource`
return
const linkType = this.getVideoLinkType()
if (linkType === 'internal') {
- return [ '/video-channels', channel.nameWithHost ]
+ return [ '/c', channel.nameWithHost ]
}
if (linkType === 'lazy-load') {
}
getAccountUrl () {
- return [ '/accounts', this.videoChannel.ownerBy ]
+ return [ '/a', this.videoChannel.ownerBy ]
}
private loadChannelVideosCount () {
<div class="comment-account-date">
<div class="comment-account">
- <a [routerLink]="[ '/accounts', comment.by ]">
+ <a [routerLink]="[ '/a', comment.by ]">
<span class="comment-account-name" [ngClass]="{ 'video-author': video.account.id === comment.account.id }">
{{ comment.account.displayName }}
</span>
<div class="wrapper" [ngClass]="{ 'generic-channel': genericChannel }">
<my-actor-avatar
class="channel" [channel]="video.channel"
- [internalHref]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle"
+ [internalHref]="[ '/c', video.byVideoChannel ]" [title]="channelLinkTitle"
></my-actor-avatar>
<my-actor-avatar
class="account" [account]="video.account"
- [internalHref]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle">
+ [internalHref]="[ '/a', video.byAccount ]" [title]="accountLinkTitle">
</my-actor-avatar>
</div>
<div class="video-info-channel-left-links ml-1">
<ng-container *ngIf="!isChannelDisplayNameGeneric()">
- <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" i18n-title title="Channel page">
+ <a [routerLink]="[ '/c', video.byVideoChannel ]" i18n-title title="Channel page">
{{ video.channel.displayName }}
</a>
- <a [routerLink]="[ '/accounts', video.byAccount ]" i18n-title title="Account page">
+ <a [routerLink]="[ '/a', video.byAccount ]" i18n-title title="Account page">
<span i18n>By {{ video.byAccount }}</span>
</a>
</ng-container>
<ng-container *ngIf="isChannelDisplayNameGeneric()">
- <a [routerLink]="[ '/accounts', video.byAccount ]" class="single-link" i18n-title title="Account page">
+ <a [routerLink]="[ '/a', video.byAccount ]" class="single-link" i18n-title title="Account page">
<span i18n>{{ video.byAccount }}</span>
</a>
</ng-container>
<div class="section channel videos" *ngFor="let object of overview.channels">
<div class="section-title">
- <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]">
+ <a [routerLink]="[ '/c', buildVideoChannelBy(object) ]">
<my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar>
<h2 class="section-title">{{ object.channel.displayName }}</h2>
import { NgModule } from '@angular/core'
-import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router'
+import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment } from '@angular/router'
import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
import { MenuGuards } from '@app/core/routing/menu-guard.service'
import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
import { MetaGuard, PreloadSelectedModulesList } from './core'
import { EmptyComponent } from './empty.component'
+import { RootComponent } from './root.component'
const routes: Routes = [
{
canActivateChild: [ MetaGuard ]
},
{
- path: 'accounts',
+ path: 'a',
loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule),
canActivateChild: [ MetaGuard ]
},
{
- path: 'video-channels',
+ path: 'c',
loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
canActivateChild: [ MetaGuard ]
},
path: 'video-playlists/watch',
redirectTo: 'videos/watch/playlist'
},
+ {
+ path: 'accounts',
+ redirectTo: 'a'
+ },
+ {
+ path: 'video-channels',
+ redirectTo: 'c'
+ },
+ {
+ matcher: (url): UrlMatchResult => {
+ // Matches /@:actorName
+ if (url.length === 1 && url[0].path.match(/^@[\w]+$/gm)) {
+ return {
+ consumed: url,
+ posParams: {
+ actorName: new UrlSegment(url[0].path.substr(1), {})
+ }
+ }
+ }
+
+ return null
+ },
+ component: RootComponent
+ },
{
path: '',
component: EmptyComponent // Avoid 404, app component will redirect dynamically
</div>
<div ngbDropdownMenu>
- <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/accounts', user.account.nameWithHost ]"
+ <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/a', user.account.nameWithHost ]"
#profile (click)="onActiveLinkScrollToAnchor(profile)">
<my-global-icon iconName="go" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container>
</a>
--- /dev/null
+import { Component, OnInit } from '@angular/core'
+import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators'
+import { ActivatedRoute, Router } from '@angular/router'
+import { RestExtractor } from '@app/core'
+import { ActorService } from '@app/shared/shared-main/account'
+import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+
+@Component({
+ selector: 'my-root',
+ template: ''
+})
+export class RootComponent implements OnInit {
+ constructor (
+ private actorService: ActorService,
+ private route: ActivatedRoute,
+ private restExtractor: RestExtractor,
+ private router: Router
+ ) {
+ }
+
+ ngOnInit () {
+ this.route.params
+ .pipe(
+ map(params => params[ 'actorName' ]),
+ distinctUntilChanged(),
+ switchMap(actorName => this.actorService.getActorType(actorName)),
+ catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [
+ HttpStatusCode.BAD_REQUEST_400,
+ HttpStatusCode.NOT_FOUND_404
+ ]))
+ )
+ .subscribe(actorType => {
+ const actorName = this.route.snapshot.params[ 'actorName' ]
+
+ if (actorType === 'Account') {
+ this.router.navigate([ `/a/${actorName}` ], { state: { type: 'others', obj: { status: 200 } }, skipLocationChange: true })
+ }
+
+ if (actorType === 'VideoChannel') {
+ this.router.navigate([ `/c/${actorName}` ], { state: { type: 'others', obj: { status: 200 } }, skipLocationChange: true })
+ }
+ })
+ }
+}
}
getAccountUrl (abuse: ProcessedAbuse) {
- return '/accounts/' + abuse.flaggedAccount.nameWithHost
+ return '/a/' + abuse.flaggedAccount.nameWithHost
}
getVideoEmbed (abuse: AdminAbuse) {
--- /dev/null
+import { Observable, ReplaySubject } from 'rxjs'
+import { catchError, map, tap } from 'rxjs/operators'
+import { HttpClient } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { RestExtractor } from '@app/core'
+import { Account as ServerAccount, VideoChannel as ServerVideoChannel } from '@shared/models'
+import { environment } from '../../../../environments/environment'
+
+type KeysOfUnion<T> = T extends T ? keyof T: never
+type ServerActor = KeysOfUnion<ServerAccount | ServerVideoChannel>
+
+@Injectable()
+export class ActorService {
+ static BASE_ACTOR_API_URL = environment.apiUrl + '/api/v1/actors/'
+
+ actorLoaded = new ReplaySubject<string>(1)
+
+ constructor (
+ private authHttp: HttpClient,
+ private restExtractor: RestExtractor
+ ) {}
+
+ getActorType (actorName: string): Observable<string> {
+ return this.authHttp.get<ServerActor>(ActorService.BASE_ACTOR_API_URL + actorName)
+ .pipe(
+ map(actorHash => {
+ if (actorHash[ 'userId' ]) {
+ return 'Account'
+ }
+
+ return 'VideoChannel'
+ }),
+ tap(actor => this.actorLoaded.next(actor)),
+ catchError(res => this.restExtractor.handleError(res))
+ )
+ }
+}
export * from './account.model'
export * from './account.service'
export * from './actor.model'
+export * from './actor.service'
import { LoadingBarModule } from '@ngx-loading-bar/core'
import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
import { SharedGlobalIconModule } from '../shared-icons'
-import { AccountService } from './account'
+import { AccountService, ActorService } from './account'
import {
AutofocusDirective,
BytesPipe,
AUTH_INTERCEPTOR_PROVIDER,
AccountService,
+ ActorService,
UserHistoryService,
UserNotificationService,
}
private buildAccountUrl (account: { name: string, host: string }) {
- return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host)
+ return '/a/' + Actor.CREATE_BY_STRING(account.name, account.host)
}
private buildVideoImportUrl () {
if (this.account) {
this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
- this.account.localUrl = '/accounts/' + this.by
+ this.account.localUrl = '/a/' + this.by
}
}
}
<div class="d-flex video-miniature-meta">
<my-actor-avatar
*ngIf="displayOptions.avatar && displayOwnerVideoChannel()" [title]="channelLinkTitle"
- [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]"
+ [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
></my-actor-avatar>
<my-actor-avatar
*ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle"
- [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]"
+ [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
></my-actor-avatar>
<div class="w-100 d-flex flex-column">
</span>
</span>
- <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
+ <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/c', video.byVideoChannel ]">
{{ video.byAccount }}
</a>
- <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
+ <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/c', video.byVideoChannel ]">
{{ video.byVideoChannel }}
</a>
[attr.title]="playlistElement.video.name"
>{{ playlistElement.video.name }}</a>
- <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', playlistElement.video.byAccount ]">
+ <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/a', playlistElement.video.byAccount ]">
{{ playlistElement.video.byAccount }}
</a>
<span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span>
{{ playlist.displayName }}
</a>
- <a i18n [routerLink]="[ '/video-channels', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy">
+ <a i18n [routerLink]="[ '/c', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy">
{{ playlist.videoChannelBy }}
</a>
--- /dev/null
+import * as express from 'express'
+import { JobQueue } from '../../lib/job-queue'
+import { asyncMiddleware } from '../../middlewares'
+import { actorNameWithHostGetValidator } from '../../middlewares/validators'
+
+const actorRouter = express.Router()
+
+actorRouter.get('/:actorName',
+ asyncMiddleware(actorNameWithHostGetValidator),
+ getActor
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ actorRouter
+}
+
+// ---------------------------------------------------------------------------
+
+function getActor (req: express.Request, res: express.Response) {
+ let accountOrVideoChannel
+
+ if (res.locals.account) {
+ accountOrVideoChannel = res.locals.account
+ }
+
+ if (res.locals.videoChannel) {
+ accountOrVideoChannel = res.locals.videoChannel
+ }
+
+ if (accountOrVideoChannel.isOutdated()) {
+ JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: accountOrVideoChannel.Actor.url } })
+ }
+
+ return res.json(accountOrVideoChannel.toFormattedJSON())
+}
import { searchRouter } from './search'
import { serverRouter } from './server'
import { usersRouter } from './users'
+import { actorRouter } from './actor'
import { videoChannelRouter } from './video-channel'
import { videoPlaylistRouter } from './video-playlist'
import { videosRouter } from './videos'
apiRouter.use('/oauth-clients', oauthClientsRouter)
apiRouter.use('/config', configRouter)
apiRouter.use('/users', usersRouter)
+apiRouter.use('/actors', actorRouter)
apiRouter.use('/accounts', accountsRouter)
apiRouter.use('/video-channels', videoChannelRouter)
apiRouter.use('/video-playlists', videoPlaylistRouter)
// Do not use a template engine for a so little thing
clientsRouter.use('/videos/watch/playlist/:id', asyncMiddleware(generateWatchPlaylistHtmlPage))
clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
-clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage))
-clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage))
+clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ], asyncMiddleware(generateAccountHtmlPage))
+clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ], asyncMiddleware(generateVideoChannelHtmlPage))
+clientsRouter.use('/@:nameWithHost', asyncMiddleware(generateActorHtmlPage))
const embedMiddlewares = [
CONFIG.CSP.ENABLED
return sendHTML(html, res)
}
+async function generateActorHtmlPage (req: express.Request, res: express.Response) {
+ const html = await ClientHtml.getActorHTMLPage(req.params.nameWithHost, req, res)
+
+ return sendHTML(html, res)
+}
+
async function generateManifest (req: express.Request, res: express.Response) {
const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest')
const manifestJson = await readFile(manifestPhysicalPath, 'utf8')
--- /dev/null
+import { isAccountNameValid } from './accounts'
+import { isVideoChannelNameValid } from './video-channels'
+
+function isActorNameValid (value: string) {
+ return isAccountNameValid(value) || isVideoChannelNameValid(value)
+}
+
+export {
+ isActorNameValid
+}
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { VideoChannelModel } from '../../models/video/video-channel'
-async function doesLocalVideoChannelNameExist (name: string, res: express.Response) {
+async function doesLocalVideoChannelNameExist (name: string, res: express.Response, sendNotFound = true) {
const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
- return processVideoChannelExist(videoChannel, res)
+ return processVideoChannelExist(videoChannel, res, sendNotFound)
}
-async function doesVideoChannelIdExist (id: number, res: express.Response) {
+async function doesVideoChannelIdExist (id: number, res: express.Response, sendNotFound = true) {
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
- return processVideoChannelExist(videoChannel, res)
+ return processVideoChannelExist(videoChannel, res, sendNotFound)
}
-async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
+async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response, sendNotFound = true) {
const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
- return processVideoChannelExist(videoChannel, res)
+ return processVideoChannelExist(videoChannel, res, sendNotFound)
}
// ---------------------------------------------------------------------------
doesVideoChannelNameWithHostExist
}
-function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
+function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response, sendNotFound = true) {
if (!videoChannel) {
- res.status(HttpStatusCode.NOT_FOUND_404)
- .json({ error: 'Video channel not found' })
+ if (sendNotFound) {
+ res.status(HttpStatusCode.NOT_FOUND_404)
+ .json({ error: 'Video channel not found' })
+ }
return false
}
}
static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
- return this.getAccountOrChannelHTMLPage(() => AccountModel.loadByNameWithHost(nameWithHost), req, res)
+ const accountModelPromise = AccountModel.loadByNameWithHost(nameWithHost)
+ return this.getAccountOrChannelHTMLPage(() => accountModelPromise, req, res)
}
static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
- return this.getAccountOrChannelHTMLPage(() => VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost), req, res)
+ const videoChannelModelPromise = VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost)
+ return this.getAccountOrChannelHTMLPage(() => videoChannelModelPromise, req, res)
+ }
+
+ static async getActorHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
+ const accountModel = await AccountModel.loadByNameWithHost(nameWithHost)
+
+ if (accountModel) {
+ return this.getAccountOrChannelHTMLPage(() => new Promise(resolve => resolve(accountModel)), req, res)
+ } else {
+ const videoChannelModelPromise = VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost)
+ return this.getAccountOrChannelHTMLPage(() => videoChannelModelPromise, req, res)
+ }
}
static async getEmbedHTML () {
--- /dev/null
+import * as express from 'express'
+import { param } from 'express-validator'
+import { isActorNameValid } from '../../helpers/custom-validators/actor'
+import { logger } from '../../helpers/logger'
+import { areValidationErrors } from './utils'
+import {
+ doesAccountNameWithHostExist,
+ doesLocalAccountNameExist,
+ doesVideoChannelNameWithHostExist,
+ doesLocalVideoChannelNameExist
+} from '../../helpers/middlewares'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+
+const localActorValidator = [
+ param('actorName').custom(isActorNameValid).withMessage('Should have a valid actor name'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking localActorValidator parameters', { parameters: req.params })
+
+ if (areValidationErrors(req, res)) return
+
+ const isAccount = await doesLocalAccountNameExist(req.params.actorName, res, false)
+ const isVideoChannel = await doesLocalVideoChannelNameExist(req.params.actorName, res, false)
+
+ if (!isAccount || !isVideoChannel) {
+ res.status(HttpStatusCode.NOT_FOUND_404)
+ .json({ error: 'Actor not found' })
+ }
+
+ return next()
+ }
+]
+
+const actorNameWithHostGetValidator = [
+ param('actorName').exists().withMessage('Should have an actor name with host'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking actorNameWithHostGetValidator parameters', { parameters: req.params })
+
+ if (areValidationErrors(req, res)) return
+
+ const isAccount = await doesAccountNameWithHostExist(req.params.actorName, res, false)
+ const isVideoChannel = await doesVideoChannelNameWithHostExist(req.params.actorName, res, false)
+
+ if (!isAccount && !isVideoChannel) {
+ res.status(HttpStatusCode.NOT_FOUND_404)
+ .json({ error: 'Actor not found' })
+ }
+
+ return next()
+ }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+ localActorValidator,
+ actorNameWithHostGetValidator
+}
export * from './abuse'
export * from './account'
+export * from './actor'
export * from './actor-image'
export * from './blocklist'
export * from './oembed'
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import 'mocha'
+
+import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../../shared/extra-utils'
+import { getActor } from '../../../../shared/extra-utils/actors/actors'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+
+describe('Test actors API validators', function () {
+ let server: ServerInfo
+
+ // ---------------------------------------------------------------
+
+ before(async function () {
+ this.timeout(30000)
+
+ server = await flushAndRunServer(1)
+ })
+
+ describe('When getting an actor', function () {
+ it('Should return 404 with a non existing actorName', async function () {
+ await getActor(server.url, 'arfaze', HttpStatusCode.NOT_FOUND_404)
+ })
+
+ it('Should return 200 with an existing accountName', async function () {
+ await getActor(server.url, 'root', HttpStatusCode.OK_200)
+ })
+
+ it('Should return 200 with an existing channelName', async function () {
+ await getActor(server.url, 'root_channel', HttpStatusCode.OK_200)
+ })
+ })
+
+ after(async function () {
+ await cleanupTests([ server ])
+ })
+})
describe('Open Graph', function () {
it('Should have valid Open Graph tags on the account page', async function () {
- const res = await request(servers[0].url)
+ const accountPageTests = (res) => {
+ expect(res.text).to.contain(`<meta property="og:title" content="${account.displayName}" />`)
+ expect(res.text).to.contain(`<meta property="og:description" content="${account.description}" />`)
+ expect(res.text).to.contain('<meta property="og:type" content="website" />')
+ expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`)
+ }
+
+ accountPageTests(await request(servers[0].url)
.get('/accounts/' + servers[0].user.username)
.set('Accept', 'text/html')
- .expect(HttpStatusCode.OK_200)
+ .expect(HttpStatusCode.OK_200))
+
+ accountPageTests(await request(servers[0].url)
+ .get('/a/' + servers[0].user.username)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
- expect(res.text).to.contain(`<meta property="og:title" content="${account.displayName}" />`)
- expect(res.text).to.contain(`<meta property="og:description" content="${account.description}" />`)
- expect(res.text).to.contain('<meta property="og:type" content="website" />')
- expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`)
+ accountPageTests(await request(servers[0].url)
+ .get('/@' + servers[0].user.username)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
})
it('Should have valid Open Graph tags on the channel page', async function () {
- const res = await request(servers[0].url)
+ const channelPageOGtests = (res) => {
+ expect(res.text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`)
+ expect(res.text).to.contain(`<meta property="og:description" content="${channelDescription}" />`)
+ expect(res.text).to.contain('<meta property="og:type" content="website" />')
+ expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`)
+ }
+
+ channelPageOGtests(await request(servers[0].url)
.get('/video-channels/' + servers[0].videoChannel.name)
.set('Accept', 'text/html')
- .expect(HttpStatusCode.OK_200)
+ .expect(HttpStatusCode.OK_200))
+
+ channelPageOGtests(await request(servers[0].url)
+ .get('/c/' + servers[0].videoChannel.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
- expect(res.text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`)
- expect(res.text).to.contain(`<meta property="og:description" content="${channelDescription}" />`)
- expect(res.text).to.contain('<meta property="og:type" content="website" />')
- expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`)
+ channelPageOGtests(await request(servers[0].url)
+ .get('/@' + servers[0].videoChannel.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
})
it('Should have valid Open Graph tags on the watch page with video id', async function () {
})
it('Should have valid twitter card on the account page', async function () {
- const res = await request(servers[0].url)
+ const accountPageTests = (res) => {
+ expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
+ expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
+ expect(res.text).to.contain(`<meta property="twitter:title" content="${account.name}" />`)
+ expect(res.text).to.contain(`<meta property="twitter:description" content="${account.description}" />`)
+ }
+
+ accountPageTests(await request(servers[0].url)
.get('/accounts/' + account.name)
.set('Accept', 'text/html')
- .expect(HttpStatusCode.OK_200)
+ .expect(HttpStatusCode.OK_200))
- expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
- expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
- expect(res.text).to.contain(`<meta property="twitter:title" content="${account.name}" />`)
- expect(res.text).to.contain(`<meta property="twitter:description" content="${account.description}" />`)
+ accountPageTests(await request(servers[0].url)
+ .get('/a/' + account.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
+
+ accountPageTests(await request(servers[0].url)
+ .get('/@' + account.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
})
it('Should have valid twitter card on the channel page', async function () {
- const res = await request(servers[0].url)
+ const channelPageTests = (res) => {
+ expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
+ expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
+ expect(res.text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`)
+ expect(res.text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`)
+ }
+
+ channelPageTests(await request(servers[0].url)
.get('/video-channels/' + servers[0].videoChannel.name)
.set('Accept', 'text/html')
- .expect(HttpStatusCode.OK_200)
+ .expect(HttpStatusCode.OK_200))
- expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
- expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
- expect(res.text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`)
- expect(res.text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`)
+ channelPageTests(await request(servers[0].url)
+ .get('/c/' + servers[0].videoChannel.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
+
+ channelPageTests(await request(servers[0].url)
+ .get('/@' + servers[0].videoChannel.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
})
it('Should have valid twitter card if Twitter is whitelisted', async function () {
expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:card" content="player" />')
expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
- const resAccountRequest = await request(servers[0].url)
+ const accountTests = (res) => {
+ expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
+ expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
+ }
+
+ accountTests(await request(servers[0].url)
.get('/accounts/' + account.name)
.set('Accept', 'text/html')
- .expect(HttpStatusCode.OK_200)
+ .expect(HttpStatusCode.OK_200))
+
+ accountTests(await request(servers[0].url)
+ .get('/a/' + account.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
+
+ accountTests(await request(servers[0].url)
+ .get('/@' + account.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
- expect(resAccountRequest.text).to.contain('<meta property="twitter:card" content="summary" />')
- expect(resAccountRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
+ const channelTests = (res) => {
+ expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
+ expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
+ }
- const resChannelRequest = await request(servers[0].url)
+ channelTests(await request(servers[0].url)
.get('/video-channels/' + servers[0].videoChannel.name)
.set('Accept', 'text/html')
- .expect(HttpStatusCode.OK_200)
+ .expect(HttpStatusCode.OK_200))
+
+ channelTests(await request(servers[0].url)
+ .get('/c/' + servers[0].videoChannel.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
- expect(resChannelRequest.text).to.contain('<meta property="twitter:card" content="summary" />')
- expect(resChannelRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
+ channelTests(await request(servers[0].url)
+ .get('/@' + servers[0].videoChannel.name)
+ .set('Accept', 'text/html')
+ .expect(HttpStatusCode.OK_200))
})
})
})
it('Should use the original account URL for the canonical tag', async function () {
- const res = await makeHTMLRequest(servers[1].url, '/accounts/root@' + servers[0].host)
- expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/accounts/root" />`)
+ const accountURLtest = (res) => {
+ expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/accounts/root" />`)
+ }
+
+ accountURLtest(await makeHTMLRequest(servers[1].url, '/accounts/root@' + servers[0].host))
+ accountURLtest(await makeHTMLRequest(servers[1].url, '/a/root@' + servers[0].host))
+ accountURLtest(await makeHTMLRequest(servers[1].url, '/@root@' + servers[0].host))
})
it('Should use the original channel URL for the canonical tag', async function () {
- const res = await makeHTMLRequest(servers[1].url, '/video-channels/root_channel@' + servers[0].host)
- expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-channels/root_channel" />`)
+ const channelURLtests = (res) => {
+ expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-channels/root_channel" />`)
+ }
+
+ channelURLtests(await makeHTMLRequest(servers[1].url, '/video-channels/root_channel@' + servers[0].host))
+ channelURLtests(await makeHTMLRequest(servers[1].url, '/c/root_channel@' + servers[0].host))
+ channelURLtests(await makeHTMLRequest(servers[1].url, '/@root_channel@' + servers[0].host))
})
it('Should use the original playlist URL for the canonical tag', async function () {
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { makeGetRequest } from '../requests/requests'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+
+function getActor (url: string, actorName: string, statusCodeExpected = HttpStatusCode.OK_200) {
+ const path = '/api/v1/actors/' + actorName
+
+ return makeGetRequest({
+ url,
+ path,
+ statusCodeExpected
+ })
+}
+
+export {
+ getActor
+}
+export * from './actors/actors'
export * from './bulk/bulk'
export * from './cli/cli'