diff options
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 @@ | |||
1 | import { NgModule } from '@angular/core' | 1 | import { NgModule } from '@angular/core' |
2 | import { RouteReuseStrategy, RouterModule, Routes } from '@angular/router' | 2 | import { RouteReuseStrategy, RouterModule, Routes, UrlMatchResult, UrlSegment } from '@angular/router' |
3 | import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' | 3 | import { CustomReuseStrategy } from '@app/core/routing/custom-reuse-strategy' |
4 | import { MenuGuards } from '@app/core/routing/menu-guard.service' | 4 | import { MenuGuards } from '@app/core/routing/menu-guard.service' |
5 | import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' | 5 | import { POSSIBLE_LOCALES } from '@shared/core-utils/i18n' |
6 | import { MetaGuard, PreloadSelectedModulesList } from './core' | 6 | import { MetaGuard, PreloadSelectedModulesList } from './core' |
7 | import { EmptyComponent } from './empty.component' | 7 | import { EmptyComponent } from './empty.component' |
8 | import { RootComponent } from './root.component' | ||
8 | 9 | ||
9 | const routes: Routes = [ | 10 | const 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 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | ||
2 | import { catchError, distinctUntilChanged, map, switchMap } from 'rxjs/operators' | ||
3 | import { ActivatedRoute, Router } from '@angular/router' | ||
4 | import { RestExtractor } from '@app/core' | ||
5 | import { ActorService } from '@app/shared/shared-main/account' | ||
6 | import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes' | ||
7 | |||
8 | @Component({ | ||
9 | selector: 'my-root', | ||
10 | template: '' | ||
11 | }) | ||
12 | export 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 @@ | |||
1 | import { Observable, ReplaySubject } from 'rxjs' | ||
2 | import { catchError, map, tap } from 'rxjs/operators' | ||
3 | import { HttpClient } from '@angular/common/http' | ||
4 | import { Injectable } from '@angular/core' | ||
5 | import { RestExtractor } from '@app/core' | ||
6 | import { Account as ServerAccount, VideoChannel as ServerVideoChannel } from '@shared/models' | ||
7 | import { environment } from '../../../../environments/environment' | ||
8 | |||
9 | type KeysOfUnion<T> = T extends T ? keyof T: never | ||
10 | type ServerActor = KeysOfUnion<ServerAccount | ServerVideoChannel> | ||
11 | |||
12 | @Injectable() | ||
13 | export 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 @@ | |||
1 | export * from './account.model' | 1 | export * from './account.model' |
2 | export * from './account.service' | 2 | export * from './account.service' |
3 | export * from './actor.model' | 3 | export * from './actor.model' |
4 | export * 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 { | |||
17 | import { LoadingBarModule } from '@ngx-loading-bar/core' | 17 | import { LoadingBarModule } from '@ngx-loading-bar/core' |
18 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' | 18 | import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' |
19 | import { SharedGlobalIconModule } from '../shared-icons' | 19 | import { SharedGlobalIconModule } from '../shared-icons' |
20 | import { AccountService } from './account' | 20 | import { AccountService, ActorService } from './account' |
21 | import { | 21 | import { |
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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { JobQueue } from '../../lib/job-queue' | ||
3 | import { asyncMiddleware } from '../../middlewares' | ||
4 | import { actorNameWithHostGetValidator } from '../../middlewares/validators' | ||
5 | |||
6 | const actorRouter = express.Router() | ||
7 | |||
8 | actorRouter.get('/:actorName', | ||
9 | asyncMiddleware(actorNameWithHostGetValidator), | ||
10 | getActor | ||
11 | ) | ||
12 | |||
13 | // --------------------------------------------------------------------------- | ||
14 | |||
15 | export { | ||
16 | actorRouter | ||
17 | } | ||
18 | |||
19 | // --------------------------------------------------------------------------- | ||
20 | |||
21 | function 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' | |||
16 | import { searchRouter } from './search' | 16 | import { searchRouter } from './search' |
17 | import { serverRouter } from './server' | 17 | import { serverRouter } from './server' |
18 | import { usersRouter } from './users' | 18 | import { usersRouter } from './users' |
19 | import { actorRouter } from './actor' | ||
19 | import { videoChannelRouter } from './video-channel' | 20 | import { videoChannelRouter } from './video-channel' |
20 | import { videoPlaylistRouter } from './video-playlist' | 21 | import { videoPlaylistRouter } from './video-playlist' |
21 | import { videosRouter } from './videos' | 22 | import { videosRouter } from './videos' |
@@ -40,6 +41,7 @@ apiRouter.use('/bulk', bulkRouter) | |||
40 | apiRouter.use('/oauth-clients', oauthClientsRouter) | 41 | apiRouter.use('/oauth-clients', oauthClientsRouter) |
41 | apiRouter.use('/config', configRouter) | 42 | apiRouter.use('/config', configRouter) |
42 | apiRouter.use('/users', usersRouter) | 43 | apiRouter.use('/users', usersRouter) |
44 | apiRouter.use('/actors', actorRouter) | ||
43 | apiRouter.use('/accounts', accountsRouter) | 45 | apiRouter.use('/accounts', accountsRouter) |
44 | apiRouter.use('/video-channels', videoChannelRouter) | 46 | apiRouter.use('/video-channels', videoChannelRouter) |
45 | apiRouter.use('/video-playlists', videoPlaylistRouter) | 47 | apiRouter.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 |
22 | clientsRouter.use('/videos/watch/playlist/:id', asyncMiddleware(generateWatchPlaylistHtmlPage)) | 22 | clientsRouter.use('/videos/watch/playlist/:id', asyncMiddleware(generateWatchPlaylistHtmlPage)) |
23 | clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage)) | 23 | clientsRouter.use('/videos/watch/:id', asyncMiddleware(generateWatchHtmlPage)) |
24 | clientsRouter.use('/accounts/:nameWithHost', asyncMiddleware(generateAccountHtmlPage)) | 24 | clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ], asyncMiddleware(generateAccountHtmlPage)) |
25 | clientsRouter.use('/video-channels/:nameWithHost', asyncMiddleware(generateVideoChannelHtmlPage)) | 25 | clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ], asyncMiddleware(generateVideoChannelHtmlPage)) |
26 | clientsRouter.use('/@:nameWithHost', asyncMiddleware(generateActorHtmlPage)) | ||
26 | 27 | ||
27 | const embedMiddlewares = [ | 28 | const 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 | ||
159 | async 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 | |||
158 | async function generateManifest (req: express.Request, res: express.Response) { | 165 | async 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 @@ | |||
1 | import { isAccountNameValid } from './accounts' | ||
2 | import { isVideoChannelNameValid } from './video-channels' | ||
3 | |||
4 | function isActorNameValid (value: string) { | ||
5 | return isAccountNameValid(value) || isVideoChannelNameValid(value) | ||
6 | } | ||
7 | |||
8 | export { | ||
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' | |||
3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | 3 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' |
4 | import { VideoChannelModel } from '../../models/video/video-channel' | 4 | import { VideoChannelModel } from '../../models/video/video-channel' |
5 | 5 | ||
6 | async function doesLocalVideoChannelNameExist (name: string, res: express.Response) { | 6 | async 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 | ||
12 | async function doesVideoChannelIdExist (id: number, res: express.Response) { | 12 | async 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 | ||
18 | async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) { | 18 | async 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 | ||
32 | function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { | 32 | function 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 @@ | |||
1 | import * as express from 'express' | ||
2 | import { param } from 'express-validator' | ||
3 | import { isActorNameValid } from '../../helpers/custom-validators/actor' | ||
4 | import { logger } from '../../helpers/logger' | ||
5 | import { areValidationErrors } from './utils' | ||
6 | import { | ||
7 | doesAccountNameWithHostExist, | ||
8 | doesLocalAccountNameExist, | ||
9 | doesVideoChannelNameWithHostExist, | ||
10 | doesLocalVideoChannelNameExist | ||
11 | } from '../../helpers/middlewares' | ||
12 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
13 | |||
14 | const 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 | |||
34 | const 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 | |||
56 | export { | ||
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 @@ | |||
1 | export * from './abuse' | 1 | export * from './abuse' |
2 | export * from './account' | 2 | export * from './account' |
3 | export * from './actor' | ||
3 | export * from './actor-image' | 4 | export * from './actor-image' |
4 | export * from './blocklist' | 5 | export * from './blocklist' |
5 | export * from './oembed' | 6 | export * 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 | |||
3 | import 'mocha' | ||
4 | |||
5 | import { cleanupTests, flushAndRunServer, ServerInfo } from '../../../../shared/extra-utils' | ||
6 | import { getActor } from '../../../../shared/extra-utils/actors/actors' | ||
7 | import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes' | ||
8 | |||
9 | describe('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 | |||
3 | import { makeGetRequest } from '../requests/requests' | ||
4 | import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' | ||
5 | |||
6 | function 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 | |||
16 | export { | ||
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 @@ | |||
1 | export * from './actors/actors' | ||
1 | export * from './bulk/bulk' | 2 | export * from './bulk/bulk' |
2 | 3 | ||
3 | export * from './cli/cli' | 4 | export * from './cli/cli' |