Add account view
authorChocobozzz <me@florianbigard.com>
Tue, 24 Apr 2018 13:10:54 +0000 (15:10 +0200)
committerChocobozzz <me@florianbigard.com>
Tue, 24 Apr 2018 13:13:19 +0000 (15:13 +0200)
33 files changed:
client/src/app/+account/account-about/account-about.component.html [new file with mode: 0644]
client/src/app/+account/account-about/account-about.component.scss [new file with mode: 0644]
client/src/app/+account/account-about/account-about.component.ts [new file with mode: 0644]
client/src/app/+account/account-routing.module.ts [new file with mode: 0644]
client/src/app/+account/account-videos/account-videos.component.scss [new file with mode: 0644]
client/src/app/+account/account-videos/account-videos.component.ts [new file with mode: 0644]
client/src/app/+account/account.component.html [new file with mode: 0644]
client/src/app/+account/account.component.scss [new file with mode: 0644]
client/src/app/+account/account.component.ts [new file with mode: 0644]
client/src/app/+account/account.module.ts [new file with mode: 0644]
client/src/app/+account/index.ts [new file with mode: 0644]
client/src/app/app-routing.module.ts
client/src/app/my-account/my-account-settings/my-account-settings.component.scss
client/src/app/my-account/my-account.component.ts
client/src/app/shared/account/account.model.ts
client/src/app/shared/account/account.service.ts [new file with mode: 0644]
client/src/app/shared/shared.module.ts
client/src/app/shared/video/abstract-video-list.html
client/src/app/shared/video/abstract-video-list.ts
client/src/app/shared/video/video.service.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.ts
client/src/sass/include/_mixins.scss
server/controllers/api/accounts.ts
server/controllers/api/index.ts
server/controllers/api/users.ts
server/controllers/api/videos/index.ts
server/controllers/feeds.ts
server/helpers/express-utils.ts [new file with mode: 0644]
server/helpers/utils.ts
server/middlewares/servers.ts
server/middlewares/validators/webfinger.ts
server/models/video/video.ts

diff --git a/client/src/app/+account/account-about/account-about.component.html b/client/src/app/+account/account-about/account-about.component.html
new file mode 100644 (file)
index 0000000..003a804
--- /dev/null
@@ -0,0 +1,12 @@
+<div *ngIf="account" class="row">
+  <div class="description col-md-6 col-sm-12">
+    <div class="small-title">Description</div>
+    <div class="content">{{ getAccountDescription() }}</div>
+  </div>
+
+  <div class="stats col-md-6 col-sm-12">
+    <div class="small-title">Stats</div>
+
+    <div class="content">Joined {{ account.createdAt | date }}</div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/client/src/app/+account/account-about/account-about.component.scss b/client/src/app/+account/account-about/account-about.component.scss
new file mode 100644 (file)
index 0000000..b1be7d4
--- /dev/null
@@ -0,0 +1,8 @@
+@import '_variables';
+@import '_mixins';
+
+.small-title {
+  @include in-content-small-title;
+
+  margin-bottom: 20px;
+}
diff --git a/client/src/app/+account/account-about/account-about.component.ts b/client/src/app/+account/account-about/account-about.component.ts
new file mode 100644 (file)
index 0000000..0772b84
--- /dev/null
@@ -0,0 +1,39 @@
+import { Component, OnDestroy, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { Location } from '@angular/common'
+import { getParameterByName, immutableAssign } from '@app/shared/misc/utils'
+import { NotificationsService } from 'angular2-notifications'
+import 'rxjs/add/observable/from'
+import 'rxjs/add/operator/concatAll'
+import { AuthService } from '../../core/auth'
+import { ConfirmService } from '../../core/confirm'
+import { AbstractVideoList } from '../../shared/video/abstract-video-list'
+import { VideoService } from '../../shared/video/video.service'
+import { Account } from '@app/shared/account/account.model'
+import { AccountService } from '@app/shared/account/account.service'
+
+@Component({
+  selector: 'my-account-about',
+  templateUrl: './account-about.component.html',
+  styleUrls: [ './account-about.component.scss' ]
+})
+export class AccountAboutComponent implements OnInit {
+  private account: Account
+
+  constructor (
+    protected route: ActivatedRoute,
+    private accountService: AccountService
+  ) { }
+
+  ngOnInit () {
+    // Parent get the account for us
+    this.accountService.accountLoaded
+      .subscribe(account => this.account = account)
+  }
+
+  getAccountDescription () {
+    if (this.account.description) return this.account.description
+
+    return 'No description'
+  }
+}
diff --git a/client/src/app/+account/account-routing.module.ts b/client/src/app/+account/account-routing.module.ts
new file mode 100644 (file)
index 0000000..5341021
--- /dev/null
@@ -0,0 +1,45 @@
+import { NgModule } from '@angular/core'
+import { RouterModule, Routes } from '@angular/router'
+import { MetaGuard } from '@ngx-meta/core'
+import { AccountComponent } from './account.component'
+import { AccountVideosComponent } from './account-videos/account-videos.component'
+import { AccountAboutComponent } from '@app/+account/account-about/account-about.component'
+
+const accountRoutes: Routes = [
+  {
+    path: ':accountId',
+    component: AccountComponent,
+    canActivateChild: [ MetaGuard ],
+    children: [
+      {
+        path: '',
+        redirectTo: 'videos',
+        pathMatch: 'full'
+      },
+      {
+        path: 'videos',
+        component: AccountVideosComponent,
+        data: {
+          meta: {
+            title: 'Account videos'
+          }
+        }
+      },
+      {
+        path: 'about',
+        component: AccountAboutComponent,
+        data: {
+          meta: {
+            title: 'About account'
+          }
+        }
+      }
+    ]
+  }
+]
+
+@NgModule({
+  imports: [ RouterModule.forChild(accountRoutes) ],
+  exports: [ RouterModule ]
+})
+export class AccountRoutingModule {}
diff --git a/client/src/app/+account/account-videos/account-videos.component.scss b/client/src/app/+account/account-videos/account-videos.component.scss
new file mode 100644 (file)
index 0000000..2ba85c0
--- /dev/null
@@ -0,0 +1,3 @@
+.title-page-single {
+  margin-top: 0;
+}
\ No newline at end of file
diff --git a/client/src/app/+account/account-videos/account-videos.component.ts b/client/src/app/+account/account-videos/account-videos.component.ts
new file mode 100644 (file)
index 0000000..6c0f0bb
--- /dev/null
@@ -0,0 +1,71 @@
+import { Component, OnDestroy, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { Location } from '@angular/common'
+import { immutableAssign } from '@app/shared/misc/utils'
+import { NotificationsService } from 'angular2-notifications'
+import 'rxjs/add/observable/from'
+import 'rxjs/add/operator/concatAll'
+import { AuthService } from '../../core/auth'
+import { ConfirmService } from '../../core/confirm'
+import { AbstractVideoList } from '../../shared/video/abstract-video-list'
+import { VideoService } from '../../shared/video/video.service'
+import { Account } from '@app/shared/account/account.model'
+import { AccountService } from '@app/shared/account/account.service'
+
+@Component({
+  selector: 'my-account-videos',
+  templateUrl: '../../shared/video/abstract-video-list.html',
+  styleUrls: [
+    '../../shared/video/abstract-video-list.scss',
+    './account-videos.component.scss'
+  ]
+})
+export class AccountVideosComponent extends AbstractVideoList implements OnInit, OnDestroy {
+  titlePage = 'Published videos'
+  marginContent = false // Disable margin
+  currentRoute = '/account/videos'
+  loadOnInit = false
+
+  private account: Account
+
+  constructor (
+    protected router: Router,
+    protected route: ActivatedRoute,
+    protected authService: AuthService,
+    protected notificationsService: NotificationsService,
+    protected confirmService: ConfirmService,
+    protected location: Location,
+    private accountService: AccountService,
+    private videoService: VideoService
+  ) {
+    super()
+  }
+
+  ngOnInit () {
+    super.ngOnInit()
+
+    // Parent get the account for us
+    this.accountService.accountLoaded
+      .subscribe(account => {
+        this.account = account
+        this.currentRoute = '/account/' + this.account.id + '/videos'
+
+        this.loadMoreVideos(this.pagination.currentPage)
+        this.generateSyndicationList()
+      })
+  }
+
+  ngOnDestroy () {
+    super.ngOnDestroy()
+  }
+
+  getVideosObservable (page: number) {
+    const newPagination = immutableAssign(this.pagination, { currentPage: page })
+
+    return this.videoService.getAccountVideos(this.account, newPagination, this.sort)
+  }
+
+  generateSyndicationList () {
+    this.syndicationItems = this.videoService.getAccountFeedUrls(this.account.id)
+  }
+}
diff --git a/client/src/app/+account/account.component.html b/client/src/app/+account/account.component.html
new file mode 100644 (file)
index 0000000..f875b37
--- /dev/null
@@ -0,0 +1,23 @@
+<div *ngIf="account" class="row">
+  <div class="sub-menu">
+
+    <div class="account">
+      <img [src]="getAvatarUrl()" alt="Avatar" />
+
+      <div class="account-info">
+        <div class="account-display-name">{{ account.displayName }}</div>
+        <div class="account-followers">{{ account.followersCount }} subscribers</div>
+      </div>
+    </div>
+
+    <div class="links">
+      <a routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
+
+      <a routerLink="about" routerLinkActive="active" class="title-page">About</a>
+    </div>
+  </div>
+
+  <div class="margin-content">
+    <router-outlet></router-outlet>
+  </div>
+</div>
diff --git a/client/src/app/+account/account.component.scss b/client/src/app/+account/account.component.scss
new file mode 100644 (file)
index 0000000..c7b8f03
--- /dev/null
@@ -0,0 +1,46 @@
+@import '_variables';
+@import '_mixins';
+
+.sub-menu {
+  height: 160px;
+  display: flex;
+  flex-direction: column;
+  align-items: start;
+
+  .account {
+    display: flex;
+    margin-top: 20px;
+    margin-bottom: 20px;
+
+    img {
+      @include avatar(80px);
+
+      margin-right: 20px;
+    }
+
+    .account-info {
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+
+      .account-display-name {
+        font-size: 23px;
+        font-weight: $font-bold;
+      }
+
+      .account-followers {
+        font-size: 15px;
+      }
+    }
+  }
+
+  .links {
+    margin-top: 0;
+    margin-bottom: 10px;
+
+    a {
+      margin-top: 0;
+      margin-bottom: 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/client/src/app/+account/account.component.ts b/client/src/app/+account/account.component.ts
new file mode 100644 (file)
index 0000000..1c3e528
--- /dev/null
@@ -0,0 +1,29 @@
+import { Component, OnInit } from '@angular/core'
+import { ActivatedRoute } from '@angular/router'
+import { AccountService } from '@app/shared/account/account.service'
+import { Account } from '@app/shared/account/account.model'
+
+@Component({
+  selector: 'my-account',
+  templateUrl: './account.component.html',
+  styleUrls: [ './account.component.scss' ]
+})
+export class AccountComponent implements OnInit {
+  private account: Account
+
+  constructor (
+    private route: ActivatedRoute,
+    private accountService: AccountService
+  ) {}
+
+  ngOnInit () {
+    const accountId = parseInt(this.route.snapshot.params['accountId'], 10)
+
+    this.accountService.getAccount(accountId)
+        .subscribe(account => this.account = account)
+  }
+
+  getAvatarUrl () {
+    return Account.GET_ACCOUNT_AVATAR_URL(this.account)
+  }
+}
diff --git a/client/src/app/+account/account.module.ts b/client/src/app/+account/account.module.ts
new file mode 100644 (file)
index 0000000..2fe67f3
--- /dev/null
@@ -0,0 +1,26 @@
+import { NgModule } from '@angular/core'
+import { SharedModule } from '../shared'
+import { AccountRoutingModule } from './account-routing.module'
+import { AccountComponent } from './account.component'
+import { AccountVideosComponent } from './account-videos/account-videos.component'
+import { AccountAboutComponent } from './account-about/account-about.component'
+
+@NgModule({
+  imports: [
+    AccountRoutingModule,
+    SharedModule
+  ],
+
+  declarations: [
+    AccountComponent,
+    AccountVideosComponent,
+    AccountAboutComponent
+  ],
+
+  exports: [
+    AccountComponent
+  ],
+
+  providers: []
+})
+export class AccountModule { }
diff --git a/client/src/app/+account/index.ts b/client/src/app/+account/index.ts
new file mode 100644 (file)
index 0000000..dc56ffd
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './account-routing.module'
+export * from './account.component'
+export * from './account.module'
index 2ee3cf97436b00e45f3f489a07f27472001d3f56..1d55b4cea2db272dac5ec2b3737a138487c39cd8 100644 (file)
@@ -7,6 +7,10 @@ const routes: Routes = [
   {
     path: 'admin',
     loadChildren: './+admin/admin.module#AdminModule'
+  },
+  {
+    path: 'account',
+    loadChildren: './+account/account.module#AccountModule'
   }
 ]
 
index 1cc00ca498d7ad6e59521068482dd598f79f56b4..85079d6208be8eec50315061daf52db816f27f11 100644 (file)
 }
 
 .account-title {
-  text-transform: uppercase;
-  color: $orange-color;
-  font-weight: $font-bold;
-  font-size: 13px;
+  @include in-content-small-title;
+
   margin-top: 55px;
   margin-bottom: 30px;
 }
index 0955e2b7b2a5bf3a15b1bc77435df601fbded30e..7bb461d3c9908dd918578e08942a8e520e4e17ff 100644 (file)
@@ -1,7 +1,7 @@
 import { Component } from '@angular/core'
 
 @Component({
-  selector: 'my-account',
+  selector: 'my-my-account',
   templateUrl: './my-account.component.html'
 })
 export class MyAccountComponent {}
index 0bdc76478264fc3743fd249df37e048f2b38e162..3d5176bdd691288957072c8b4aaf25540db19101 100644 (file)
@@ -16,6 +16,21 @@ export class Account implements ServerAccount {
   updatedAt: Date
   avatar: Avatar
 
+  constructor (hash: ServerAccount) {
+    this.id = hash.id
+    this.uuid = hash.uuid
+    this.url = hash.url
+    this.name = hash.name
+    this.displayName = hash.displayName
+    this.description = hash.description
+    this.host = hash.host
+    this.followingCount = hash.followingCount
+    this.followersCount = hash.followersCount
+    this.createdAt = new Date(hash.createdAt.toString())
+    this.updatedAt = new Date(hash.updatedAt.toString())
+    this.avatar = hash.avatar
+  }
+
   static GET_ACCOUNT_AVATAR_URL (account: Account) {
     const absoluteAPIUrl = getAbsoluteAPIUrl()
 
diff --git a/client/src/app/shared/account/account.service.ts b/client/src/app/shared/account/account.service.ts
new file mode 100644 (file)
index 0000000..8c66ae0
--- /dev/null
@@ -0,0 +1,31 @@
+import { Injectable } from '@angular/core'
+import 'rxjs/add/operator/catch'
+import 'rxjs/add/operator/map'
+import { environment } from '../../../environments/environment'
+import { Observable } from 'rxjs/Observable'
+import { Account } from '@app/shared/account/account.model'
+import { RestExtractor } from '@app/shared/rest/rest-extractor.service'
+import { RestService } from '@app/shared/rest/rest.service'
+import { HttpClient } from '@angular/common/http'
+import { Account as ServerAccount } from '../../../../../shared/models/actors/account.model'
+import { ReplaySubject } from 'rxjs/ReplaySubject'
+
+@Injectable()
+export class AccountService {
+  static BASE_ACCOUNT_URL = environment.apiUrl + '/api/v1/accounts/'
+
+  accountLoaded = new ReplaySubject<Account>(1)
+
+  constructor (
+    private authHttp: HttpClient,
+    private restExtractor: RestExtractor,
+    private restService: RestService
+  ) {}
+
+  getAccount (id: number): Observable<Account> {
+    return this.authHttp.get<ServerAccount>(AccountService.BASE_ACCOUNT_URL + id)
+               .map(accountHash => new Account(accountHash))
+               .do(account => this.accountLoaded.next(account))
+               .catch((res) => this.restExtractor.handleError(res))
+  }
+}
index 74730e2aa5f95f248c28752d4d90209c2028559b..2178eebc815c201cfc6c64b6369783ece3e75bfc 100644 (file)
@@ -31,6 +31,7 @@ import { VideoMiniatureComponent } from './video/video-miniature.component'
 import { VideoFeedComponent } from './video/video-feed.component'
 import { VideoThumbnailComponent } from './video/video-thumbnail.component'
 import { VideoService } from './video/video.service'
+import { AccountService } from '@app/shared/account/account.service'
 
 @NgModule({
   imports: [
@@ -104,6 +105,7 @@ import { VideoService } from './video/video.service'
     VideoBlacklistService,
     UserService,
     VideoService,
+    AccountService,
     MarkdownService
   ]
 })
index cb04e07b4de291522d231b2c7124f1ed4de874ee..690529dcf5a371ff14ca9ef90bc42b8d2bc565b6 100644 (file)
@@ -1,5 +1,5 @@
-<div class="margin-content">
-  <div class="title-page title-page-single">
+<div [ngClass]="{ 'margin-content': marginContent }">
+  <div *ngIf="titlePage" class="title-page title-page-single">
     {{ titlePage }}
   </div>
   <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed>
index 728c864e9fb719519b36ba3d8de9bd50d0411660..642a85f656ef6eb2a5f1345b86c24fbc39932966 100644 (file)
@@ -29,6 +29,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
   syndicationItems = []
 
   loadOnInit = true
+  marginContent = true
   pageHeight: number
   videoWidth: number
   videoHeight: number
index ef8babd55a9ac9fc0ada06666967b4e76529cc59..f82aa738961db263e79049802e69444e19ef13ed 100644 (file)
@@ -21,6 +21,8 @@ import { VideoDetails } from './video-details.model'
 import { VideoEdit } from './video-edit.model'
 import { Video } from './video.model'
 import { objectToFormData } from '@app/shared/misc/utils'
+import { Account } from '@app/shared/account/account.model'
+import { AccountService } from '@app/shared/account/account.service'
 
 @Injectable()
 export class VideoService {
@@ -97,6 +99,22 @@ export class VideoService {
       .catch((res) => this.restExtractor.handleError(res))
   }
 
+  getAccountVideos (
+    account: Account,
+    videoPagination: ComponentPagination,
+    sort: VideoSortField
+  ): Observable<{ videos: Video[], totalVideos: number}> {
+    const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
+
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
+
+    return this.authHttp
+               .get(AccountService.BASE_ACCOUNT_URL + account.id + '/videos', { params })
+               .map(this.extractVideos)
+               .catch((res) => this.restExtractor.handleError(res))
+  }
+
   getVideos (
     videoPagination: ComponentPagination,
     sort: VideoSortField,
index 91e590094d68d8327a6c69caa57daf69dbde9aba..abda5043e79136cb996b5a69beffc68988b40ddd 100644 (file)
           </div>
 
           <div class="video-info-by">
-            <a [routerLink]="[ '/videos', 'search' ]" [queryParams]="{ search: video.account.name }" title="Search videos of this account">
+            <a [routerLink]="[ '/account', video.account.id ]" title="Go the account page">
               By {{ video.by }}
               <img [src]="getAvatarPath()" alt="Account avatar" />
             </a>
-
-            <my-video-feed [syndicationItems]="syndicationItems"></my-video-feed>
           </div>
         </div>
 
index 6f6f02378ca577b2ebfb48d2fe80533de72051b1..4b0c495832833a3e7089aabdf4043b7c7c55b82b 100644 (file)
@@ -39,8 +39,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
 
   otherVideosDisplayed: Video[] = []
 
-  syndicationItems = {}
-
   player: videojs.Player
   playerElement: HTMLVideoElement
   userRating: UserVideoRateType = null
@@ -110,7 +108,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
           const startTime = this.route.snapshot.queryParams.start
           this.onVideoFetched(video, startTime)
             .catch(err => this.handleError(err))
-          this.generateSyndicationList()
         },
 
         error => {
@@ -247,10 +244,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     return this.video.tags.join(', ')
   }
 
-  generateSyndicationList () {
-    this.syndicationItems = this.videoService.getAccountFeedUrls(this.video.account.id)
-  }
-
   isVideoRemovable () {
     return this.video.isRemovableBy(this.authService.getUser())
   }
index 7e7a38bbdae86d19900c7f373c71d1a177a4d4b6..cbf9b566a337cf3334a9020885e3e510bd21d5b5 100644 (file)
   left: 0.25em;
   transform: rotate(-135deg);
 }
+
+@mixin in-content-small-title {
+  text-transform: uppercase;
+  color: $orange-color;
+  font-weight: $font-bold;
+  font-size: 13px;
+}
\ No newline at end of file
index 4dc0cc16d5723ba628f48d0fc2bd6b342d765fe6..06ab040339eb3f63dd528ff9370f906ff3915030 100644 (file)
@@ -1,8 +1,11 @@
 import * as express from 'express'
 import { getFormattedObjects } from '../../helpers/utils'
-import { asyncMiddleware, paginationValidator, setDefaultSort, setDefaultPagination } from '../../middlewares'
-import { accountsGetValidator, accountsSortValidator } from '../../middlewares/validators'
+import { asyncMiddleware, optionalAuthenticate, paginationValidator, setDefaultPagination, setDefaultSort } from '../../middlewares'
+import { accountsGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators'
 import { AccountModel } from '../../models/account/account'
+import { VideoModel } from '../../models/video/video'
+import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type'
+import { isNSFWHidden } from '../../helpers/express-utils'
 
 const accountsRouter = express.Router()
 
@@ -19,6 +22,16 @@ accountsRouter.get('/:id',
   getAccount
 )
 
+accountsRouter.get('/:id/videos',
+  asyncMiddleware(accountsGetValidator),
+  paginationValidator,
+  videosSortValidator,
+  setDefaultSort,
+  setDefaultPagination,
+  optionalAuthenticate,
+  asyncMiddleware(getAccountVideos)
+)
+
 // ---------------------------------------------------------------------------
 
 export {
@@ -28,7 +41,9 @@ export {
 // ---------------------------------------------------------------------------
 
 function getAccount (req: express.Request, res: express.Response, next: express.NextFunction) {
-  return res.json(res.locals.account.toFormattedJSON())
+  const account: AccountModel = res.locals.account
+
+  return res.json(account.toFormattedJSON())
 }
 
 async function listAccounts (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -36,3 +51,19 @@ async function listAccounts (req: express.Request, res: express.Response, next:
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
+
+async function getAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const account: AccountModel = res.locals.account
+
+  const resultList = await VideoModel.listForApi(
+    req.query.start as number,
+    req.query.count as number,
+    req.query.sort as VideoSortField,
+    isNSFWHidden(res),
+    null,
+    false,
+    account.id
+  )
+
+  return res.json(getFormattedObjects(resultList.data, resultList.total))
+}
index 3b499f3b7f3180412c105cbe092b4b5b51e79499..964d5d04c3c893b13b9855111f53be26220acfb6 100644 (file)
@@ -1,5 +1,4 @@
 import * as express from 'express'
-import { badRequest } from '../../helpers/utils'
 import { configRouter } from './config'
 import { jobsRouter } from './jobs'
 import { oauthClientsRouter } from './oauth-clients'
@@ -7,6 +6,7 @@ import { serverRouter } from './server'
 import { usersRouter } from './users'
 import { accountsRouter } from './accounts'
 import { videosRouter } from './videos'
+import { badRequest } from '../../helpers/express-utils'
 
 const apiRouter = express.Router()
 
index 6540adb1c1a797f9199a9309bdbc2258bf5c58bd..474329b5853d00623e49808868a164a3448a888f 100644 (file)
@@ -7,7 +7,7 @@ import { UserCreate, UserRight, UserRole, UserUpdate, UserUpdateMe, UserVideoRat
 import { retryTransactionWrapper } from '../../helpers/database-utils'
 import { processImage } from '../../helpers/image-utils'
 import { logger } from '../../helpers/logger'
-import { createReqFiles, getFormattedObjects } from '../../helpers/utils'
+import { getFormattedObjects } from '../../helpers/utils'
 import { AVATARS_SIZE, CONFIG, IMAGE_MIMETYPE_EXT, RATES_LIMIT, sequelizeTypescript } from '../../initializers'
 import { updateActorAvatarInstance } from '../../lib/activitypub'
 import { sendUpdateActor } from '../../lib/activitypub/send'
@@ -43,6 +43,7 @@ import { UserModel } from '../../models/account/user'
 import { OAuthTokenModel } from '../../models/oauth/oauth-token'
 import { VideoModel } from '../../models/video/video'
 import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type'
+import { createReqFiles } from '../../helpers/express-utils'
 
 const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
 const loginRateLimiter = new RateLimit({
index 6e8601fa1c65cdab1c8e3b81961f91c31c6a718d..61b6c58260f526d74763c60dda5f67d478388f9c 100644 (file)
@@ -6,7 +6,7 @@ import { retryTransactionWrapper } from '../../../helpers/database-utils'
 import { getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
 import { processImage } from '../../../helpers/image-utils'
 import { logger } from '../../../helpers/logger'
-import { createReqFiles, getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
+import { getFormattedObjects, getServerActor, resetSequelizeInstance } from '../../../helpers/utils'
 import {
   CONFIG,
   IMAGE_MIMETYPE_EXT,
@@ -19,11 +19,7 @@ import {
   VIDEO_MIMETYPE_EXT,
   VIDEO_PRIVACIES
 } from '../../../initializers'
-import {
-  fetchRemoteVideoDescription,
-  getVideoActivityPubUrl,
-  shareVideoByServerAndChannel
-} from '../../../lib/activitypub'
+import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub'
 import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send'
 import { JobQueue } from '../../../lib/job-queue'
 import { Redis } from '../../../lib/redis'
@@ -49,9 +45,9 @@ import { blacklistRouter } from './blacklist'
 import { videoChannelRouter } from './channel'
 import { videoCommentRouter } from './comment'
 import { rateVideoRouter } from './rate'
-import { User } from '../../../../shared/models/users'
 import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
 import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
+import { isNSFWHidden, createReqFiles } from '../../../helpers/express-utils'
 
 const videosRouter = express.Router()
 
@@ -444,12 +440,3 @@ async function searchVideos (req: express.Request, res: express.Response, next:
 
   return res.json(getFormattedObjects(resultList.data, resultList.total))
 }
-
-function isNSFWHidden (res: express.Response) {
-  if (res.locals.oauth) {
-    const user: User = res.locals.oauth.token.User
-    if (user) return user.nsfwPolicy === 'do_not_list'
-  }
-
-  return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
-}
index 4a4dc3820a81b2ec480c03de67ff0f9fc751dc43..6a6af3e0910f9d50d4ebb17a29e97fb475098445 100644 (file)
@@ -30,29 +30,18 @@ async function generateFeed (req: express.Request, res: express.Response, next:
   let feed = initFeed()
   const start = 0
 
-  let resultList: ResultList<VideoModel>
   const account: AccountModel = res.locals.account
   const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
 
-  if (account) {
-    resultList = await VideoModel.listAccountVideosForApi(
-      account.id,
-      start,
-      FEEDS.COUNT,
-      req.query.sort as VideoSortField,
-      hideNSFW,
-      true
-    )
-  } else {
-    resultList = await VideoModel.listForApi(
-      start,
-      FEEDS.COUNT,
-      req.query.sort as VideoSortField,
-      hideNSFW,
-      req.query.filter,
-      true
-    )
-  }
+  const resultList = await VideoModel.listForApi(
+    start,
+    FEEDS.COUNT,
+    req.query.sort as VideoSortField,
+    hideNSFW,
+    req.query.filter,
+    true,
+    account ? account.id : null
+  )
 
   // Adding video items to the feed, one at a time
   resultList.data.forEach(video => {
diff --git a/server/helpers/express-utils.ts b/server/helpers/express-utils.ts
new file mode 100644 (file)
index 0000000..d023117
--- /dev/null
@@ -0,0 +1,77 @@
+import * as express from 'express'
+import * as multer from 'multer'
+import { CONFIG, REMOTE_SCHEME } from '../initializers'
+import { logger } from './logger'
+import { User } from '../../shared/models/users'
+import { generateRandomString } from './utils'
+
+function isNSFWHidden (res: express.Response) {
+  if (res.locals.oauth) {
+    const user: User = res.locals.oauth.token.User
+    if (user) return user.nsfwPolicy === 'do_not_list'
+  }
+
+  return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
+}
+
+function getHostWithPort (host: string) {
+  const splitted = host.split(':')
+
+  // The port was not specified
+  if (splitted.length === 1) {
+    if (REMOTE_SCHEME.HTTP === 'https') return host + ':443'
+
+    return host + ':80'
+  }
+
+  return host
+}
+
+function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) {
+  return res.type('json').status(400).end()
+}
+
+function createReqFiles (
+  fieldNames: string[],
+  mimeTypes: { [ id: string ]: string },
+  destinations: { [ fieldName: string ]: string }
+) {
+  const storage = multer.diskStorage({
+    destination: (req, file, cb) => {
+      cb(null, destinations[ file.fieldname ])
+    },
+
+    filename: async (req, file, cb) => {
+      const extension = mimeTypes[ file.mimetype ]
+      let randomString = ''
+
+      try {
+        randomString = await generateRandomString(16)
+      } catch (err) {
+        logger.error('Cannot generate random string for file name.', { err })
+        randomString = 'fake-random-string'
+      }
+
+      cb(null, randomString + extension)
+    }
+  })
+
+  const fields = []
+  for (const fieldName of fieldNames) {
+    fields.push({
+      name: fieldName,
+      maxCount: 1
+    })
+  }
+
+  return multer({ storage }).fields(fields)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  isNSFWHidden,
+  getHostWithPort,
+  badRequest,
+  createReqFiles
+}
index c581172195072fb52d7437f32ddcf6ceba869bc3..058c3211efd39b810c8dd73ea0c094aa23af876e 100644 (file)
@@ -1,68 +1,13 @@
-import * as express from 'express'
-import * as multer from 'multer'
 import { Model } from 'sequelize-typescript'
 import { ResultList } from '../../shared'
 import { VideoResolution } from '../../shared/models/videos'
-import { CONFIG, REMOTE_SCHEME } from '../initializers'
+import { CONFIG } from '../initializers'
 import { UserModel } from '../models/account/user'
 import { ActorModel } from '../models/activitypub/actor'
 import { ApplicationModel } from '../models/application/application'
 import { pseudoRandomBytesPromise } from './core-utils'
 import { logger } from './logger'
 
-function getHostWithPort (host: string) {
-  const splitted = host.split(':')
-
-  // The port was not specified
-  if (splitted.length === 1) {
-    if (REMOTE_SCHEME.HTTP === 'https') return host + ':443'
-
-    return host + ':80'
-  }
-
-  return host
-}
-
-function badRequest (req: express.Request, res: express.Response, next: express.NextFunction) {
-  return res.type('json').status(400).end()
-}
-
-function createReqFiles (
-  fieldNames: string[],
-  mimeTypes: { [ id: string ]: string },
-  destinations: { [ fieldName: string ]: string }
-) {
-  const storage = multer.diskStorage({
-    destination: (req, file, cb) => {
-      cb(null, destinations[file.fieldname])
-    },
-
-    filename: async (req, file, cb) => {
-      const extension = mimeTypes[file.mimetype]
-      let randomString = ''
-
-      try {
-        randomString = await generateRandomString(16)
-      } catch (err) {
-        logger.error('Cannot generate random string for file name.', { err })
-        randomString = 'fake-random-string'
-      }
-
-      cb(null, randomString + extension)
-    }
-  })
-
-  const fields = []
-  for (const fieldName of fieldNames) {
-    fields.push({
-      name: fieldName,
-      maxCount: 1
-    })
-  }
-
-  return multer({ storage }).fields(fields)
-}
-
 async function generateRandomString (size: number) {
   const raw = await pseudoRandomBytesPromise(size)
 
@@ -151,14 +96,11 @@ type SortType = { sortModel: any, sortValue: string }
 // ---------------------------------------------------------------------------
 
 export {
-  badRequest,
   generateRandomString,
   getFormattedObjects,
   isSignupAllowed,
   computeResolutionsToTranscode,
   resetSequelizeInstance,
   getServerActor,
-  SortType,
-  getHostWithPort,
-  createReqFiles
+  SortType
 }
index a9dcad2d48acebea556ba2155aaedaefff4d6036..c52f4685b94785b94f10e4e65aca0f5ff3fcfc22 100644 (file)
@@ -1,6 +1,6 @@
 import * as express from 'express'
 import 'express-validator'
-import { getHostWithPort } from '../helpers/utils'
+import { getHostWithPort } from '../helpers/express-utils'
 
 function setBodyHostsPort (req: express.Request, res: express.Response, next: express.NextFunction) {
   if (!req.body.hosts) return next()
index 3dbec6e4481d2af07f0428e1c9120e3913e48fd4..3b9645048c4130ac2d52d4782f6a5e58b703c62f 100644 (file)
@@ -2,9 +2,9 @@ import * as express from 'express'
 import { query } from 'express-validator/check'
 import { isWebfingerResourceValid } from '../../helpers/custom-validators/webfinger'
 import { logger } from '../../helpers/logger'
-import { getHostWithPort } from '../../helpers/utils'
 import { ActorModel } from '../../models/activitypub/actor'
 import { areValidationErrors } from './utils'
+import { getHostWithPort } from '../../helpers/express-utils'
 
 const webfingerValidator = [
   query('resource').custom(isWebfingerResourceValid).withMessage('Should have a valid webfinger resource'),
index b0fff65268fd8542153fb16649ab6f915cfd42a7..2ad9c00dd482fa6f9cc9a41550bb68e26a6ae472 100644 (file)
@@ -95,7 +95,33 @@ enum ScopeNames {
 }
 
 @Scopes({
-  [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, hideNSFW: boolean, filter?: VideoFilter, withFiles?: boolean) => {
+  [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, hideNSFW: boolean, filter?: VideoFilter, withFiles?: boolean, accountId?: number) => {
+    const accountInclude = {
+      attributes: [ 'name' ],
+      model: AccountModel.unscoped(),
+      required: true,
+      where: {},
+      include: [
+        {
+          attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ],
+          model: ActorModel.unscoped(),
+          required: true,
+          where: VideoModel.buildActorWhereWithFilter(filter),
+          include: [
+            {
+              attributes: [ 'host' ],
+              model: ServerModel.unscoped(),
+              required: false
+            },
+            {
+              model: AvatarModel.unscoped(),
+              required: false
+            }
+          ]
+        }
+      ]
+    }
+
     const query: IFindOptions<VideoModel> = {
       where: {
         id: {
@@ -125,30 +151,7 @@ enum ScopeNames {
           model: VideoChannelModel.unscoped(),
           required: true,
           include: [
-            {
-              attributes: [ 'name' ],
-              model: AccountModel.unscoped(),
-              required: true,
-              include: [
-                {
-                  attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ],
-                  model: ActorModel.unscoped(),
-                  required: true,
-                  where: VideoModel.buildActorWhereWithFilter(filter),
-                  include: [
-                    {
-                      attributes: [ 'host' ],
-                      model: ServerModel.unscoped(),
-                      required: false
-                    },
-                    {
-                      model: AvatarModel.unscoped(),
-                      required: false
-                    }
-                  ]
-                }
-              ]
-            }
+            accountInclude
           ]
         }
       ]
@@ -166,6 +169,12 @@ enum ScopeNames {
       query.where['nsfw'] = false
     }
 
+    if (accountId) {
+      accountInclude.where = {
+        id: accountId
+      }
+    }
+
     return query
   },
   [ScopeNames.WITH_ACCOUNT_DETAILS]: {
@@ -688,7 +697,15 @@ export class VideoModel extends Model<VideoModel> {
     })
   }
 
-  static async listForApi (start: number, count: number, sort: string, hideNSFW: boolean, filter?: VideoFilter, withFiles = false) {
+  static async listForApi (
+    start: number,
+    count: number,
+    sort: string,
+    hideNSFW: boolean,
+    filter?: VideoFilter,
+    withFiles = false,
+    accountId?: number
+  ) {
     const query = {
       offset: start,
       limit: count,
@@ -696,7 +713,7 @@ export class VideoModel extends Model<VideoModel> {
     }
 
     const serverActor = await getServerActor()
-    return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles ] })
+    return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles, accountId ] })
       .findAndCountAll(query)
       .then(({ rows, count }) => {
         return {
@@ -879,8 +896,6 @@ export class VideoModel extends Model<VideoModel> {
 
   private static getLanguageLabel (id: string) {
     let languageLabel = VIDEO_LANGUAGES[id]
-    console.log(VIDEO_LANGUAGES)
-    console.log(id)
     if (!languageLabel) languageLabel = 'Unknown'
 
     return languageLabel