]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
refactor scoped token service
authorRigel Kent <sendmemail@rigelk.eu>
Mon, 9 Nov 2020 15:25:27 +0000 (16:25 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Wed, 25 Nov 2020 10:07:56 +0000 (11:07 +0100)
26 files changed:
client/src/app/+about/about-instance/about-instance.component.ts
client/src/app/+admin/moderation/video-block-list/video-block-list.component.ts
client/src/app/+my-account/my-account-applications/my-account-applications.component.html
client/src/app/+my-account/my-account-applications/my-account-applications.component.ts
client/src/app/+my-account/my-account.component.ts
client/src/app/+my-account/my-account.module.ts
client/src/app/+videos/video-list/video-user-subscriptions.component.ts
client/src/app/core/auth/auth.service.ts
client/src/app/core/core.module.ts
client/src/app/core/index.ts
client/src/app/core/scoped-tokens/index.ts [new file with mode: 0644]
client/src/app/core/scoped-tokens/scoped-tokens.service.ts [new file with mode: 0644]
client/src/app/helpers/utils.ts
client/src/app/shared/shared-abuse-list/abuse-list-table.component.ts
client/src/app/shared/shared-forms/input-readonly-copy.component.ts
client/src/app/shared/shared-main/video/video.service.ts
client/src/environments/environment.e2e.ts
client/src/environments/environment.hmr.ts
client/src/environments/environment.prod.ts
client/src/environments/environment.ts
client/src/sass/include/_mixins.scss
server/controllers/feeds.ts
server/helpers/middlewares/accounts.ts
server/middlewares/validators/feeds.ts
server/tests/feeds/feeds.ts
shared/extra-utils/feeds/feeds.ts

index e74b5daebd43fa8e717d13c62de8d0353f1c8f0b..92ecd5263455e9066a0ac48be0dafaa783d6942e 100644 (file)
@@ -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'
index 1d0e56bfddf55725ef9f8f74e2cc2b873e1d2daa..aa6b5d0a9c185e360681db870052a0ff5daf8695 100644 (file)
@@ -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
       })
index 62e2cb59b9860fdbd15739102d52b1ac1fde4b41..53a9c91ac4199b5031108cba184d43e4d1276ff5 100644 (file)
@@ -7,8 +7,8 @@
   <div class="form-group col-12 col-lg-4 col-xl-3">
     <h2 i18n class="applications-title">SUBSCRIPTION FEED</h2>
     <div i18n class="applications-description">
-      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.
     </div>
   </div>
 
index c3f09dfe38d8da89c5a5b45e038ff202cfe374c9..233e42c838c5b6b3367d7b5fe292e238b944503b 100644 (file)
@@ -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
   }
 }
index 12966aebb77ff91d580e9cde5d2ec9ea2f30befd..eaf8a72e92d0fe2b19b8d14b495055a9d90f28ff 100644 (file)
@@ -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
     ]
   }
index 70bf58aaef04f902fe2fd2a79aa31403664b68ee..076864563e4e0a4d8b894307bd5c46dd42e42f31 100644 (file)
@@ -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
   ],
 
index 10031d6cc26d7c3e6f36fbaf4ca760a3136ef46b..03881c295745d9aab8675656e300c18aa672dc21 100644 (file)
@@ -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',
index 224f35f82008711b846a23d5b7c5a58a0648b32f..fd6062d3fbdb864aecbc0398f12c5803235f65a6 100644 (file)
@@ -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<AuthStatus>
   private user: AuthUser = null
   private refreshingTokenObservable: Observable<any>
-  private scopedTokens: ScopedToken
 
   constructor (
     private http: HttpClient,
@@ -247,48 +244,6 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular
         )
   }
 
-  getScopedTokens (): Promise<ScopedToken> {
-    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<ScopedToken>(AuthService.BASE_SCOPED_TOKENS_URL, { headers })
-              .subscribe(
-                scopedTokens => {
-                  this.scopedTokens = scopedTokens
-                  res(this.scopedTokens)
-                },
-
-                err => {
-                  console.error(err)
-                  rej(err)
-                }
-              )
-    })
-  }
-
-  renewScopedTokens (): Promise<ScopedToken> {
-    return new Promise((res, rej) => {
-      const authHeaderValue = this.getRequestHeaderValue()
-      const headers = new HttpHeaders().set('Authorization', authHeaderValue)
-
-      this.http.post<ScopedToken>(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<UserLoginWithUserInformation> {
     // User is not loaded yet, set manually auth header
     const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`)
index 6c0a2245d2263f4a528093423de0554182cf47d6..f51f1920df2a493561e60ecd5363fe750b306d9e 100644 (file)
@@ -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,
index a0c34543d10c188f2c20d0a4bb8c12ca5a3c8f61..9245ff6fc2c2c0cc4fc9fe7037ac4649761db957 100644 (file)
@@ -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 (file)
index 0000000..c9a48ff
--- /dev/null
@@ -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 (file)
index 0000000..8e3697c
--- /dev/null
@@ -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<ScopedToken>(ScopedTokensService.BASE_SCOPED_TOKENS_URL)
+            .pipe(
+              catchError(res => this.restExtractor.handleError(res))
+            )
+  }
+
+  renewScopedTokens () {
+    return this.authHttp
+            .post<ScopedToken>(ScopedTokensService.BASE_SCOPED_TOKENS_URL, {})
+            .pipe(
+              catchError(res => this.restExtractor.handleError(res))
+            )
+  }
+}
index a22507f46a3e914c4711cf99d79681607e18561b..9c805b4cadf7b89641184f5e4cc488a44b332284 100644 (file)
@@ -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
index ca0d236993b6ee69ebd33ba49d06c37eb2a8259e..807665b9c4a4da633afc8c790b7d28d18e0ce294 100644 (file)
@@ -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,
index 520827a53291db4155179f20e27aa8e7ed81d8fa..b04d69d057e33ed64dcd3291ed14e1f9d7a0c3a5 100644 (file)
@@ -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',
index b81540e8ddeabd1cb1356fadcbc2fd90941b0bd1..70be5d7d29e06ae2b9ead2ee6864d11a5a6c59a8 100644 (file)
@@ -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) {
index b33ff9f8633cecfc7f0c4901e8e516ed8a51c394..a1a58e36ff9f944bc82c1674c8971255cb0fb2df 100644 (file)
@@ -2,5 +2,5 @@ export const environment = {
   production: false,
   hmr: false,
   apiUrl: 'http://localhost:9001',
-  embedUrl: 'http://localhost:9001'
+  originServerUrl: 'http://localhost:9001'
 }
index 3b6eff302c0ce0070225c32dd02f6680bceb4942..ab763192068ffb68c4b30fd6824de32319339866 100644 (file)
@@ -2,5 +2,5 @@ export const environment = {
   production: false,
   hmr: true,
   apiUrl: '',
-  embedUrl: 'http://localhost:9000'
+  originServerUrl: 'http://localhost:9000'
 }
index 2e9b9fefebc6caed54d7cf9f3ed191fd70892242..e1b736c6183c192607640a640cfa29d58a75b257 100644 (file)
@@ -2,5 +2,5 @@ export const environment = {
   production: true,
   hmr: false,
   apiUrl: '',
-  embedUrl: ''
+  originServerUrl: ''
 }
index e005239766216aaac675b64b67b4beecd61409f6..5d7011265bfd1a7cb686b92abdd73f74042e7678 100644 (file)
@@ -12,5 +12,5 @@ export const environment = {
   production: true,
   hmr: false,
   apiUrl: '',
-  embedUrl: ''
+  originServerUrl: ''
 }
index 4d70110fef8e63d1d03c8197823055c2db73fa3b..e6491b4929460456496c89242375f7ef91bd25e7 100644 (file)
   line-height: $button-height;
   border-radius: 3px;
   text-align: center;
-  padding: 0 13px 0 13px;
+  padding: 0 17px 0 13px;
   cursor: pointer;
 }
 
index 6e9f7e60cfe9bd3c7b0f4aa18ce8434fdf8883f4..5c95069fc2cc8fc035941ee2c79ab553af4c0c48 100644 (file)
@@ -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) {
index 9be80167c9f0c65cf4481d67331a2e0bc3aad4fb..fa4a51e6cfbf1b531f91cd5d1fafd07acd919aa0 100644 (file)
@@ -28,8 +28,7 @@ async function doesAccountExist (p: Bluebird<MAccountDefault>, 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<MAccountDefault>, 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
   }
index 5c76a679f69dd8be15fd2bc78e0116dbf637ba39..35080ffcab2c2d759276e50a6296f882fdd5d8d0 100644 (file)
@@ -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()
index 2cd9b2d0a8544e918758ad9684cc4ed47f9685b2..175ea91028550f8a2b085beb49ceaefe5f10632c 100644 (file)
@@ -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
       }
index af6df2b208fe02b88df90b2ed2151fff6dc70dd3..bafbb9f943e4013ec99b02a2afae145fb5992508 100644 (file)
@@ -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'