From 5beb89f223539f1e415a976ff104f772526b4d20 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Mon, 9 Nov 2020 16:25:27 +0100 Subject: [PATCH] refactor scoped token service --- .../about-instance.component.ts | 2 +- .../video-block-list.component.ts | 2 +- .../my-account-applications.component.html | 4 +- .../my-account-applications.component.ts | 59 ++++--- .../app/+my-account/my-account.component.ts | 10 +- .../src/app/+my-account/my-account.module.ts | 4 +- .../video-user-subscriptions.component.ts | 22 ++- client/src/app/core/auth/auth.service.ts | 45 ----- client/src/app/core/core.module.ts | 2 + client/src/app/core/index.ts | 1 + client/src/app/core/scoped-tokens/index.ts | 1 + .../scoped-tokens/scoped-tokens.service.ts | 33 ++++ client/src/app/helpers/utils.ts | 2 +- .../abuse-list-table.component.ts | 2 +- .../input-readonly-copy.component.ts | 1 - .../shared/shared-main/video/video.service.ts | 21 +-- client/src/environments/environment.e2e.ts | 2 +- client/src/environments/environment.hmr.ts | 2 +- client/src/environments/environment.prod.ts | 2 +- client/src/environments/environment.ts | 2 +- client/src/sass/include/_mixins.scss | 2 +- server/controllers/feeds.ts | 155 +++++++++++------- server/helpers/middlewares/accounts.ts | 8 +- server/middlewares/validators/feeds.ts | 5 +- server/tests/feeds/feeds.ts | 8 +- shared/extra-utils/feeds/feeds.ts | 2 +- 26 files changed, 223 insertions(+), 176 deletions(-) create mode 100644 client/src/app/core/scoped-tokens/index.ts create mode 100644 client/src/app/core/scoped-tokens/scoped-tokens.service.ts diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts index e74b5daeb..92ecd5263 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts @@ -3,7 +3,7 @@ import { AfterViewChecked, Component, OnInit, ViewChild } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component' import { Notifier } from '@app/core' -import { copyToClipboard } from '../../../assets/player/utils' +import { copyToClipboard } from '../../../root-helpers/utils' import { InstanceService } from '@app/shared/shared-instance' import { ServerConfig } from '@shared/models' import { ResolverData } from './about-instance.resolver' diff --git a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts index 1d0e56bfd..aa6b5d0a9 100644 --- a/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts +++ b/client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts @@ -161,7 +161,7 @@ export class VideoBlockListComponent extends RestTable implements OnInit, AfterV getVideoEmbed (entry: VideoBlacklist) { return buildVideoOrPlaylistEmbed( buildVideoLink({ - baseUrl: `${environment.embedUrl}/videos/embed/${entry.video.uuid}`, + baseUrl: `${environment.originServerUrl}/videos/embed/${entry.video.uuid}`, title: false, warningTitle: false }) diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.html b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html index 62e2cb59b..53a9c91ac 100644 --- a/client/src/app/+my-account/my-account-applications/my-account-applications.component.html +++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.html @@ -7,8 +7,8 @@

SUBSCRIPTION FEED

- Used to retrieve the list of videos of the creators - you subscribed to from outside PeerTube + Use third-party feed aggregators to retrieve the list of videos from + channels you subscribed to. Make sure to keep your token private.
diff --git a/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts b/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts index c3f09dfe3..233e42c83 100644 --- a/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts +++ b/client/src/app/+my-account/my-account-applications/my-account-applications.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit } from '@angular/core' -import { AuthService, Notifier, ConfirmService } from '@app/core' +import { AuthService, Notifier, ConfirmService, ScopedTokensService } from '@app/core' import { VideoService } from '@app/shared/shared-main' import { FeedFormat } from '@shared/models' -import { Subject, merge } from 'rxjs' -import { debounceTime } from 'rxjs/operators' +import { ScopedToken } from '@shared/models/users/user-scoped-token' +import { environment } from '../../../environments/environment' @Component({ selector: 'my-account-applications', @@ -15,11 +15,11 @@ export class MyAccountApplicationsComponent implements OnInit { feedUrl: string feedToken: string - private baseURL = window.location.protocol + '//' + window.location.host - private tokenStream = new Subject() + private baseURL = environment.originServerUrl constructor ( private authService: AuthService, + private scopedTokensService: ScopedTokensService, private videoService: VideoService, private notifier: Notifier, private confirmService: ConfirmService @@ -27,31 +27,40 @@ export class MyAccountApplicationsComponent implements OnInit { ngOnInit () { this.feedUrl = this.baseURL + this.scopedTokensService.getScopedTokens() + .subscribe( + tokens => this.regenApplications(tokens), - merge( - this.tokenStream, - this.authService.userInformationLoaded - ).pipe(debounceTime(400)) - .subscribe( - _ => { - const user = this.authService.getUser() - this.videoService.getVideoSubscriptionFeedUrls(user.account.id) - .then(feeds => this.feedUrl = this.baseURL + feeds.find(f => f.format === FeedFormat.RSS).url) - .then(_ => this.authService.getScopedTokens().then(tokens => this.feedToken = tokens.feedToken)) - }, - - err => { - this.notifier.error(err.message) - } - ) + err => { + this.notifier.error(err.message) + } + ) } async renewToken () { - const res = await this.confirmService.confirm('Renewing the token will disallow previously configured clients from retrieving the feed until they use the new token. Proceed?', 'Renew token') + const res = await this.confirmService.confirm( + $localize`Renewing the token will disallow previously configured clients from retrieving the feed until they use the new token. Proceed?`, + $localize`Renew token` + ) if (res === false) return - await this.authService.renewScopedTokens() - this.notifier.success('Token renewed. Update your client configuration accordingly.') - this.tokenStream.next() + this.scopedTokensService.renewScopedTokens().subscribe( + tokens => { + this.regenApplications(tokens) + this.notifier.success($localize`Token renewed. Update your client configuration accordingly.`) + }, + + err => { + this.notifier.error(err.message) + } + ) + + } + + private regenApplications (tokens: ScopedToken) { + const user = this.authService.getUser() + const feeds = this.videoService.getVideoSubscriptionFeedUrls(user.account.id, tokens.feedToken) + this.feedUrl = this.baseURL + feeds.find(f => f.format === FeedFormat.RSS).url + this.feedToken = tokens.feedToken } } diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index 12966aebb..eaf8a72e9 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts @@ -41,11 +41,6 @@ export class MyAccountComponent implements OnInit { label: $localize`Abuse reports`, routerLink: '/my-account/abuses', iconName: 'flag' - }, - { - label: $localize`Applications`, - routerLink: '/my-account/applications', - iconName: 'codesandbox' } ] } @@ -61,6 +56,11 @@ export class MyAccountComponent implements OnInit { routerLink: '/my-account/notifications' }, + { + label: $localize`Applications`, + routerLink: '/my-account/applications' + }, + moderationEntries ] } diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index 70bf58aae..076864563 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -20,8 +20,8 @@ import { MyAccountDangerZoneComponent } from './my-account-settings/my-account-d import { MyAccountNotificationPreferencesComponent } from './my-account-settings/my-account-notification-preferences' import { MyAccountProfileComponent } from './my-account-settings/my-account-profile/my-account-profile.component' import { MyAccountSettingsComponent } from './my-account-settings/my-account-settings.component' +import { MyAccountApplicationsComponent } from './my-account-applications/my-account-applications.component' import { MyAccountComponent } from './my-account.component' -import { VideoChangeOwnershipComponent } from './my-account-applications/my-account-applications.component' @NgModule({ imports: [ @@ -46,13 +46,13 @@ import { VideoChangeOwnershipComponent } from './my-account-applications/my-acco MyAccountChangePasswordComponent, MyAccountProfileComponent, MyAccountChangeEmailComponent, + MyAccountApplicationsComponent, MyAccountDangerZoneComponent, MyAccountBlocklistComponent, MyAccountAbusesListComponent, MyAccountServerBlocklistComponent, MyAccountNotificationsComponent, - MyAccountNotificationPreferencesComponent, MyAccountNotificationPreferencesComponent ], diff --git a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts index 10031d6cc..03881c295 100644 --- a/client/src/app/+videos/video-list/video-user-subscriptions.component.ts +++ b/client/src/app/+videos/video-list/video-user-subscriptions.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, LocalStorageService, Notifier, ScreenService, ServerService, UserService } from '@app/core' +import { AuthService, LocalStorageService, Notifier, ScopedTokensService, ScreenService, ServerService, UserService } from '@app/core' import { HooksService } from '@app/core/plugins/hooks.service' import { immutableAssign } from '@app/helpers' import { VideoService } from '@app/shared/shared-main' @@ -9,6 +9,7 @@ import { AbstractVideoList, OwnerDisplayType } from '@app/shared/shared-video-mi import { VideoSortField, FeedFormat } from '@shared/models' import { copyToClipboard } from '../../../root-helpers/utils' import { environment } from '../../../environments/environment' +import { forkJoin } from 'rxjs' @Component({ selector: 'my-videos-user-subscriptions', @@ -32,7 +33,8 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement protected storageService: LocalStorageService, private userSubscription: UserSubscriptionService, private hooks: HooksService, - private videoService: VideoService + private videoService: VideoService, + private scopedTokensService: ScopedTokensService ) { super() @@ -49,9 +51,19 @@ export class VideoUserSubscriptionsComponent extends AbstractVideoList implement super.ngOnInit() const user = this.authService.getUser() - let feedUrl = environment.embedUrl - this.videoService.getVideoSubscriptionFeedUrls(user.account.id) - .then((feeds: any) => feedUrl = feedUrl + feeds.find((f: any) => f.format === FeedFormat.RSS).url) + let feedUrl = environment.originServerUrl + + this.scopedTokensService.getScopedTokens().subscribe( + tokens => { + const feeds = this.videoService.getVideoSubscriptionFeedUrls(user.account.id, tokens.feedToken) + feedUrl = feedUrl + feeds.find((f: any) => f.format === FeedFormat.RSS).url + }, + + err => { + this.notifier.error(err.message) + } + ) + this.actions.unshift({ label: $localize`Feed`, iconName: 'syndication', diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 224f35f82..fd6062d3f 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -11,7 +11,6 @@ import { environment } from '../../../environments/environment' import { RestExtractor } from '../rest/rest-extractor.service' import { AuthStatus } from './auth-status.model' import { AuthUser } from './auth-user.model' -import { ScopedTokenType, ScopedToken } from '@shared/models/users/user-scoped-token' interface UserLoginWithUsername extends UserLogin { access_token: string @@ -27,7 +26,6 @@ export class AuthService { private static BASE_CLIENT_URL = environment.apiUrl + '/api/v1/oauth-clients/local' private static BASE_TOKEN_URL = environment.apiUrl + '/api/v1/users/token' private static BASE_REVOKE_TOKEN_URL = environment.apiUrl + '/api/v1/users/revoke-token' - private static BASE_SCOPED_TOKENS_URL = environment.apiUrl + '/api/v1/users/scoped-tokens' private static BASE_USER_INFORMATION_URL = environment.apiUrl + '/api/v1/users/me' private static LOCAL_STORAGE_OAUTH_CLIENT_KEYS = { CLIENT_ID: 'client_id', @@ -43,7 +41,6 @@ export class AuthService { private loginChanged: Subject private user: AuthUser = null private refreshingTokenObservable: Observable - private scopedTokens: ScopedToken constructor ( private http: HttpClient, @@ -247,48 +244,6 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular ) } - getScopedTokens (): Promise { - return new Promise((res, rej) => { - if (this.scopedTokens) return res(this.scopedTokens) - - const authHeaderValue = this.getRequestHeaderValue() - const headers = new HttpHeaders().set('Authorization', authHeaderValue) - - this.http.get(AuthService.BASE_SCOPED_TOKENS_URL, { headers }) - .subscribe( - scopedTokens => { - this.scopedTokens = scopedTokens - res(this.scopedTokens) - }, - - err => { - console.error(err) - rej(err) - } - ) - }) - } - - renewScopedTokens (): Promise { - return new Promise((res, rej) => { - const authHeaderValue = this.getRequestHeaderValue() - const headers = new HttpHeaders().set('Authorization', authHeaderValue) - - this.http.post(AuthService.BASE_SCOPED_TOKENS_URL, {}, { headers }) - .subscribe( - scopedTokens => { - this.scopedTokens = scopedTokens - res(this.scopedTokens) - }, - - err => { - console.error(err) - rej(err) - } - ) - }) - } - private mergeUserInformation (obj: UserLoginWithUsername): Observable { // User is not loaded yet, set manually auth header const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`) diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 6c0a2245d..f51f1920d 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts @@ -12,6 +12,7 @@ import { LoadingBarModule } from '@ngx-loading-bar/core' import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' import { LoadingBarRouterModule } from '@ngx-loading-bar/router' import { AuthService } from './auth' +import { ScopedTokensService } from './scoped-tokens' import { ConfirmService } from './confirm' import { CheatSheetComponent } from './hotkeys' import { MenuService } from './menu' @@ -57,6 +58,7 @@ import { LocalStorageService, ScreenService, SessionStorageService } from './wra providers: [ AuthService, + ScopedTokensService, ConfirmService, ServerService, ThemeService, diff --git a/client/src/app/core/index.ts b/client/src/app/core/index.ts index a0c34543d..9245ff6fc 100644 --- a/client/src/app/core/index.ts +++ b/client/src/app/core/index.ts @@ -1,4 +1,5 @@ export * from './auth' +export * from './scoped-tokens' export * from './confirm' export * from './hotkeys' export * from './menu' diff --git a/client/src/app/core/scoped-tokens/index.ts b/client/src/app/core/scoped-tokens/index.ts new file mode 100644 index 000000000..c9a48ffcd --- /dev/null +++ b/client/src/app/core/scoped-tokens/index.ts @@ -0,0 +1 @@ +export * from './scoped-tokens.service' diff --git a/client/src/app/core/scoped-tokens/scoped-tokens.service.ts b/client/src/app/core/scoped-tokens/scoped-tokens.service.ts new file mode 100644 index 000000000..8e3697c31 --- /dev/null +++ b/client/src/app/core/scoped-tokens/scoped-tokens.service.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@angular/core' +import { HttpClient } from '@angular/common/http' +import { environment } from '../../../environments/environment' +import { AuthService } from '../auth' +import { ScopedToken } from '@shared/models/users/user-scoped-token' +import { catchError } from 'rxjs/operators' +import { RestExtractor } from '../rest' + +@Injectable() +export class ScopedTokensService { + private static BASE_SCOPED_TOKENS_URL = environment.apiUrl + '/api/v1/users/scoped-tokens' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor + ) {} + + getScopedTokens () { + return this.authHttp + .get(ScopedTokensService.BASE_SCOPED_TOKENS_URL) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } + + renewScopedTokens () { + return this.authHttp + .post(ScopedTokensService.BASE_SCOPED_TOKENS_URL, {}) + .pipe( + catchError(res => this.restExtractor.handleError(res)) + ) + } +} diff --git a/client/src/app/helpers/utils.ts b/client/src/app/helpers/utils.ts index a22507f46..9c805b4ca 100644 --- a/client/src/app/helpers/utils.ts +++ b/client/src/app/helpers/utils.ts @@ -58,7 +58,7 @@ function getAbsoluteAPIUrl () { } function getAbsoluteEmbedUrl () { - let absoluteEmbedUrl = environment.embedUrl + let absoluteEmbedUrl = environment.originServerUrl if (!absoluteEmbedUrl) { // The Embed is on the same domain absoluteEmbedUrl = window.location.origin 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 ca0d23699..807665b9c 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 @@ -112,7 +112,7 @@ export class AbuseListTableComponent extends RestTable implements OnInit, AfterV getVideoEmbed (abuse: AdminAbuse) { return buildVideoOrPlaylistEmbed( buildVideoLink({ - baseUrl: `${environment.embedUrl}/videos/embed/${abuse.video.uuid}`, + baseUrl: `${environment.originServerUrl}/videos/embed/${abuse.video.uuid}`, title: false, warningTitle: false, startTime: abuse.video.startAt, diff --git a/client/src/app/shared/shared-forms/input-readonly-copy.component.ts b/client/src/app/shared/shared-forms/input-readonly-copy.component.ts index 520827a53..b04d69d05 100644 --- a/client/src/app/shared/shared-forms/input-readonly-copy.component.ts +++ b/client/src/app/shared/shared-forms/input-readonly-copy.component.ts @@ -1,6 +1,5 @@ import { Component, Input } from '@angular/core' import { Notifier } from '@app/core' -import { FormGroup } from '@angular/forms' @Component({ selector: 'my-input-readonly-copy', diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts index b81540e8d..70be5d7d2 100644 --- a/client/src/app/shared/shared-main/video/video.service.ts +++ b/client/src/app/shared/shared-main/video/video.service.ts @@ -18,8 +18,7 @@ import { VideoFilter, VideoPrivacy, VideoSortField, - VideoUpdate, - VideoCreate + VideoUpdate } from '@shared/models' import { environment } from '../../../../environments/environment' import { Account } from '../account/account.model' @@ -44,13 +43,13 @@ export interface VideosProvider { export class VideoService implements VideosProvider { static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' static BASE_FEEDS_URL = environment.apiUrl + '/feeds/videos.' + static BASE_SUBSCRIPTION_FEEDS_URL = environment.apiUrl + '/feeds/subscriptions.' constructor ( private authHttp: HttpClient, private restExtractor: RestExtractor, private restService: RestService, - private serverService: ServerService, - private authService: AuthService + private serverService: ServerService ) {} getVideoViewUrl (uuid: string) { @@ -238,22 +237,22 @@ export class VideoService implements VideosProvider { ) } - buildBaseFeedUrls (params: HttpParams) { + buildBaseFeedUrls (params: HttpParams, base = VideoService.BASE_FEEDS_URL) { const feeds = [ { format: FeedFormat.RSS, label: 'media rss 2.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.RSS.toLowerCase() + url: base + FeedFormat.RSS.toLowerCase() }, { format: FeedFormat.ATOM, label: 'atom 1.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.ATOM.toLowerCase() + url: base + FeedFormat.ATOM.toLowerCase() }, { format: FeedFormat.JSON, label: 'json 1.0', - url: VideoService.BASE_FEEDS_URL + FeedFormat.JSON.toLowerCase() + url: base + FeedFormat.JSON.toLowerCase() } ] @@ -294,14 +293,12 @@ export class VideoService implements VideosProvider { return this.buildBaseFeedUrls(params) } - async getVideoSubscriptionFeedUrls (accountId: number) { + getVideoSubscriptionFeedUrls (accountId: number, feedToken: string) { let params = this.restService.addRestGetParams(new HttpParams()) params = params.set('accountId', accountId.toString()) - - const { feedToken } = await this.authService.getScopedTokens() params = params.set('token', feedToken) - return this.buildBaseFeedUrls(params) + return this.buildBaseFeedUrls(params, VideoService.BASE_SUBSCRIPTION_FEEDS_URL) } getVideoFileMetadata (metadataUrl: string) { diff --git a/client/src/environments/environment.e2e.ts b/client/src/environments/environment.e2e.ts index b33ff9f86..a1a58e36f 100644 --- a/client/src/environments/environment.e2e.ts +++ b/client/src/environments/environment.e2e.ts @@ -2,5 +2,5 @@ export const environment = { production: false, hmr: false, apiUrl: 'http://localhost:9001', - embedUrl: 'http://localhost:9001' + originServerUrl: 'http://localhost:9001' } diff --git a/client/src/environments/environment.hmr.ts b/client/src/environments/environment.hmr.ts index 3b6eff302..ab7631920 100644 --- a/client/src/environments/environment.hmr.ts +++ b/client/src/environments/environment.hmr.ts @@ -2,5 +2,5 @@ export const environment = { production: false, hmr: true, apiUrl: '', - embedUrl: 'http://localhost:9000' + originServerUrl: 'http://localhost:9000' } diff --git a/client/src/environments/environment.prod.ts b/client/src/environments/environment.prod.ts index 2e9b9fefe..e1b736c61 100644 --- a/client/src/environments/environment.prod.ts +++ b/client/src/environments/environment.prod.ts @@ -2,5 +2,5 @@ export const environment = { production: true, hmr: false, apiUrl: '', - embedUrl: '' + originServerUrl: '' } diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts index e00523976..5d7011265 100644 --- a/client/src/environments/environment.ts +++ b/client/src/environments/environment.ts @@ -12,5 +12,5 @@ export const environment = { production: true, hmr: false, apiUrl: '', - embedUrl: '' + originServerUrl: '' } diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss index 4d70110fe..e6491b492 100644 --- a/client/src/sass/include/_mixins.scss +++ b/client/src/sass/include/_mixins.scss @@ -225,7 +225,7 @@ line-height: $button-height; border-radius: 3px; text-align: center; - padding: 0 13px 0 13px; + padding: 0 17px 0 13px; cursor: pointer; } diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 6e9f7e60c..5c95069fc 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts @@ -18,7 +18,6 @@ import { cacheRoute } from '../middlewares/cache' import { VideoModel } from '../models/video/video' import { VideoCommentModel } from '../models/video/video-comment' import { VideoFilter } from '../../shared/models/videos/video-query.type' -import { logger } from '../helpers/logger' const feedsRouter = express.Router() @@ -47,10 +46,24 @@ feedsRouter.get('/feeds/videos.:format', })(ROUTE_CACHE_LIFETIME.FEEDS)), commonVideosFiltersValidator, asyncMiddleware(videoFeedsValidator), - asyncMiddleware(videoSubscriptonFeedsValidator), asyncMiddleware(generateVideoFeed) ) +feedsRouter.get('/feeds/subscriptions.:format', + videosSortValidator, + setDefaultVideosSort, + feedsFormatValidator, + setFeedFormatContentType, + asyncMiddleware(cacheRoute({ + headerBlacklist: [ + 'Content-Type' + ] + })(ROUTE_CACHE_LIFETIME.FEEDS)), + commonVideosFiltersValidator, + asyncMiddleware(videoSubscriptonFeedsValidator), + asyncMiddleware(generateVideoFeedForSubscriptions) +) + // --------------------------------------------------------------------------- export { @@ -61,7 +74,6 @@ export { async function generateVideoCommentsFeed (req: express.Request, res: express.Response) { const start = 0 - const video = res.locals.videoAll const account = res.locals.account const videoChannel = res.locals.videoChannel @@ -125,10 +137,8 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res async function generateVideoFeed (req: express.Request, res: express.Response) { const start = 0 - const account = res.locals.account const videoChannel = res.locals.videoChannel - const token = req.query.token const nsfw = buildNSFWFilter(res, req.query.nsfw) let name: string @@ -152,21 +162,10 @@ async function generateVideoFeed (req: express.Request, res: express.Response) { queryString: new URL(WEBSERVER.URL + req.url).search }) - /** - * We have two ways to query video results: - * - one with account and token -> get subscription videos - * - one with either account, channel, or nothing: just videos with these filters - */ - const options = token && token !== '' && res.locals.user - ? { - followerActorId: res.locals.user.Account.Actor.id, - user: res.locals.user, - includeLocalVideos: false - } - : { - accountId: account ? account.id : null, - videoChannelId: videoChannel ? videoChannel.id : null - } + const options = { + accountId: account ? account.id : null, + videoChannelId: videoChannel ? videoChannel.id : null + } const resultList = await VideoModel.listForApi({ start, @@ -179,10 +178,86 @@ async function generateVideoFeed (req: express.Request, res: express.Response) { ...options }) + addVideosToFeed(feed, resultList.data) + + // Now the feed generation is done, let's send it! + return sendFeed(feed, req, res) +} + +async function generateVideoFeedForSubscriptions (req: express.Request, res: express.Response) { + const start = 0 + const account = res.locals.account + const nsfw = buildNSFWFilter(res, req.query.nsfw) + const name = account.getDisplayName() + const description = account.description + + const feed = initFeed({ + name, + description, + resourceType: 'videos', + queryString: new URL(WEBSERVER.URL + req.url).search + }) + + const options = { + followerActorId: res.locals.user.Account.Actor.id, + user: res.locals.user + } + + const resultList = await VideoModel.listForApi({ + start, + count: FEEDS.COUNT, + sort: req.query.sort, + includeLocalVideos: true, + nsfw, + filter: req.query.filter as VideoFilter, + withFiles: true, + ...options + }) + + addVideosToFeed(feed, resultList.data) + + // Now the feed generation is done, let's send it! + return sendFeed(feed, req, res) +} + +function initFeed (parameters: { + name: string + description: string + resourceType?: 'videos' | 'video-comments' + queryString?: string +}) { + const webserverUrl = WEBSERVER.URL + const { name, description, resourceType, queryString } = parameters + + return new Feed({ + title: name, + description, + // updated: TODO: somehowGetLatestUpdate, // optional, default = today + id: webserverUrl, + link: webserverUrl, + image: webserverUrl + '/client/assets/images/icons/icon-96x96.png', + favicon: webserverUrl + '/client/assets/images/favicon.png', + copyright: `All rights reserved, unless otherwise specified in the terms specified at ${webserverUrl}/about` + + ` and potential licenses granted by each content's rightholder.`, + generator: `Toraifōsu`, // ^.~ + feedLinks: { + json: `${webserverUrl}/feeds/${resourceType}.json${queryString}`, + atom: `${webserverUrl}/feeds/${resourceType}.atom${queryString}`, + rss: `${webserverUrl}/feeds/${resourceType}.xml${queryString}` + }, + author: { + name: 'Instance admin of ' + CONFIG.INSTANCE.NAME, + email: CONFIG.ADMIN.EMAIL, + link: `${webserverUrl}/about` + } + }) +} + +function addVideosToFeed (feed, videos: VideoModel[]) { /** * Adding video items to the feed object, one at a time */ - resultList.data.forEach(video => { + for (const video of videos) { const formattedVideoFiles = video.getFormattedVideoFilesJSON() const torrents = formattedVideoFiles.map(videoFile => ({ @@ -252,43 +327,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response) { } ] }) - }) - - // Now the feed generation is done, let's send it! - return sendFeed(feed, req, res) -} - -function initFeed (parameters: { - name: string - description: string - resourceType?: 'videos' | 'video-comments' - queryString?: string -}) { - const webserverUrl = WEBSERVER.URL - const { name, description, resourceType, queryString } = parameters - - return new Feed({ - title: name, - description, - // updated: TODO: somehowGetLatestUpdate, // optional, default = today - id: webserverUrl, - link: webserverUrl, - image: webserverUrl + '/client/assets/images/icons/icon-96x96.png', - favicon: webserverUrl + '/client/assets/images/favicon.png', - copyright: `All rights reserved, unless otherwise specified in the terms specified at ${webserverUrl}/about` + - ` and potential licenses granted by each content's rightholder.`, - generator: `Toraifōsu`, // ^.~ - feedLinks: { - json: `${webserverUrl}/feeds/${resourceType}.json${queryString}`, - atom: `${webserverUrl}/feeds/${resourceType}.atom${queryString}`, - rss: `${webserverUrl}/feeds/${resourceType}.xml${queryString}` - }, - author: { - name: 'Instance admin of ' + CONFIG.INSTANCE.NAME, - email: CONFIG.ADMIN.EMAIL, - link: `${webserverUrl}/about` - } - }) + } } function sendFeed (feed, req: express.Request, res: express.Response) { diff --git a/server/helpers/middlewares/accounts.ts b/server/helpers/middlewares/accounts.ts index 9be80167c..fa4a51e6c 100644 --- a/server/helpers/middlewares/accounts.ts +++ b/server/helpers/middlewares/accounts.ts @@ -28,8 +28,7 @@ async function doesAccountExist (p: Bluebird, res: Response, se if (!account) { if (sendNotFound === true) { res.status(404) - .send({ error: 'Account not found' }) - .end() + .json({ error: 'Account not found' }) } return false @@ -41,12 +40,11 @@ async function doesAccountExist (p: Bluebird, res: Response, se } async function doesUserFeedTokenCorrespond (id: number | string, token: string, res: Response) { - const user = await UserModel.loadById(parseInt(id + '', 10)) + const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10)) if (token !== user.feedToken) { res.status(401) - .send({ error: 'User and token mismatch' }) - .end() + .json({ error: 'User and token mismatch' }) return false } diff --git a/server/middlewares/validators/feeds.ts b/server/middlewares/validators/feeds.ts index 5c76a679f..35080ffca 100644 --- a/server/middlewares/validators/feeds.ts +++ b/server/middlewares/validators/feeds.ts @@ -64,8 +64,8 @@ const videoFeedsValidator = [ ] const videoSubscriptonFeedsValidator = [ - query('accountId').optional().custom(isIdValid), - query('token').optional(), + query('accountId').custom(isIdValid), + query('token'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking feeds parameters', { parameters: req.query }) @@ -74,6 +74,7 @@ const videoSubscriptonFeedsValidator = [ // a token alone is erroneous if (req.query.token && !req.query.accountId) return + if (req.query.accountId && !await doesAccountIdExist(req.query.accountId, res)) return if (req.query.token && !await doesUserFeedTokenCorrespond(res.locals.account.userId, req.query.token, res)) return return next() diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 2cd9b2d0a..175ea9102 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts @@ -326,7 +326,7 @@ describe('Test syndication feeds', () => { const res = await listUserSubscriptionVideos(servers[0].url, feeduserAccessToken) expect(res.body.total).to.equal(0) - const json = await getJSONfeed(servers[0].url, 'videos', { accountId: feeduserAccountId, token: feeduserFeedToken }) + const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: feeduserAccountId, token: feeduserFeedToken }) const jsonObj = JSON.parse(json.text) expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos } @@ -337,7 +337,7 @@ describe('Test syndication feeds', () => { const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken) expect(res.body.total).to.equal(0) - const json = await getJSONfeed(servers[0].url, 'videos', { accountId: userAccountId, token: userFeedToken }) + const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken }) const jsonObj = JSON.parse(json.text) expect(jsonObj.items.length).to.be.equal(0) // no subscription, it should not list the instance's videos but list 0 videos } @@ -354,7 +354,7 @@ describe('Test syndication feeds', () => { expect(res.body.total).to.equal(1) expect(res.body.data[0].name).to.equal('user video') - const json = await getJSONfeed(servers[0].url, 'videos', { accountId: userAccountId, token: userFeedToken, version: 1 }) + const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 1 }) const jsonObj = JSON.parse(json.text) expect(jsonObj.items.length).to.be.equal(1) // subscribed to self, it should not list the instance's videos but list john's } @@ -370,7 +370,7 @@ describe('Test syndication feeds', () => { const res = await listUserSubscriptionVideos(servers[0].url, userAccessToken) expect(res.body.total).to.equal(2, "there should be 2 videos part of the subscription") - const json = await getJSONfeed(servers[0].url, 'videos', { accountId: userAccountId, token: userFeedToken, version: 2 }) + const json = await getJSONfeed(servers[0].url, 'subscriptions', { accountId: userAccountId, token: userFeedToken, version: 2 }) const jsonObj = JSON.parse(json.text) expect(jsonObj.items.length).to.be.equal(2) // subscribed to root, it should not list the instance's videos but list root/john's } diff --git a/shared/extra-utils/feeds/feeds.ts b/shared/extra-utils/feeds/feeds.ts index af6df2b20..bafbb9f94 100644 --- a/shared/extra-utils/feeds/feeds.ts +++ b/shared/extra-utils/feeds/feeds.ts @@ -1,6 +1,6 @@ import * as request from 'supertest' -type FeedType = 'videos' | 'video-comments' +type FeedType = 'videos' | 'video-comments' | 'subscriptions' function getXMLfeed (url: string, feed: FeedType, format?: string) { const path = '/feeds/' + feed + '.xml' -- 2.41.0