aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+accounts/account-video-channels/account-video-channels.component.ts2
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html2
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html2
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channels.component.html4
-rw-r--r--client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html6
-rw-r--r--client/src/app/+remote-interaction/remote-interaction.component.ts2
-rw-r--r--client/src/app/+search/search.component.ts2
-rw-r--r--client/src/app/+video-channels/video-channels.component.ts2
-rw-r--r--client/src/app/+videos/+video-watch/comment/video-comment.component.html2
-rw-r--r--client/src/app/+videos/+video-watch/video-avatar-channel.component.html4
-rw-r--r--client/src/app/+videos/+video-watch/video-watch.component.html6
-rw-r--r--client/src/app/+videos/video-list/overview/video-overview.component.html2
-rw-r--r--client/src/app/app-routing.module.ts31
-rw-r--r--client/src/app/menu/menu.component.html2
-rw-r--r--client/src/app/root.component.ts44
-rw-r--r--client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts2
-rw-r--r--client/src/app/shared/shared-main/account/actor.service.ts37
-rw-r--r--client/src/app/shared/shared-main/account/index.ts1
-rw-r--r--client/src/app/shared/shared-main/shared-main.module.ts3
-rw-r--r--client/src/app/shared/shared-main/users/user-notification.model.ts2
-rw-r--r--client/src/app/shared/shared-video-comment/video-comment.model.ts2
-rw-r--r--client/src/app/shared/shared-video-miniature/video-miniature.component.html8
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html2
-rw-r--r--client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html2
-rw-r--r--server/controllers/api/actor.ts37
-rw-r--r--server/controllers/api/index.ts2
-rw-r--r--server/controllers/client.ts11
-rw-r--r--server/helpers/custom-validators/actor.ts10
-rw-r--r--server/helpers/middlewares/video-channels.ts20
-rw-r--r--server/lib/client-html.ts17
-rw-r--r--server/middlewares/validators/actor.ts59
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/tests/api/check-params/actors.ts37
-rw-r--r--server/tests/client.ts154
-rw-r--r--shared/extra-utils/actors/actors.ts18
-rw-r--r--shared/extra-utils/index.ts1
36 files changed, 458 insertions, 81 deletions
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
index 7e916e122..e146a5cd2 100644
--- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
+++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.ts
@@ -139,6 +139,6 @@ export class AccountVideoChannelsComponent implements OnInit, OnDestroy {
139 } 139 }
140 140
141 getVideoChannelLink (videoChannel: VideoChannel) { 141 getVideoChannelLink (videoChannel: VideoChannel) {
142 return [ '/video-channels', videoChannel.nameWithHost ] 142 return [ '/c', videoChannel.nameWithHost ]
143 } 143 }
144} 144}
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html
index 5e92c0f36..772ebf272 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.component.html
+++ b/client/src/app/+admin/users/user-edit/user-edit.component.html
@@ -10,7 +10,7 @@
10 <ng-container *ngIf="!isCreation()"> 10 <ng-container *ngIf="!isCreation()">
11 <li class="breadcrumb-item active" i18n>Edit</li> 11 <li class="breadcrumb-item active" i18n>Edit</li>
12 <li class="breadcrumb-item active" aria-current="page"> 12 <li class="breadcrumb-item active" aria-current="page">
13 <a *ngIf="user" [routerLink]="[ '/accounts', user?.username ]">{{ user?.username }}</a> 13 <a *ngIf="user" [routerLink]="[ '/a', user?.username ]">{{ user?.username }}</a>
14 </li> 14 </li>
15 </ng-container> 15 </ng-container>
16 </ol> 16 </ol>
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 44d8a7e87..5b4f35c77 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -87,7 +87,7 @@
87 </td> 87 </td>
88 88
89 <td *ngIf="isSelected('username')"> 89 <td *ngIf="isSelected('username')">
90 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> 90 <a i18n-title title="Open account in a new tab" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/a/' + user.username ]">
91 <div class="chip two-lines"> 91 <div class="chip two-lines">
92 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar> 92 <my-actor-avatar [account]="user?.account" size="32"></my-actor-avatar>
93 <div> 93 <div>
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
index e41cbe921..9f139b4f2 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channels.component.html
@@ -17,10 +17,10 @@
17 17
18<div class="video-channels"> 18<div class="video-channels">
19 <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel"> 19 <div *ngFor="let videoChannel of videoChannels; let i = index" class="video-channel">
20 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/video-channels', videoChannel.nameWithHost ]"></my-actor-avatar> 20 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
21 21
22 <div class="video-channel-info"> 22 <div class="video-channel-info">
23 <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> 23 <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
24 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> 24 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
25 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> 25 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
26 </a> 26 </a>
diff --git a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
index f91cebacf..1bd459059 100644
--- a/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
+++ b/client/src/app/+my-library/my-subscriptions/my-subscriptions.component.html
@@ -14,17 +14,17 @@
14 14
15<div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()"> 15<div class="video-channels" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
16 <div *ngFor="let videoChannel of videoChannels" class="video-channel"> 16 <div *ngFor="let videoChannel of videoChannels" class="video-channel">
17 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/video-channels', videoChannel.nameWithHost ]"></my-actor-avatar> 17 <my-actor-avatar [channel]="videoChannel" [internalHref]="[ '/c', videoChannel.nameWithHost ]"></my-actor-avatar>
18 18
19 <div class="video-channel-info"> 19 <div class="video-channel-info">
20 <a [routerLink]="[ '/video-channels', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page"> 20 <a [routerLink]="[ '/c', videoChannel.nameWithHost ]" class="video-channel-names" i18n-title title="Channel page">
21 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div> 21 <div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
22 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div> 22 <div class="video-channel-name">{{ videoChannel.nameWithHost }}</div>
23 </a> 23 </a>
24 24
25 <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div> 25 <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
26 26
27 <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner"> 27 <a [routerLink]="[ '/a', videoChannel.ownerBy ]" i18n-title title="Owner account page" class="actor-owner">
28 <span i18n>Created by {{ videoChannel.ownerBy }}</span> 28 <span i18n>Created by {{ videoChannel.ownerBy }}</span>
29 29
30 <my-actor-avatar [account]="videoChannel.ownerAccount" size="18"></my-actor-avatar> 30 <my-actor-avatar [account]="videoChannel.ownerAccount" size="18"></my-actor-avatar>
diff --git a/client/src/app/+remote-interaction/remote-interaction.component.ts b/client/src/app/+remote-interaction/remote-interaction.component.ts
index e24607b24..3ebe62f49 100644
--- a/client/src/app/+remote-interaction/remote-interaction.component.ts
+++ b/client/src/app/+remote-interaction/remote-interaction.component.ts
@@ -43,7 +43,7 @@ export class RemoteInteractionComponent implements OnInit {
43 } else if (channelResult.data.length !== 0) { 43 } else if (channelResult.data.length !== 0) {
44 const channel = new VideoChannel(channelResult.data[0]) 44 const channel = new VideoChannel(channelResult.data[0])
45 45
46 redirectUrl = '/video-channels/' + channel.nameWithHost 46 redirectUrl = '/c/' + channel.nameWithHost
47 } else { 47 } else {
48 this.error = $localize`Cannot access to the remote resource` 48 this.error = $localize`Cannot access to the remote resource`
49 return 49 return
diff --git a/client/src/app/+search/search.component.ts b/client/src/app/+search/search.component.ts
index dcf654b7a..4381659e1 100644
--- a/client/src/app/+search/search.component.ts
+++ b/client/src/app/+search/search.component.ts
@@ -213,7 +213,7 @@ export class SearchComponent implements OnInit, OnDestroy {
213 const linkType = this.getVideoLinkType() 213 const linkType = this.getVideoLinkType()
214 214
215 if (linkType === 'internal') { 215 if (linkType === 'internal') {
216 return [ '/video-channels', channel.nameWithHost ] 216 return [ '/c', channel.nameWithHost ]
217 } 217 }
218 218
219 if (linkType === 'lazy-load') { 219 if (linkType === 'lazy-load') {
diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts
index 41fdb5e79..3833d9c54 100644
--- a/client/src/app/+video-channels/video-channels.component.ts
+++ b/client/src/app/+video-channels/video-channels.component.ts
@@ -112,7 +112,7 @@ export class VideoChannelsComponent implements OnInit, OnDestroy {
112 } 112 }
113 113
114 getAccountUrl () { 114 getAccountUrl () {
115 return [ '/accounts', this.videoChannel.ownerBy ] 115 return [ '/a', this.videoChannel.ownerBy ]
116 } 116 }
117 117
118 private loadChannelVideosCount () { 118 private loadChannelVideosCount () {
diff --git a/client/src/app/+videos/+video-watch/comment/video-comment.component.html b/client/src/app/+videos/+video-watch/comment/video-comment.component.html
index d7ba40ef6..fc0d66ffd 100644
--- a/client/src/app/+videos/+video-watch/comment/video-comment.component.html
+++ b/client/src/app/+videos/+video-watch/comment/video-comment.component.html
@@ -11,7 +11,7 @@
11 11
12 <div class="comment-account-date"> 12 <div class="comment-account-date">
13 <div class="comment-account"> 13 <div class="comment-account">
14 <a [routerLink]="[ '/accounts', comment.by ]"> 14 <a [routerLink]="[ '/a', comment.by ]">
15 <span class="comment-account-name" [ngClass]="{ 'video-author': video.account.id === comment.account.id }"> 15 <span class="comment-account-name" [ngClass]="{ 'video-author': video.account.id === comment.account.id }">
16 {{ comment.account.displayName }} 16 {{ comment.account.displayName }}
17 </span> 17 </span>
diff --git a/client/src/app/+videos/+video-watch/video-avatar-channel.component.html b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
index 5f149cbd1..5a7221858 100644
--- a/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
+++ b/client/src/app/+videos/+video-watch/video-avatar-channel.component.html
@@ -1,11 +1,11 @@
1<div class="wrapper" [ngClass]="{ 'generic-channel': genericChannel }"> 1<div class="wrapper" [ngClass]="{ 'generic-channel': genericChannel }">
2 <my-actor-avatar 2 <my-actor-avatar
3 class="channel" [channel]="video.channel" 3 class="channel" [channel]="video.channel"
4 [internalHref]="[ '/video-channels', video.byVideoChannel ]" [title]="channelLinkTitle" 4 [internalHref]="[ '/c', video.byVideoChannel ]" [title]="channelLinkTitle"
5 ></my-actor-avatar> 5 ></my-actor-avatar>
6 6
7 <my-actor-avatar 7 <my-actor-avatar
8 class="account" [account]="video.account" 8 class="account" [account]="video.account"
9 [internalHref]="[ '/accounts', video.byAccount ]" [title]="accountLinkTitle"> 9 [internalHref]="[ '/a', video.byAccount ]" [title]="accountLinkTitle">
10 </my-actor-avatar> 10 </my-actor-avatar>
11</div> 11</div>
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.html b/client/src/app/+videos/+video-watch/video-watch.component.html
index 4779602d2..bb41fba77 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -183,16 +183,16 @@
183 183
184 <div class="video-info-channel-left-links ml-1"> 184 <div class="video-info-channel-left-links ml-1">
185 <ng-container *ngIf="!isChannelDisplayNameGeneric()"> 185 <ng-container *ngIf="!isChannelDisplayNameGeneric()">
186 <a [routerLink]="[ '/video-channels', video.byVideoChannel ]" i18n-title title="Channel page"> 186 <a [routerLink]="[ '/c', video.byVideoChannel ]" i18n-title title="Channel page">
187 {{ video.channel.displayName }} 187 {{ video.channel.displayName }}
188 </a> 188 </a>
189 <a [routerLink]="[ '/accounts', video.byAccount ]" i18n-title title="Account page"> 189 <a [routerLink]="[ '/a', video.byAccount ]" i18n-title title="Account page">
190 <span i18n>By {{ video.byAccount }}</span> 190 <span i18n>By {{ video.byAccount }}</span>
191 </a> 191 </a>
192 </ng-container> 192 </ng-container>
193 193
194 <ng-container *ngIf="isChannelDisplayNameGeneric()"> 194 <ng-container *ngIf="isChannelDisplayNameGeneric()">
195 <a [routerLink]="[ '/accounts', video.byAccount ]" class="single-link" i18n-title title="Account page"> 195 <a [routerLink]="[ '/a', video.byAccount ]" class="single-link" i18n-title title="Account page">
196 <span i18n>{{ video.byAccount }}</span> 196 <span i18n>{{ video.byAccount }}</span>
197 </a> 197 </a>
198 </ng-container> 198 </ng-container>
diff --git a/client/src/app/+videos/video-list/overview/video-overview.component.html b/client/src/app/+videos/video-list/overview/video-overview.component.html
index e21bffb6c..d3c602aa5 100644
--- a/client/src/app/+videos/video-list/overview/video-overview.component.html
+++ b/client/src/app/+videos/video-list/overview/video-overview.component.html
@@ -32,7 +32,7 @@
32 32
33 <div class="section channel videos" *ngFor="let object of overview.channels"> 33 <div class="section channel videos" *ngFor="let object of overview.channels">
34 <div class="section-title"> 34 <div class="section-title">
35 <a [routerLink]="[ '/video-channels', buildVideoChannelBy(object) ]"> 35 <a [routerLink]="[ '/c', buildVideoChannelBy(object) ]">
36 <my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar> 36 <my-actor-avatar [channel]="buildVideoChannel(object)"></my-actor-avatar>
37 37
38 <h2 class="section-title">{{ object.channel.displayName }}</h2> 38 <h2 class="section-title">{{ object.channel.displayName }}</h2>
diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts
index 4e3cce590..4619c4046 100644
--- a/client/src/app/app-routing.module.ts
+++ b/client/src/app/app-routing.module.ts
@@ -1,10 +1,11 @@
1import { NgModule } from '@angular/core' 1import { NgModule } from '@angular/core'
2import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' 2import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment } from '@angular/router'
3import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' 3import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy'
4import { MenuGuards } from '@app/core/routing/menu-guard.service' 4import { MenuGuards } from '@app/core/routing/menu-guard.service'
5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' 5import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n'
6import { MetaGuard, PreloadSelectedModulesList } from './core' 6import { MetaGuard, PreloadSelectedModulesList } from './core'
7import { EmptyComponent } from './empty.component' 7import { EmptyComponent } from './empty.component'
8import { RootComponent } from './root.component'
8 9
9const routes: Routes = [ 10const routes: Routes = [
10 { 11 {
@@ -34,12 +35,12 @@ const routes: Routes = [
34 canActivateChild: [ MetaGuard ] 35 canActivateChild: [ MetaGuard ]
35 }, 36 },
36 { 37 {
37 path: 'accounts', 38 path: 'a',
38 loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule), 39 loadChildren: () => import('./+accounts/accounts.module').then(m => m.AccountsModule),
39 canActivateChild: [ MetaGuard ] 40 canActivateChild: [ MetaGuard ]
40 }, 41 },
41 { 42 {
42 path: 'video-channels', 43 path: 'c',
43 loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule), 44 loadChildren: () => import('./+video-channels/video-channels.module').then(m => m.VideoChannelsModule),
44 canActivateChild: [ MetaGuard ] 45 canActivateChild: [ MetaGuard ]
45 }, 46 },
@@ -83,6 +84,30 @@ const routes: Routes = [
83 redirectTo: 'videos/watch/playlist' 84 redirectTo: 'videos/watch/playlist'
84 }, 85 },
85 { 86 {
87 path: 'accounts',
88 redirectTo: 'a'
89 },
90 {
91 path: 'video-channels',
92 redirectTo: 'c'
93 },
94 {
95 matcher: (url): UrlMatchResult => {
96 // Matches /@:actorName
97 if (url.length === 1 && url[0].path.match(/^@[\w]+$/gm)) {
98 return {
99 consumed: url,
100 posParams: {
101 actorName: new UrlSegment(url[0].path.substr(1), {})
102 }
103 }
104 }
105
106 return null
107 },
108 component: RootComponent
109 },
110 {
86 path: '', 111 path: '',
87 component: EmptyComponent // Avoid 404, app component will redirect dynamically 112 component: EmptyComponent // Avoid 404, app component will redirect dynamically
88 } 113 }
diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html
index fcc0bc21a..2c2c4f260 100644
--- a/client/src/app/menu/menu.component.html
+++ b/client/src/app/menu/menu.component.html
@@ -18,7 +18,7 @@
18 </div> 18 </div>
19 19
20 <div ngbDropdownMenu> 20 <div ngbDropdownMenu>
21 <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/accounts', user.account.nameWithHost ]" 21 <a *ngIf="user.account" ngbDropdownItem ngbDropdownToggle class="dropdown-item" [routerLink]="[ '/a', user.account.nameWithHost ]"
22 #profile (click)="onActiveLinkScrollToAnchor(profile)"> 22 #profile (click)="onActiveLinkScrollToAnchor(profile)">
23 <my-global-icon iconName="go" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container> 23 <my-global-icon iconName="go" aria-hidden="true"></my-global-icon> <ng-container i18n>Public profile</ng-container>
24 </a> 24 </a>
diff --git a/client/src/app/root.component.ts b/client/src/app/root.component.ts
new file mode 100644
index 000000000..5a09e50d1
--- /dev/null
+++ b/client/src/app/root.component.ts
@@ -0,0 +1,44 @@
1import { Component, OnInit } from '@angular/core'
2import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators'
3import { ActivatedRoute, Router } from '@angular/router'
4import { RestExtractor } from '@app/core'
5import { ActorService } from '@app/shared/shared-main/account'
6import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
7
8@Component({
9 selector: 'my-root',
10 template: ''
11})
12export class RootComponent implements OnInit {
13 constructor (
14 private actorService: ActorService,
15 private route: ActivatedRoute,
16 private restExtractor: RestExtractor,
17 private router: Router
18 ) {
19 }
20
21 ngOnInit () {
22 this.route.params
23 .pipe(
24 map(params => params[ 'actorName' ]),
25 distinctUntilChanged(),
26 switchMap(actorName => this.actorService.getActorType(actorName)),
27 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, 'other', [
28 HttpStatusCode.BAD_REQUEST_400,
29 HttpStatusCode.NOT_FOUND_404
30 ]))
31 )
32 .subscribe(actorType => {
33 const actorName = this.route.snapshot.params[ 'actorName' ]
34
35 if (actorType === 'Account') {
36 this.router.navigate([ `/a/${actorName}` ], { state: { type: 'others', obj: { status: 200 } }, skipLocationChange: true })
37 }
38
39 if (actorType === 'VideoChannel') {
40 this.router.navigate([ `/c/${actorName}` ], { state: { type: 'others', obj: { status: 200 } }, skipLocationChange: true })
41 }
42 })
43 }
44}
diff --git a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
index 4dc2b4f10..07b9dddba 100644
--- a/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
+++ b/client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
@@ -124,7 +124,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit {
124 } 124 }
125 125
126 getAccountUrl (abuse: ProcessedAbuse) { 126 getAccountUrl (abuse: ProcessedAbuse) {
127 return '/accounts/' + abuse.flaggedAccount.nameWithHost 127 return '/a/' + abuse.flaggedAccount.nameWithHost
128 } 128 }
129 129
130 getVideoEmbed (abuse: AdminAbuse) { 130 getVideoEmbed (abuse: AdminAbuse) {
diff --git a/client/src/app/shared/shared-main/account/actor.service.ts b/client/src/app/shared/shared-main/account/actor.service.ts
new file mode 100644
index 000000000..464ed4519
--- /dev/null
+++ b/client/src/app/shared/shared-main/account/actor.service.ts
@@ -0,0 +1,37 @@
1import { Observable, ReplaySubject } from 'rxjs'
2import { catchError, map, tap } from 'rxjs/operators'
3import { HttpClient } from '@angular/common/http'
4import { Injectable } from '@angular/core'
5import { RestExtractor } from '@app/core'
6import { Account as ServerAccount, VideoChannel as ServerVideoChannel } from '@shared/models'
7import { environment } from '../../../../environments/environment'
8
9type KeysOfUnion<T> = T extends T ? keyof T: never
10type ServerActor = KeysOfUnion<ServerAccount | ServerVideoChannel>
11
12@Injectable()
13export class ActorService {
14 static BASE_ACTOR_API_URL = environment.apiUrl + '/api/v1/actors/'
15
16 actorLoaded = new ReplaySubject<string>(1)
17
18 constructor (
19 private authHttp: HttpClient,
20 private restExtractor: RestExtractor
21 ) {}
22
23 getActorType (actorName: string): Observable<string> {
24 return this.authHttp.get<ServerActor>(ActorService.BASE_ACTOR_API_URL + actorName)
25 .pipe(
26 map(actorHash => {
27 if (actorHash[ 'userId' ]) {
28 return 'Account'
29 }
30
31 return 'VideoChannel'
32 }),
33 tap(actor => this.actorLoaded.next(actor)),
34 catchError(res => this.restExtractor.handleError(res))
35 )
36 }
37}
diff --git a/client/src/app/shared/shared-main/account/index.ts b/client/src/app/shared/shared-main/account/index.ts
index b80ddb9f5..c6cdcd574 100644
--- a/client/src/app/shared/shared-main/account/index.ts
+++ b/client/src/app/shared/shared-main/account/index.ts
@@ -1,3 +1,4 @@
1export * from './account.model' 1export * from './account.model'
2export * from './account.service' 2export * from './account.service'
3export * from './actor.model' 3export * from './actor.model'
4export * from './actor.service'
diff --git a/client/src/app/shared/shared-main/shared-main.module.ts b/client/src/app/shared/shared-main/shared-main.module.ts
index f9b6085cf..f06f25ca5 100644
--- a/client/src/app/shared/shared-main/shared-main.module.ts
+++ b/client/src/app/shared/shared-main/shared-main.module.ts
@@ -17,7 +17,7 @@ import {
17import { LoadingBarModule } from '@ngx-loading-bar/core' 17import { LoadingBarModule } from '@ngx-loading-bar/core'
18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' 18import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
19import { SharedGlobalIconModule } from '../shared-icons' 19import { SharedGlobalIconModule } from '../shared-icons'
20import { AccountService } from './account' 20import { AccountService, ActorService } from './account'
21import { 21import {
22 AutofocusDirective, 22 AutofocusDirective,
23 BytesPipe, 23 BytesPipe,
@@ -161,6 +161,7 @@ import { VideoChannelService } from './video-channel'
161 AUTH_INTERCEPTOR_PROVIDER, 161 AUTH_INTERCEPTOR_PROVIDER,
162 162
163 AccountService, 163 AccountService,
164 ActorService,
164 165
165 UserHistoryService, 166 UserHistoryService,
166 UserNotificationService, 167 UserNotificationService,
diff --git a/client/src/app/shared/shared-main/users/user-notification.model.ts b/client/src/app/shared/shared-main/users/user-notification.model.ts
index ed5791794..002a01583 100644
--- a/client/src/app/shared/shared-main/users/user-notification.model.ts
+++ b/client/src/app/shared/shared-main/users/user-notification.model.ts
@@ -242,7 +242,7 @@ export class UserNotification implements UserNotificationServer {
242 } 242 }
243 243
244 private buildAccountUrl (account: { name: string, host: string }) { 244 private buildAccountUrl (account: { name: string, host: string }) {
245 return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host) 245 return '/a/' + Actor.CREATE_BY_STRING(account.name, account.host)
246 } 246 }
247 247
248 private buildVideoImportUrl () { 248 private buildVideoImportUrl () {
diff --git a/client/src/app/shared/shared-video-comment/video-comment.model.ts b/client/src/app/shared/shared-video-comment/video-comment.model.ts
index 9a4e3954e..1a2fe03db 100644
--- a/client/src/app/shared/shared-video-comment/video-comment.model.ts
+++ b/client/src/app/shared/shared-video-comment/video-comment.model.ts
@@ -95,7 +95,7 @@ export class VideoCommentAdmin implements VideoCommentAdminServerModel {
95 if (this.account) { 95 if (this.account) {
96 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host) 96 this.by = Actor.CREATE_BY_STRING(this.account.name, this.account.host)
97 97
98 this.account.localUrl = '/accounts/' + this.by 98 this.account.localUrl = '/a/' + this.by
99 } 99 }
100 } 100 }
101} 101}
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.html b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
index 645be92bd..6c34123ed 100644
--- a/client/src/app/shared/shared-video-miniature/video-miniature.component.html
+++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.html
@@ -12,12 +12,12 @@
12 <div class="d-flex video-miniature-meta"> 12 <div class="d-flex video-miniature-meta">
13 <my-actor-avatar 13 <my-actor-avatar
14 *ngIf="displayOptions.avatar && displayOwnerVideoChannel()" [title]="channelLinkTitle" 14 *ngIf="displayOptions.avatar && displayOwnerVideoChannel()" [title]="channelLinkTitle"
15 [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]" 15 [channel]="video.channel" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
16 ></my-actor-avatar> 16 ></my-actor-avatar>
17 17
18 <my-actor-avatar 18 <my-actor-avatar
19 *ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle" 19 *ngIf="displayOptions.avatar && displayOwnerAccount()" [title]="channelLinkTitle"
20 [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/video-channels', video.byVideoChannel ]" 20 [account]="video.account" [size]="actorImageSize" [internalHref]="[ '/c', video.byVideoChannel ]"
21 ></my-actor-avatar> 21 ></my-actor-avatar>
22 22
23 <div class="w-100 d-flex flex-column"> 23 <div class="w-100 d-flex flex-column">
@@ -39,10 +39,10 @@
39 </span> 39 </span>
40 </span> 40 </span>
41 41
42 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 42 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/c', video.byVideoChannel ]">
43 {{ video.byAccount }} 43 {{ video.byAccount }}
44 </a> 44 </a>
45 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]"> 45 <a tabindex="-1" *ngIf="displayOptions.by && displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/c', video.byVideoChannel ]">
46 {{ video.byVideoChannel }} 46 {{ video.byVideoChannel }}
47 </a> 47 </a>
48 48
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
index ec004a407..e74f58f47 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-element-miniature.component.html
@@ -20,7 +20,7 @@
20 [attr.title]="playlistElement.video.name" 20 [attr.title]="playlistElement.video.name"
21 >{{ playlistElement.video.name }}</a> 21 >{{ playlistElement.video.name }}</a>
22 22
23 <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', playlistElement.video.byAccount ]"> 23 <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/a', playlistElement.video.byAccount ]">
24 {{ playlistElement.video.byAccount }} 24 {{ playlistElement.video.byAccount }}
25 </a> 25 </a>
26 <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span> 26 <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span>
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
index f50f95003..81c36e6fe 100644
--- a/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
+++ b/client/src/app/shared/shared-video-playlist/video-playlist-miniature.component.html
@@ -19,7 +19,7 @@
19 {{ playlist.displayName }} 19 {{ playlist.displayName }}
20 </a> 20 </a>
21 21
22 <a i18n [routerLink]="[ '/video-channels', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy"> 22 <a i18n [routerLink]="[ '/c', playlist.videoChannelBy ]" class="by" *ngIf="displayChannel && playlist.videoChannelBy">
23 {{ playlist.videoChannelBy }} 23 {{ playlist.videoChannelBy }}
24 </a> 24 </a>
25 25
diff --git a/server/controllers/api/actor.ts b/server/controllers/api/actor.ts
new file mode 100644
index 000000000..da7f2eb91
--- /dev/null
+++ b/server/controllers/api/actor.ts
@@ -0,0 +1,37 @@
1import * as express from 'express'
2import { JobQueue } from '../../lib/job-queue'
3import { asyncMiddleware } from '../../middlewares'
4import { actorNameWithHostGetValidator } from '../../middlewares/validators'
5
6const actorRouter = express.Router()
7
8actorRouter.get('/:actorName',
9 asyncMiddleware(actorNameWithHostGetValidator),
10 getActor
11)
12
13// ---------------------------------------------------------------------------
14
15export {
16 actorRouter
17}
18
19// ---------------------------------------------------------------------------
20
21function getActor (req: express.Request, res: express.Response) {
22 let accountOrVideoChannel
23
24 if (res.locals.account) {
25 accountOrVideoChannel = res.locals.account
26 }
27
28 if (res.locals.videoChannel) {
29 accountOrVideoChannel = res.locals.videoChannel
30 }
31
32 if (accountOrVideoChannel.isOutdated()) {
33 JobQueue.Instance.createJob({ type: 'activitypub-refresher', payload: { type: 'actor', url: accountOrVideoChannel.Actor.url } })
34 }
35
36 return res.json(accountOrVideoChannel.toFormattedJSON())
37}
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 28378654a..9ffcf1337 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -16,6 +16,7 @@ import { pluginRouter } from './plugins'
16import { searchRouter } from './search' 16import { searchRouter } from './search'
17import { serverRouter } from './server' 17import { serverRouter } from './server'
18import { usersRouter } from './users' 18import { usersRouter } from './users'
19import { actorRouter } from './actor'
19import { videoChannelRouter } from './video-channel' 20import { videoChannelRouter } from './video-channel'
20import { videoPlaylistRouter } from './video-playlist' 21import { videoPlaylistRouter } from './video-playlist'
21import { videosRouter } from './videos' 22import { videosRouter } from './videos'
@@ -40,6 +41,7 @@ apiRouter.use('/bulk', bulkRouter)
40apiRouter.use('/oauth-clients', oauthClientsRouter) 41apiRouter.use('/oauth-clients', oauthClientsRouter)
41apiRouter.use('/config', configRouter) 42apiRouter.use('/config', configRouter)
42apiRouter.use('/users', usersRouter) 43apiRouter.use('/users', usersRouter)
44apiRouter.use('/actors', actorRouter)
43apiRouter.use('/accounts', accountsRouter) 45apiRouter.use('/accounts', accountsRouter)
44apiRouter.use('/video-channels', videoChannelRouter) 46apiRouter.use('/video-channels', videoChannelRouter)
45apiRouter.use('/video-playlists', videoPlaylistRouter) 47apiRouter.use('/video-playlists', videoPlaylistRouter)
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 022a17ff4..35e5af9d1 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -21,8 +21,9 @@ const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html')
21// Do not use a template engine for a so little thing 21// Do not use a template engine for a so little thing
22clientsRouter.use('/videos/watch/playlist/:id', asyncMiddleware(generateWatchPlaylistHtmlPage)) 22clientsRouter.use('/videos/watch/playlist/:id', asyncMiddleware(generateWatchPlaylistHtmlPage))
23clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage)) 23clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage))
24clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage)) 24clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ], asyncMiddleware(generateAccountHtmlPage))
25clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage)) 25clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ], asyncMiddleware(generateVideoChannelHtmlPage))
26clientsRouter.use('/@:nameWithHost', asyncMiddleware(generateActorHtmlPage))
26 27
27const embedMiddlewares = [ 28const embedMiddlewares = [
28 CONFIG.CSP.ENABLED 29 CONFIG.CSP.ENABLED
@@ -155,6 +156,12 @@ async function generateVideoChannelHtmlPage (req: express.Request, res: express.
155 return sendHTML(html, res) 156 return sendHTML(html, res)
156} 157}
157 158
159async function generateActorHtmlPage (req: express.Request, res: express.Response) {
160 const html = await ClientHtml.getActorHTMLPage(req.params.nameWithHost, req, res)
161
162 return sendHTML(html, res)
163}
164
158async function generateManifest (req: express.Request, res: express.Response) { 165async function generateManifest (req: express.Request, res: express.Response) {
159 const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest') 166 const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest')
160 const manifestJson = await readFile(manifestPhysicalPath, 'utf8') 167 const manifestJson = await readFile(manifestPhysicalPath, 'utf8')
diff --git a/server/helpers/custom-validators/actor.ts b/server/helpers/custom-validators/actor.ts
new file mode 100644
index 000000000..ad129e080
--- /dev/null
+++ b/server/helpers/custom-validators/actor.ts
@@ -0,0 +1,10 @@
1import { isAccountNameValid } from './accounts'
2import { isVideoChannelNameValid } from './video-channels'
3
4function isActorNameValid (value: string) {
5 return isAccountNameValid(value) || isVideoChannelNameValid(value)
6}
7
8export {
9 isActorNameValid
10}
diff --git a/server/helpers/middlewares/video-channels.ts b/server/helpers/middlewares/video-channels.ts
index e6eab65a2..e30ea90b3 100644
--- a/server/helpers/middlewares/video-channels.ts
+++ b/server/helpers/middlewares/video-channels.ts
@@ -3,22 +3,22 @@ import { MChannelBannerAccountDefault } from '@server/types/models'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { VideoChannelModel } from '../../models/video/video-channel' 4import { VideoChannelModel } from '../../models/video/video-channel'
5 5
6async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { 6async function doesLocalVideoChannelNameExist (name: string, res: express.Response, sendNotFound = true) {
7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name) 7 const videoChannel = await VideoChannelModel.loadLocalByNameAndPopulateAccount(name)
8 8
9 return processVideoChannelExist(videoChannel, res) 9 return processVideoChannelExist(videoChannel, res, sendNotFound)
10} 10}
11 11
12async function doesVideoChannelIdExist (id: number, res: express.Response) { 12async function doesVideoChannelIdExist (id: number, res: express.Response, sendNotFound = true) {
13 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id) 13 const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
14 14
15 return processVideoChannelExist(videoChannel, res) 15 return processVideoChannelExist(videoChannel, res, sendNotFound)
16} 16}
17 17
18async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) { 18async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response, sendNotFound = true) {
19 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain) 19 const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
20 20
21 return processVideoChannelExist(videoChannel, res) 21 return processVideoChannelExist(videoChannel, res, sendNotFound)
22} 22}
23 23
24// --------------------------------------------------------------------------- 24// ---------------------------------------------------------------------------
@@ -29,10 +29,12 @@ export {
29 doesVideoChannelNameWithHostExist 29 doesVideoChannelNameWithHostExist
30} 30}
31 31
32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { 32function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response, sendNotFound = true) {
33 if (!videoChannel) { 33 if (!videoChannel) {
34 res.status(HttpStatusCode.NOT_FOUND_404) 34 if (sendNotFound) {
35 .json({ error: 'Video channel not found' }) 35 res.status(HttpStatusCode.NOT_FOUND_404)
36 .json({ error: 'Video channel not found' })
37 }
36 38
37 return false 39 return false
38 } 40 }
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 4b2968e8b..2f6bce1c7 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -198,11 +198,24 @@ class ClientHtml {
198 } 198 }
199 199
200 static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { 200 static async getAccountHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
201 return this.getAccountOrChannelHTMLPage(() => AccountModel.loadByNameWithHost(nameWithHost), req, res) 201 const accountModelPromise = AccountModel.loadByNameWithHost(nameWithHost)
202 return this.getAccountOrChannelHTMLPage(() => accountModelPromise, req, res)
202 } 203 }
203 204
204 static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) { 205 static async getVideoChannelHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
205 return this.getAccountOrChannelHTMLPage(() => VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost), req, res) 206 const videoChannelModelPromise = VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost)
207 return this.getAccountOrChannelHTMLPage(() => videoChannelModelPromise, req, res)
208 }
209
210 static async getActorHTMLPage (nameWithHost: string, req: express.Request, res: express.Response) {
211 const accountModel = await AccountModel.loadByNameWithHost(nameWithHost)
212
213 if (accountModel) {
214 return this.getAccountOrChannelHTMLPage(() => new Promise(resolve => resolve(accountModel)), req, res)
215 } else {
216 const videoChannelModelPromise = VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithHost)
217 return this.getAccountOrChannelHTMLPage(() => videoChannelModelPromise, req, res)
218 }
206 } 219 }
207 220
208 static async getEmbedHTML () { 221 static async getEmbedHTML () {
diff --git a/server/middlewares/validators/actor.ts b/server/middlewares/validators/actor.ts
new file mode 100644
index 000000000..99b529dd6
--- /dev/null
+++ b/server/middlewares/validators/actor.ts
@@ -0,0 +1,59 @@
1import * as express from 'express'
2import { param } from 'express-validator'
3import { isActorNameValid } from '../../helpers/custom-validators/actor'
4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils'
6import {
7 doesAccountNameWithHostExist,
8 doesLocalAccountNameExist,
9 doesVideoChannelNameWithHostExist,
10 doesLocalVideoChannelNameExist
11} from '../../helpers/middlewares'
12import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
13
14const localActorValidator = [
15 param('actorName').custom(isActorNameValid).withMessage('Should have a valid actor name'),
16
17 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking localActorValidator parameters', { parameters: req.params })
19
20 if (areValidationErrors(req, res)) return
21
22 const isAccount = await doesLocalAccountNameExist(req.params.actorName, res, false)
23 const isVideoChannel = await doesLocalVideoChannelNameExist(req.params.actorName, res, false)
24
25 if (!isAccount || !isVideoChannel) {
26 res.status(HttpStatusCode.NOT_FOUND_404)
27 .json({ error: 'Actor not found' })
28 }
29
30 return next()
31 }
32]
33
34const actorNameWithHostGetValidator = [
35 param('actorName').exists().withMessage('Should have an actor name with host'),
36
37 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
38 logger.debug('Checking actorNameWithHostGetValidator parameters', { parameters: req.params })
39
40 if (areValidationErrors(req, res)) return
41
42 const isAccount = await doesAccountNameWithHostExist(req.params.actorName, res, false)
43 const isVideoChannel = await doesVideoChannelNameWithHostExist(req.params.actorName, res, false)
44
45 if (!isAccount && !isVideoChannel) {
46 res.status(HttpStatusCode.NOT_FOUND_404)
47 .json({ error: 'Actor not found' })
48 }
49
50 return next()
51 }
52]
53
54// ---------------------------------------------------------------------------
55
56export {
57 localActorValidator,
58 actorNameWithHostGetValidator
59}
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 24faeea3e..3e1a1e5ce 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,5 +1,6 @@
1export * from './abuse' 1export * from './abuse'
2export * from './account' 2export * from './account'
3export * from './actor'
3export * from './actor-image' 4export * from './actor-image'
4export * from './blocklist' 5export * from './blocklist'
5export * from './oembed' 6export * from './oembed'
diff --git a/server/tests/api/check-params/actors.ts b/server/tests/api/check-params/actors.ts
new file mode 100644
index 000000000..3a03edc39
--- /dev/null
+++ b/server/tests/api/check-params/actors.ts
@@ -0,0 +1,37 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import 'mocha'
4
5import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../../shared/extra-utils'
6import { getActor } from '../../../../shared/extra-utils/actors/actors'
7import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
8
9describe('Test actors API validators', function () {
10 let server: ServerInfo
11
12 // ---------------------------------------------------------------
13
14 before(async function () {
15 this.timeout(30000)
16
17 server = await flushAndRunServer(1)
18 })
19
20 describe('When getting an actor', function () {
21 it('Should return 404 with a non existing actorName', async function () {
22 await getActor(server.url, 'arfaze', HttpStatusCode.NOT_FOUND_404)
23 })
24
25 it('Should return 200 with an existing accountName', async function () {
26 await getActor(server.url, 'root', HttpStatusCode.OK_200)
27 })
28
29 it('Should return 200 with an existing channelName', async function () {
30 await getActor(server.url, 'root_channel', HttpStatusCode.OK_200)
31 })
32 })
33
34 after(async function () {
35 await cleanupTests([ server ])
36 })
37})
diff --git a/server/tests/client.ts b/server/tests/client.ts
index a385edd26..d9a472fdd 100644
--- a/server/tests/client.ts
+++ b/server/tests/client.ts
@@ -145,27 +145,51 @@ describe('Test a client controllers', function () {
145 describe('Open Graph', function () { 145 describe('Open Graph', function () {
146 146
147 it('Should have valid Open Graph tags on the account page', async function () { 147 it('Should have valid Open Graph tags on the account page', async function () {
148 const res = await request(servers[0].url) 148 const accountPageTests = (res) => {
149 expect(res.text).to.contain(`<meta property="og:title" content="${account.displayName}" />`)
150 expect(res.text).to.contain(`<meta property="og:description" content="${account.description}" />`)
151 expect(res.text).to.contain('<meta property="og:type" content="website" />')
152 expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`)
153 }
154
155 accountPageTests(await request(servers[0].url)
149 .get('/accounts/' + servers[0].user.username) 156 .get('/accounts/' + servers[0].user.username)
150 .set('Accept', 'text/html') 157 .set('Accept', 'text/html')
151 .expect(HttpStatusCode.OK_200) 158 .expect(HttpStatusCode.OK_200))
159
160 accountPageTests(await request(servers[0].url)
161 .get('/a/' + servers[0].user.username)
162 .set('Accept', 'text/html')
163 .expect(HttpStatusCode.OK_200))
152 164
153 expect(res.text).to.contain(`<meta property="og:title" content="${account.displayName}" />`) 165 accountPageTests(await request(servers[0].url)
154 expect(res.text).to.contain(`<meta property="og:description" content="${account.description}" />`) 166 .get('/@' + servers[0].user.username)
155 expect(res.text).to.contain('<meta property="og:type" content="website" />') 167 .set('Accept', 'text/html')
156 expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/accounts/${servers[0].user.username}" />`) 168 .expect(HttpStatusCode.OK_200))
157 }) 169 })
158 170
159 it('Should have valid Open Graph tags on the channel page', async function () { 171 it('Should have valid Open Graph tags on the channel page', async function () {
160 const res = await request(servers[0].url) 172 const channelPageOGtests = (res) => {
173 expect(res.text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`)
174 expect(res.text).to.contain(`<meta property="og:description" content="${channelDescription}" />`)
175 expect(res.text).to.contain('<meta property="og:type" content="website" />')
176 expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`)
177 }
178
179 channelPageOGtests(await request(servers[0].url)
161 .get('/video-channels/' + servers[0].videoChannel.name) 180 .get('/video-channels/' + servers[0].videoChannel.name)
162 .set('Accept', 'text/html') 181 .set('Accept', 'text/html')
163 .expect(HttpStatusCode.OK_200) 182 .expect(HttpStatusCode.OK_200))
183
184 channelPageOGtests(await request(servers[0].url)
185 .get('/c/' + servers[0].videoChannel.name)
186 .set('Accept', 'text/html')
187 .expect(HttpStatusCode.OK_200))
164 188
165 expect(res.text).to.contain(`<meta property="og:title" content="${servers[0].videoChannel.displayName}" />`) 189 channelPageOGtests(await request(servers[0].url)
166 expect(res.text).to.contain(`<meta property="og:description" content="${channelDescription}" />`) 190 .get('/@' + servers[0].videoChannel.name)
167 expect(res.text).to.contain('<meta property="og:type" content="website" />') 191 .set('Accept', 'text/html')
168 expect(res.text).to.contain(`<meta property="og:url" content="${servers[0].url}/video-channels/${servers[0].videoChannel.name}" />`) 192 .expect(HttpStatusCode.OK_200))
169 }) 193 })
170 194
171 it('Should have valid Open Graph tags on the watch page with video id', async function () { 195 it('Should have valid Open Graph tags on the watch page with video id', async function () {
@@ -232,27 +256,51 @@ describe('Test a client controllers', function () {
232 }) 256 })
233 257
234 it('Should have valid twitter card on the account page', async function () { 258 it('Should have valid twitter card on the account page', async function () {
235 const res = await request(servers[0].url) 259 const accountPageTests = (res) => {
260 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
261 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
262 expect(res.text).to.contain(`<meta property="twitter:title" content="${account.name}" />`)
263 expect(res.text).to.contain(`<meta property="twitter:description" content="${account.description}" />`)
264 }
265
266 accountPageTests(await request(servers[0].url)
236 .get('/accounts/' + account.name) 267 .get('/accounts/' + account.name)
237 .set('Accept', 'text/html') 268 .set('Accept', 'text/html')
238 .expect(HttpStatusCode.OK_200) 269 .expect(HttpStatusCode.OK_200))
239 270
240 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') 271 accountPageTests(await request(servers[0].url)
241 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') 272 .get('/a/' + account.name)
242 expect(res.text).to.contain(`<meta property="twitter:title" content="${account.name}" />`) 273 .set('Accept', 'text/html')
243 expect(res.text).to.contain(`<meta property="twitter:description" content="${account.description}" />`) 274 .expect(HttpStatusCode.OK_200))
275
276 accountPageTests(await request(servers[0].url)
277 .get('/@' + account.name)
278 .set('Accept', 'text/html')
279 .expect(HttpStatusCode.OK_200))
244 }) 280 })
245 281
246 it('Should have valid twitter card on the channel page', async function () { 282 it('Should have valid twitter card on the channel page', async function () {
247 const res = await request(servers[0].url) 283 const channelPageTests = (res) => {
284 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
285 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />')
286 expect(res.text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`)
287 expect(res.text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`)
288 }
289
290 channelPageTests(await request(servers[0].url)
248 .get('/video-channels/' + servers[0].videoChannel.name) 291 .get('/video-channels/' + servers[0].videoChannel.name)
249 .set('Accept', 'text/html') 292 .set('Accept', 'text/html')
250 .expect(HttpStatusCode.OK_200) 293 .expect(HttpStatusCode.OK_200))
251 294
252 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />') 295 channelPageTests(await request(servers[0].url)
253 expect(res.text).to.contain('<meta property="twitter:site" content="@Chocobozzz" />') 296 .get('/c/' + servers[0].videoChannel.name)
254 expect(res.text).to.contain(`<meta property="twitter:title" content="${servers[0].videoChannel.displayName}" />`) 297 .set('Accept', 'text/html')
255 expect(res.text).to.contain(`<meta property="twitter:description" content="${channelDescription}" />`) 298 .expect(HttpStatusCode.OK_200))
299
300 channelPageTests(await request(servers[0].url)
301 .get('/@' + servers[0].videoChannel.name)
302 .set('Accept', 'text/html')
303 .expect(HttpStatusCode.OK_200))
256 }) 304 })
257 305
258 it('Should have valid twitter card if Twitter is whitelisted', async function () { 306 it('Should have valid twitter card if Twitter is whitelisted', async function () {
@@ -280,21 +328,45 @@ describe('Test a client controllers', function () {
280 expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:card" content="player" />') 328 expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:card" content="player" />')
281 expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') 329 expect(resVideoPlaylistRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
282 330
283 const resAccountRequest = await request(servers[0].url) 331 const accountTests = (res) => {
332 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
333 expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
334 }
335
336 accountTests(await request(servers[0].url)
284 .get('/accounts/' + account.name) 337 .get('/accounts/' + account.name)
285 .set('Accept', 'text/html') 338 .set('Accept', 'text/html')
286 .expect(HttpStatusCode.OK_200) 339 .expect(HttpStatusCode.OK_200))
340
341 accountTests(await request(servers[0].url)
342 .get('/a/' + account.name)
343 .set('Accept', 'text/html')
344 .expect(HttpStatusCode.OK_200))
345
346 accountTests(await request(servers[0].url)
347 .get('/@' + account.name)
348 .set('Accept', 'text/html')
349 .expect(HttpStatusCode.OK_200))
287 350
288 expect(resAccountRequest.text).to.contain('<meta property="twitter:card" content="summary" />') 351 const channelTests = (res) => {
289 expect(resAccountRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') 352 expect(res.text).to.contain('<meta property="twitter:card" content="summary" />')
353 expect(res.text).to.contain('<meta property="twitter:site" content="@Kuja" />')
354 }
290 355
291 const resChannelRequest = await request(servers[0].url) 356 channelTests(await request(servers[0].url)
292 .get('/video-channels/' + servers[0].videoChannel.name) 357 .get('/video-channels/' + servers[0].videoChannel.name)
293 .set('Accept', 'text/html') 358 .set('Accept', 'text/html')
294 .expect(HttpStatusCode.OK_200) 359 .expect(HttpStatusCode.OK_200))
360
361 channelTests(await request(servers[0].url)
362 .get('/c/' + servers[0].videoChannel.name)
363 .set('Accept', 'text/html')
364 .expect(HttpStatusCode.OK_200))
295 365
296 expect(resChannelRequest.text).to.contain('<meta property="twitter:card" content="summary" />') 366 channelTests(await request(servers[0].url)
297 expect(resChannelRequest.text).to.contain('<meta property="twitter:site" content="@Kuja" />') 367 .get('/@' + servers[0].videoChannel.name)
368 .set('Accept', 'text/html')
369 .expect(HttpStatusCode.OK_200))
298 }) 370 })
299 }) 371 })
300 372
@@ -343,13 +415,23 @@ describe('Test a client controllers', function () {
343 }) 415 })
344 416
345 it('Should use the original account URL for the canonical tag', async function () { 417 it('Should use the original account URL for the canonical tag', async function () {
346 const res = await makeHTMLRequest(servers[1].url, '/accounts/root@' + servers[0].host) 418 const accountURLtest = (res) => {
347 expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/accounts/root" />`) 419 expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/accounts/root" />`)
420 }
421
422 accountURLtest(await makeHTMLRequest(servers[1].url, '/accounts/root@' + servers[0].host))
423 accountURLtest(await makeHTMLRequest(servers[1].url, '/a/root@' + servers[0].host))
424 accountURLtest(await makeHTMLRequest(servers[1].url, '/@root@' + servers[0].host))
348 }) 425 })
349 426
350 it('Should use the original channel URL for the canonical tag', async function () { 427 it('Should use the original channel URL for the canonical tag', async function () {
351 const res = await makeHTMLRequest(servers[1].url, '/video-channels/root_channel@' + servers[0].host) 428 const channelURLtests = (res) => {
352 expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-channels/root_channel" />`) 429 expect(res.text).to.contain(`<link rel="canonical" href="${servers[0].url}/video-channels/root_channel" />`)
430 }
431
432 channelURLtests(await makeHTMLRequest(servers[1].url, '/video-channels/root_channel@' + servers[0].host))
433 channelURLtests(await makeHTMLRequest(servers[1].url, '/c/root_channel@' + servers[0].host))
434 channelURLtests(await makeHTMLRequest(servers[1].url, '/@root_channel@' + servers[0].host))
353 }) 435 })
354 436
355 it('Should use the original playlist URL for the canonical tag', async function () { 437 it('Should use the original playlist URL for the canonical tag', async function () {
diff --git a/shared/extra-utils/actors/actors.ts b/shared/extra-utils/actors/actors.ts
new file mode 100644
index 000000000..4a4aba775
--- /dev/null
+++ b/shared/extra-utils/actors/actors.ts
@@ -0,0 +1,18 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { makeGetRequest } from '../requests/requests'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function getActor (url: string, actorName: string, statusCodeExpected = HttpStatusCode.OK_200) {
7 const path = '/api/v1/actors/' + actorName
8
9 return makeGetRequest({
10 url,
11 path,
12 statusCodeExpected
13 })
14}
15
16export {
17 getActor
18}
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts
index 3bc09ead5..9f5b5bb28 100644
--- a/shared/extra-utils/index.ts
+++ b/shared/extra-utils/index.ts
@@ -1,3 +1,4 @@
1export * from './actors/actors'
1export * from './bulk/bulk' 2export * from './bulk/bulk'
2 3
3export * from './cli/cli' 4export * from './cli/cli'