diff options
146 files changed, 2282 insertions, 1408 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 65e1acec6..678b0674b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml | |||
@@ -48,6 +48,7 @@ jobs: | |||
48 | ENABLE_OBJECT_STORAGE_TESTS: true | 48 | ENABLE_OBJECT_STORAGE_TESTS: true |
49 | OBJECT_STORAGE_SCALEWAY_KEY_ID: ${{ secrets.OBJECT_STORAGE_SCALEWAY_KEY_ID }} | 49 | OBJECT_STORAGE_SCALEWAY_KEY_ID: ${{ secrets.OBJECT_STORAGE_SCALEWAY_KEY_ID }} |
50 | OBJECT_STORAGE_SCALEWAY_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }} | 50 | OBJECT_STORAGE_SCALEWAY_ACCESS_KEY: ${{ secrets.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY }} |
51 | YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
51 | 52 | ||
52 | steps: | 53 | steps: |
53 | - uses: actions/checkout@v3 | 54 | - uses: actions/checkout@v3 |
diff --git a/client/package.json b/client/package.json index 115a4a199..6f88d4fb9 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -52,8 +52,8 @@ | |||
52 | "@ngx-loading-bar/core": "^6.0.0", | 52 | "@ngx-loading-bar/core": "^6.0.0", |
53 | "@ngx-loading-bar/http-client": "^6.0.0", | 53 | "@ngx-loading-bar/http-client": "^6.0.0", |
54 | "@ngx-loading-bar/router": "^6.0.0", | 54 | "@ngx-loading-bar/router": "^6.0.0", |
55 | "@peertube/p2p-media-loader-core": "^1.0.13", | 55 | "@peertube/p2p-media-loader-core": "^1.0.14", |
56 | "@peertube/p2p-media-loader-hlsjs": "^1.0.13", | 56 | "@peertube/p2p-media-loader-hlsjs": "^1.0.14", |
57 | "@peertube/videojs-contextmenu": "^5.5.0", | 57 | "@peertube/videojs-contextmenu": "^5.5.0", |
58 | "@peertube/xliffmerge": "^2.0.3", | 58 | "@peertube/xliffmerge": "^2.0.3", |
59 | "@popperjs/core": "^2.11.5", | 59 | "@popperjs/core": "^2.11.5", |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html index 43f1438e0..174f5d29c 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-basic-configuration.component.html | |||
@@ -44,9 +44,13 @@ | |||
44 | 44 | ||
45 | <div class="peertube-select-container"> | 45 | <div class="peertube-select-container"> |
46 | <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> | 46 | <select id="trendingVideosAlgorithmsDefault" formControlName="default" class="form-control"> |
47 | <option i18n value="publishedAt">Recently added videos</option> | ||
48 | <option i18n value="originallyPublishedAt">Original publication date</option> | ||
49 | <option i18n value="name">Name</option> | ||
47 | <option i18n value="hot">Hot videos</option> | 50 | <option i18n value="hot">Hot videos</option> |
48 | <option i18n value="most-viewed">Most viewed videos</option> | 51 | <option i18n value="most-viewed">Recent views</option> |
49 | <option i18n value="most-liked">Most liked videos</option> | 52 | <option i18n value="most-liked">Most liked videos</option> |
53 | <option i18n value="views">Global views</option> | ||
50 | </select> | 54 | </select> |
51 | </div> | 55 | </div> |
52 | 56 | ||
diff --git a/client/src/app/+login/login.component.ts b/client/src/app/+login/login.component.ts index c1705807f..5f6aa842e 100644 --- a/client/src/app/+login/login.component.ts +++ b/client/src/app/+login/login.component.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { environment } from 'src/environments/environment' | ||
1 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' | 2 | import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 3 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' | 4 | import { AuthService, Notifier, RedirectService, SessionStorageService, UserService } from '@app/core' |
@@ -7,7 +8,7 @@ import { USER_OTP_TOKEN_VALIDATOR } from '@app/shared/form-validators/user-valid | |||
7 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' | 8 | import { FormReactive, FormReactiveService, InputTextComponent } from '@app/shared/shared-forms' |
8 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' | 9 | import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' |
9 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' | 10 | import { NgbAccordion, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap' |
10 | import { PluginsManager } from '@root-helpers/plugins-manager' | 11 | import { getExternalAuthHref } from '@shared/core-utils' |
11 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' | 12 | import { RegisteredExternalAuthConfig, ServerConfig } from '@shared/models' |
12 | 13 | ||
13 | @Component({ | 14 | @Component({ |
@@ -119,7 +120,7 @@ export class LoginComponent extends FormReactive implements OnInit, AfterViewIni | |||
119 | } | 120 | } |
120 | 121 | ||
121 | getAuthHref (auth: RegisteredExternalAuthConfig) { | 122 | getAuthHref (auth: RegisteredExternalAuthConfig) { |
122 | return PluginsManager.getExternalAuthHref(auth) | 123 | return getExternalAuthHref(environment.apiUrl, auth) |
123 | } | 124 | } |
124 | 125 | ||
125 | login () { | 126 | login () { |
diff --git a/client/src/app/+videos/+video-watch/video-watch.component.ts b/client/src/app/+videos/+video-watch/video-watch.component.ts index 94853423b..84548de97 100644 --- a/client/src/app/+videos/+video-watch/video-watch.component.ts +++ b/client/src/app/+videos/+video-watch/video-watch.component.ts | |||
@@ -133,8 +133,6 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
133 | this.loadRouteParams() | 133 | this.loadRouteParams() |
134 | this.loadRouteQuery() | 134 | this.loadRouteQuery() |
135 | 135 | ||
136 | this.initHotkeys() | ||
137 | |||
138 | this.theaterEnabled = getStoredTheater() | 136 | this.theaterEnabled = getStoredTheater() |
139 | 137 | ||
140 | this.hooks.runAction('action:video-watch.init', 'video-watch') | 138 | this.hooks.runAction('action:video-watch.init', 'video-watch') |
@@ -295,6 +293,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
295 | subtitle: queryParams.subtitle, | 293 | subtitle: queryParams.subtitle, |
296 | 294 | ||
297 | playerMode: queryParams.mode, | 295 | playerMode: queryParams.mode, |
296 | playbackRate: queryParams.playbackRate, | ||
298 | peertubeLink: false | 297 | peertubeLink: false |
299 | } | 298 | } |
300 | 299 | ||
@@ -406,6 +405,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
406 | if (res === false) return this.location.back() | 405 | if (res === false) return this.location.back() |
407 | } | 406 | } |
408 | 407 | ||
408 | this.buildHotkeysHelp(video) | ||
409 | |||
409 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) | 410 | this.buildPlayer({ urlOptions, loggedInOrAnonymousUser, forceAutoplay }) |
410 | .catch(err => logger.error('Cannot build the player', err)) | 411 | .catch(err => logger.error('Cannot build the player', err)) |
411 | 412 | ||
@@ -657,6 +658,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
657 | muted: urlOptions.muted, | 658 | muted: urlOptions.muted, |
658 | loop: urlOptions.loop, | 659 | loop: urlOptions.loop, |
659 | subtitle: urlOptions.subtitle, | 660 | subtitle: urlOptions.subtitle, |
661 | playbackRate: urlOptions.playbackRate, | ||
660 | 662 | ||
661 | peertubeLink: urlOptions.peertubeLink, | 663 | peertubeLink: urlOptions.peertubeLink, |
662 | 664 | ||
@@ -785,33 +787,43 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
785 | this.video.viewers = newViewers | 787 | this.video.viewers = newViewers |
786 | } | 788 | } |
787 | 789 | ||
788 | private initHotkeys () { | 790 | private buildHotkeysHelp (video: Video) { |
791 | if (this.hotkeys.length !== 0) { | ||
792 | this.hotkeysService.remove(this.hotkeys) | ||
793 | } | ||
794 | |||
789 | this.hotkeys = [ | 795 | this.hotkeys = [ |
790 | // These hotkeys are managed by the player | 796 | // These hotkeys are managed by the player |
791 | new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), | 797 | new Hotkey('f', e => e, undefined, $localize`Enter/exit fullscreen`), |
792 | new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), | 798 | new Hotkey('space', e => e, undefined, $localize`Play/Pause the video`), |
793 | new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), | 799 | new Hotkey('m', e => e, undefined, $localize`Mute/unmute the video`), |
794 | 800 | ||
795 | new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`), | ||
796 | |||
797 | new Hotkey('up', e => e, undefined, $localize`Increase the volume`), | 801 | new Hotkey('up', e => e, undefined, $localize`Increase the volume`), |
798 | new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), | 802 | new Hotkey('down', e => e, undefined, $localize`Decrease the volume`), |
799 | 803 | ||
800 | new Hotkey('right', e => e, undefined, $localize`Seek the video forward`), | ||
801 | new Hotkey('left', e => e, undefined, $localize`Seek the video backward`), | ||
802 | |||
803 | new Hotkey('>', e => e, undefined, $localize`Increase playback rate`), | ||
804 | new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`), | ||
805 | |||
806 | new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`), | ||
807 | new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`), | ||
808 | |||
809 | new Hotkey('t', e => { | 804 | new Hotkey('t', e => { |
810 | this.theaterEnabled = !this.theaterEnabled | 805 | this.theaterEnabled = !this.theaterEnabled |
811 | return false | 806 | return false |
812 | }, undefined, $localize`Toggle theater mode`) | 807 | }, undefined, $localize`Toggle theater mode`) |
813 | ] | 808 | ] |
814 | 809 | ||
810 | if (!video.isLive) { | ||
811 | this.hotkeys = this.hotkeys.concat([ | ||
812 | // These hotkeys are also managed by the player but only for VOD | ||
813 | |||
814 | new Hotkey('0-9', e => e, undefined, $localize`Skip to a percentage of the video: 0 is 0% and 9 is 90%`), | ||
815 | |||
816 | new Hotkey('right', e => e, undefined, $localize`Seek the video forward`), | ||
817 | new Hotkey('left', e => e, undefined, $localize`Seek the video backward`), | ||
818 | |||
819 | new Hotkey('>', e => e, undefined, $localize`Increase playback rate`), | ||
820 | new Hotkey('<', e => e, undefined, $localize`Decrease playback rate`), | ||
821 | |||
822 | new Hotkey(',', e => e, undefined, $localize`Navigate in the video to the previous frame`), | ||
823 | new Hotkey('.', e => e, undefined, $localize`Navigate in the video to the next frame`) | ||
824 | ]) | ||
825 | } | ||
826 | |||
815 | if (this.isUserLoggedIn()) { | 827 | if (this.isUserLoggedIn()) { |
816 | this.hotkeys = this.hotkeys.concat([ | 828 | this.hotkeys = this.hotkeys.concat([ |
817 | new Hotkey('shift+s', () => { | 829 | new Hotkey('shift+s', () => { |
diff --git a/client/src/app/+videos/video-list/videos-list-common-page.component.ts b/client/src/app/+videos/video-list/videos-list-common-page.component.ts index c8fa8ef30..bafe30fd7 100644 --- a/client/src/app/+videos/video-list/videos-list-common-page.component.ts +++ b/client/src/app/+videos/video-list/videos-list-common-page.component.ts | |||
@@ -177,6 +177,9 @@ export class VideosListCommonPageComponent implements OnInit, OnDestroy, Disable | |||
177 | case 'best': | 177 | case 'best': |
178 | return '-hot' | 178 | return '-hot' |
179 | 179 | ||
180 | case 'name': | ||
181 | return 'name' | ||
182 | |||
180 | default: | 183 | default: |
181 | return '-' + algorithm as VideoSortField | 184 | return '-' + algorithm as VideoSortField |
182 | } | 185 | } |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 4de28e51e..ed7eabb76 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -5,10 +5,11 @@ import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '@angular | |||
5 | import { Injectable } from '@angular/core' | 5 | import { Injectable } from '@angular/core' |
6 | import { Router } from '@angular/router' | 6 | import { Router } from '@angular/router' |
7 | import { Notifier } from '@app/core/notification/notifier.service' | 7 | import { Notifier } from '@app/core/notification/notifier.service' |
8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage } from '@root-helpers/index' | 8 | import { logger, OAuthUserTokens, objectToUrlEncoded, peertubeLocalStorage, PluginsManager } from '@root-helpers/index' |
9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' | 9 | import { HttpStatusCode, MyUser as UserServerModel, OAuthClientLocal, User, UserLogin, UserRefreshToken } from '@shared/models' |
10 | import { environment } from '../../../environments/environment' | 10 | import { environment } from '../../../environments/environment' |
11 | import { RestExtractor } from '../rest/rest-extractor.service' | 11 | import { RestExtractor } from '../rest/rest-extractor.service' |
12 | import { ServerService } from '../server' | ||
12 | import { AuthStatus } from './auth-status.model' | 13 | import { AuthStatus } from './auth-status.model' |
13 | import { AuthUser } from './auth-user.model' | 14 | import { AuthUser } from './auth-user.model' |
14 | 15 | ||
@@ -44,6 +45,7 @@ export class AuthService { | |||
44 | private refreshingTokenObservable: Observable<any> | 45 | private refreshingTokenObservable: Observable<any> |
45 | 46 | ||
46 | constructor ( | 47 | constructor ( |
48 | private serverService: ServerService, | ||
47 | private http: HttpClient, | 49 | private http: HttpClient, |
48 | private notifier: Notifier, | 50 | private notifier: Notifier, |
49 | private hotkeysService: HotkeysService, | 51 | private hotkeysService: HotkeysService, |
@@ -213,25 +215,28 @@ Ensure you have correctly configured PeerTube (config/ directory), in particular | |||
213 | const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') | 215 | const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded') |
214 | 216 | ||
215 | this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) | 217 | this.refreshingTokenObservable = this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers }) |
216 | .pipe( | 218 | .pipe( |
217 | map(res => this.handleRefreshToken(res)), | 219 | map(res => this.handleRefreshToken(res)), |
218 | tap(() => { | 220 | tap(() => { |
219 | this.refreshingTokenObservable = null | 221 | this.refreshingTokenObservable = null |
220 | }), | 222 | }), |
221 | catchError(err => { | 223 | catchError(err => { |
222 | this.refreshingTokenObservable = null | 224 | this.refreshingTokenObservable = null |
223 | 225 | ||
224 | logger.error(err) | 226 | logger.error(err) |
225 | logger.info('Cannot refresh token -> logout...') | 227 | logger.info('Cannot refresh token -> logout...') |
226 | this.logout() | 228 | this.logout() |
227 | this.router.navigate([ '/login' ]) | 229 | |
228 | 230 | const externalLoginUrl = PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverService.getHTMLConfig()) | |
229 | return observableThrowError(() => ({ | 231 | if (externalLoginUrl) window.location.href = externalLoginUrl |
230 | error: $localize`You need to reconnect.` | 232 | else this.router.navigate([ '/login' ]) |
231 | })) | 233 | |
232 | }), | 234 | return observableThrowError(() => ({ |
233 | share() | 235 | error: $localize`You need to reconnect.` |
234 | ) | 236 | })) |
237 | }), | ||
238 | share() | ||
239 | ) | ||
235 | 240 | ||
236 | return this.refreshingTokenObservable | 241 | return this.refreshingTokenObservable |
237 | } | 242 | } |
diff --git a/client/src/app/core/rest/rest-extractor.service.ts b/client/src/app/core/rest/rest-extractor.service.ts index de3f2bfff..daed7f178 100644 --- a/client/src/app/core/rest/rest-extractor.service.ts +++ b/client/src/app/core/rest/rest-extractor.service.ts | |||
@@ -87,7 +87,11 @@ export class RestExtractor { | |||
87 | 87 | ||
88 | if (err.status !== undefined) { | 88 | if (err.status !== undefined) { |
89 | const errorMessage = this.buildServerErrorMessage(err) | 89 | const errorMessage = this.buildServerErrorMessage(err) |
90 | logger.error(`Backend returned code ${err.status}, errorMessage is: ${errorMessage}`) | 90 | |
91 | const message = `Backend returned code ${err.status}, errorMessage is: ${errorMessage}` | ||
92 | |||
93 | if (err.status === HttpStatusCode.NOT_FOUND_404) logger.clientError(message) | ||
94 | else logger.error(message) | ||
91 | 95 | ||
92 | return errorMessage | 96 | return errorMessage |
93 | } | 97 | } |
diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 63f01df92..568cb98bb 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { HotkeysService } from 'angular2-hotkeys' | 1 | import { HotkeysService } from 'angular2-hotkeys' |
2 | import * as debug from 'debug' | 2 | import * as debug from 'debug' |
3 | import { switchMap } from 'rxjs/operators' | 3 | import { switchMap } from 'rxjs/operators' |
4 | import { environment } from 'src/environments/environment' | ||
4 | import { ViewportScroller } from '@angular/common' | 5 | import { ViewportScroller } from '@angular/common' |
5 | import { Component, OnInit, ViewChild } from '@angular/core' | 6 | import { Component, OnInit, ViewChild } from '@angular/core' |
6 | import { Router } from '@angular/router' | 7 | import { Router } from '@angular/router' |
@@ -131,12 +132,7 @@ export class MenuComponent implements OnInit { | |||
131 | } | 132 | } |
132 | 133 | ||
133 | getExternalLoginHref () { | 134 | getExternalLoginHref () { |
134 | if (!this.serverConfig || this.serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined | 135 | return PluginsManager.getDefaultLoginHref(environment.apiUrl, this.serverConfig) |
135 | |||
136 | const externalAuths = this.serverConfig.plugin.registeredExternalAuths | ||
137 | if (externalAuths.length !== 1) return undefined | ||
138 | |||
139 | return PluginsManager.getExternalAuthHref(externalAuths[0]) | ||
140 | } | 136 | } |
141 | 137 | ||
142 | isRegistrationAllowed () { | 138 | isRegistrationAllowed () { |
diff --git a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts index 85c63c173..706227e66 100644 --- a/client/src/app/shared/shared-video-miniature/video-miniature.component.ts +++ b/client/src/app/shared/shared-video-miniature/video-miniature.component.ts | |||
@@ -314,6 +314,6 @@ export class VideoMiniatureComponent implements OnInit { | |||
314 | this.cd.markForCheck() | 314 | this.cd.markForCheck() |
315 | }) | 315 | }) |
316 | 316 | ||
317 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 317 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
318 | } | 318 | } |
319 | } | 319 | } |
diff --git a/client/src/app/shared/shared-video-miniature/videos-list.component.ts b/client/src/app/shared/shared-video-miniature/videos-list.component.ts index d5cdd958e..a423377de 100644 --- a/client/src/app/shared/shared-video-miniature/videos-list.component.ts +++ b/client/src/app/shared/shared-video-miniature/videos-list.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as debug from 'debug' | 1 | import * as debug from 'debug' |
2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' | 2 | import { fromEvent, Observable, Subject, Subscription } from 'rxjs' |
3 | import { debounceTime, switchMap } from 'rxjs/operators' | 3 | import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators' |
4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' | 4 | import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { | 6 | import { |
@@ -111,6 +111,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
111 | 111 | ||
112 | private lastQueryLength: number | 112 | private lastQueryLength: number |
113 | 113 | ||
114 | private videoRequests = new Subject<{ reset: boolean, obs: Observable<ResultList<Video>> }>() | ||
115 | |||
114 | constructor ( | 116 | constructor ( |
115 | private notifier: Notifier, | 117 | private notifier: Notifier, |
116 | private authService: AuthService, | 118 | private authService: AuthService, |
@@ -124,6 +126,8 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
124 | } | 126 | } |
125 | 127 | ||
126 | ngOnInit () { | 128 | ngOnInit () { |
129 | this.subscribeToVideoRequests() | ||
130 | |||
127 | const hiddenFilters = this.hideScopeFilter | 131 | const hiddenFilters = this.hideScopeFilter |
128 | ? [ 'scope' ] | 132 | ? [ 'scope' ] |
129 | : [] | 133 | : [] |
@@ -228,30 +232,12 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
228 | } | 232 | } |
229 | 233 | ||
230 | loadMoreVideos (reset = false) { | 234 | loadMoreVideos (reset = false) { |
231 | if (reset) this.hasDoneFirstQuery = false | 235 | if (reset) { |
232 | 236 | this.hasDoneFirstQuery = false | |
233 | this.getVideosObservableFunction(this.pagination, this.filters) | 237 | this.videos = [] |
234 | .subscribe({ | 238 | } |
235 | next: ({ data }) => { | ||
236 | this.hasDoneFirstQuery = true | ||
237 | this.lastQueryLength = data.length | ||
238 | |||
239 | if (reset) this.videos = [] | ||
240 | this.videos = this.videos.concat(data) | ||
241 | |||
242 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
243 | |||
244 | this.onDataSubject.next(data) | ||
245 | this.videosLoaded.emit(this.videos) | ||
246 | }, | ||
247 | |||
248 | error: err => { | ||
249 | const message = $localize`Cannot load more videos. Try again later.` | ||
250 | 239 | ||
251 | logger.error(message, err) | 240 | this.videoRequests.next({ reset, obs: this.getVideosObservableFunction(this.pagination, this.filters) }) |
252 | this.notifier.error(message) | ||
253 | } | ||
254 | }) | ||
255 | } | 241 | } |
256 | 242 | ||
257 | reloadVideos () { | 243 | reloadVideos () { |
@@ -423,4 +409,32 @@ export class VideosListComponent implements OnInit, OnChanges, OnDestroy { | |||
423 | this.onFiltersChanged(true) | 409 | this.onFiltersChanged(true) |
424 | }) | 410 | }) |
425 | } | 411 | } |
412 | |||
413 | private subscribeToVideoRequests () { | ||
414 | this.videoRequests | ||
415 | .pipe(concatMap(({ reset, obs }) => obs.pipe(map(({ data }) => ({ data, reset }))))) | ||
416 | .subscribe({ | ||
417 | next: ({ data, reset }) => { | ||
418 | console.log(data[0].name) | ||
419 | |||
420 | this.hasDoneFirstQuery = true | ||
421 | this.lastQueryLength = data.length | ||
422 | |||
423 | if (reset) this.videos = [] | ||
424 | this.videos = this.videos.concat(data) | ||
425 | |||
426 | if (this.groupByDate) this.buildGroupedDateLabels() | ||
427 | |||
428 | this.onDataSubject.next(data) | ||
429 | this.videosLoaded.emit(this.videos) | ||
430 | }, | ||
431 | |||
432 | error: err => { | ||
433 | const message = $localize`Cannot load more videos. Try again later.` | ||
434 | |||
435 | logger.error(message, err) | ||
436 | this.notifier.error(message) | ||
437 | } | ||
438 | }) | ||
439 | } | ||
426 | } | 440 | } |
diff --git a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts index 2fc39fc75..f802416a4 100644 --- a/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts +++ b/client/src/app/shared/shared-video-playlist/video-add-to-playlist.component.ts | |||
@@ -81,7 +81,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
81 | .subscribe(result => { | 81 | .subscribe(result => { |
82 | this.playlistsData = result.data | 82 | this.playlistsData = result.data |
83 | 83 | ||
84 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 84 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
85 | }) | 85 | }) |
86 | 86 | ||
87 | this.videoPlaylistSearchChanged | 87 | this.videoPlaylistSearchChanged |
@@ -129,7 +129,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
129 | .subscribe(playlistsResult => { | 129 | .subscribe(playlistsResult => { |
130 | this.playlistsData = playlistsResult.data | 130 | this.playlistsData = playlistsResult.data |
131 | 131 | ||
132 | this.videoPlaylistService.runPlaylistCheck(this.video.id) | 132 | this.videoPlaylistService.runVideoExistsInPlaylistCheck(this.video.id) |
133 | }) | 133 | }) |
134 | } | 134 | } |
135 | 135 | ||
diff --git a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts index 330a51f91..bc9fb0d74 100644 --- a/client/src/app/shared/shared-video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/shared-video-playlist/video-playlist.service.ts | |||
@@ -206,7 +206,15 @@ export class VideoPlaylistService { | |||
206 | stopTimestamp: body.stopTimestamp | 206 | stopTimestamp: body.stopTimestamp |
207 | }) | 207 | }) |
208 | 208 | ||
209 | this.runPlaylistCheck(body.videoId) | 209 | this.runVideoExistsInPlaylistCheck(body.videoId) |
210 | |||
211 | if (this.myAccountPlaylistCache) { | ||
212 | const playlist = this.myAccountPlaylistCache.data.find(p => p.id === playlistId) | ||
213 | if (!playlist) return | ||
214 | |||
215 | const otherPlaylists = this.myAccountPlaylistCache.data.filter(p => p !== playlist) | ||
216 | this.myAccountPlaylistCache.data = [ playlist, ...otherPlaylists ] | ||
217 | } | ||
210 | }), | 218 | }), |
211 | catchError(err => this.restExtractor.handleError(err)) | 219 | catchError(err => this.restExtractor.handleError(err)) |
212 | ) | 220 | ) |
@@ -225,7 +233,7 @@ export class VideoPlaylistService { | |||
225 | elem.stopTimestamp = body.stopTimestamp | 233 | elem.stopTimestamp = body.stopTimestamp |
226 | } | 234 | } |
227 | 235 | ||
228 | this.runPlaylistCheck(videoId) | 236 | this.runVideoExistsInPlaylistCheck(videoId) |
229 | }), | 237 | }), |
230 | catchError(err => this.restExtractor.handleError(err)) | 238 | catchError(err => this.restExtractor.handleError(err)) |
231 | ) | 239 | ) |
@@ -242,7 +250,7 @@ export class VideoPlaylistService { | |||
242 | .filter(e => e.playlistElementId !== playlistElementId) | 250 | .filter(e => e.playlistElementId !== playlistElementId) |
243 | } | 251 | } |
244 | 252 | ||
245 | this.runPlaylistCheck(videoId) | 253 | this.runVideoExistsInPlaylistCheck(videoId) |
246 | }), | 254 | }), |
247 | catchError(err => this.restExtractor.handleError(err)) | 255 | catchError(err => this.restExtractor.handleError(err)) |
248 | ) | 256 | ) |
@@ -296,7 +304,7 @@ export class VideoPlaylistService { | |||
296 | return obs | 304 | return obs |
297 | } | 305 | } |
298 | 306 | ||
299 | runPlaylistCheck (videoId: number) { | 307 | runVideoExistsInPlaylistCheck (videoId: number) { |
300 | debugLogger('Running playlist check.') | 308 | debugLogger('Running playlist check.') |
301 | 309 | ||
302 | if (this.videoExistsCache[videoId]) { | 310 | if (this.videoExistsCache[videoId]) { |
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 56310c4e9..2781850b9 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -11,6 +11,7 @@ import './shared/control-bar/p2p-info-button' | |||
11 | import './shared/control-bar/peertube-link-button' | 11 | import './shared/control-bar/peertube-link-button' |
12 | import './shared/control-bar/peertube-load-progress-bar' | 12 | import './shared/control-bar/peertube-load-progress-bar' |
13 | import './shared/control-bar/theater-button' | 13 | import './shared/control-bar/theater-button' |
14 | import './shared/control-bar/peertube-live-display' | ||
14 | import './shared/settings/resolution-menu-button' | 15 | import './shared/settings/resolution-menu-button' |
15 | import './shared/settings/resolution-menu-item' | 16 | import './shared/settings/resolution-menu-item' |
16 | import './shared/settings/settings-dialog' | 17 | import './shared/settings/settings-dialog' |
@@ -96,6 +97,10 @@ export class PeertubePlayerManager { | |||
96 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { | 97 | videojs(options.common.playerElement, videojsOptions, function (this: videojs.Player) { |
97 | const player = this | 98 | const player = this |
98 | 99 | ||
100 | if (!isNaN(+options.common.playbackRate)) { | ||
101 | player.playbackRate(+options.common.playbackRate) | ||
102 | } | ||
103 | |||
99 | let alreadyFallback = false | 104 | let alreadyFallback = false |
100 | 105 | ||
101 | const handleError = () => { | 106 | const handleError = () => { |
@@ -118,7 +123,7 @@ export class PeertubePlayerManager { | |||
118 | self.addContextMenu(videojsOptionsBuilder, player, options.common) | 123 | self.addContextMenu(videojsOptionsBuilder, player, options.common) |
119 | 124 | ||
120 | if (isMobile()) player.peertubeMobile() | 125 | if (isMobile()) player.peertubeMobile() |
121 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin() | 126 | if (options.common.enableHotkeys === true) player.peerTubeHotkeysPlugin({ isLive: options.common.isLive }) |
122 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') | 127 | if (options.common.controlBar === false) player.controlBar.addClass('control-bar-hidden') |
123 | 128 | ||
124 | player.bezels() | 129 | player.bezels() |
diff --git a/client/src/assets/player/shared/control-bar/index.ts b/client/src/assets/player/shared/control-bar/index.ts index db5b8938d..e71e90713 100644 --- a/client/src/assets/player/shared/control-bar/index.ts +++ b/client/src/assets/player/shared/control-bar/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './next-previous-video-button' | 1 | export * from './next-previous-video-button' |
2 | export * from './p2p-info-button' | 2 | export * from './p2p-info-button' |
3 | export * from './peertube-link-button' | 3 | export * from './peertube-link-button' |
4 | export * from './peertube-live-display' | ||
4 | export * from './peertube-load-progress-bar' | 5 | export * from './peertube-load-progress-bar' |
5 | export * from './theater-button' | 6 | export * from './theater-button' |
diff --git a/client/src/assets/player/shared/control-bar/peertube-live-display.ts b/client/src/assets/player/shared/control-bar/peertube-live-display.ts new file mode 100644 index 000000000..649eb0b00 --- /dev/null +++ b/client/src/assets/player/shared/control-bar/peertube-live-display.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | import videojs from 'video.js' | ||
2 | import { PeerTubeLinkButtonOptions } from '../../types' | ||
3 | |||
4 | const ClickableComponent = videojs.getComponent('ClickableComponent') | ||
5 | |||
6 | class PeerTubeLiveDisplay extends ClickableComponent { | ||
7 | private interval: any | ||
8 | |||
9 | private contentEl_: any | ||
10 | |||
11 | constructor (player: videojs.Player, options?: PeerTubeLinkButtonOptions) { | ||
12 | super(player, options as any) | ||
13 | |||
14 | this.interval = this.setInterval(() => this.updateClass(), 1000) | ||
15 | |||
16 | this.show() | ||
17 | this.updateSync(true) | ||
18 | } | ||
19 | |||
20 | dispose () { | ||
21 | if (this.interval) { | ||
22 | this.clearInterval(this.interval) | ||
23 | this.interval = undefined | ||
24 | } | ||
25 | |||
26 | this.contentEl_ = null | ||
27 | |||
28 | super.dispose() | ||
29 | } | ||
30 | |||
31 | createEl () { | ||
32 | const el = super.createEl('div', { | ||
33 | className: 'vjs-live-control vjs-control' | ||
34 | }) | ||
35 | |||
36 | this.contentEl_ = videojs.dom.createEl('div', { | ||
37 | className: 'vjs-live-display' | ||
38 | }, { | ||
39 | 'aria-live': 'off' | ||
40 | }) | ||
41 | |||
42 | this.contentEl_.appendChild(videojs.dom.createEl('span', { | ||
43 | className: 'vjs-control-text', | ||
44 | textContent: `${this.localize('Stream Type')}\u00a0` | ||
45 | })) | ||
46 | |||
47 | this.contentEl_.appendChild(document.createTextNode(this.localize('LIVE'))) | ||
48 | |||
49 | el.appendChild(this.contentEl_) | ||
50 | return el | ||
51 | } | ||
52 | |||
53 | handleClick () { | ||
54 | const hlsjs = this.getHLSJS() | ||
55 | if (!hlsjs) return | ||
56 | |||
57 | this.player().currentTime(hlsjs.liveSyncPosition) | ||
58 | this.player().play() | ||
59 | this.updateSync(true) | ||
60 | } | ||
61 | |||
62 | private updateClass () { | ||
63 | const hlsjs = this.getHLSJS() | ||
64 | if (!hlsjs) return | ||
65 | |||
66 | // Not loaded yet | ||
67 | if (this.player().currentTime() === 0) return | ||
68 | |||
69 | const isSync = Math.abs(this.player().currentTime() - hlsjs.liveSyncPosition) < 10 | ||
70 | this.updateSync(isSync) | ||
71 | } | ||
72 | |||
73 | private updateSync (isSync: boolean) { | ||
74 | if (isSync) { | ||
75 | this.addClass('synced-with-live-edge') | ||
76 | this.removeAttribute('title') | ||
77 | this.disable() | ||
78 | } else { | ||
79 | this.removeClass('synced-with-live-edge') | ||
80 | this.setAttribute('title', this.localize('Go back to the live')) | ||
81 | this.enable() | ||
82 | } | ||
83 | } | ||
84 | |||
85 | private getHLSJS () { | ||
86 | const p2pMediaLoader = this.player()?.p2pMediaLoader | ||
87 | if (!p2pMediaLoader) return undefined | ||
88 | |||
89 | return p2pMediaLoader().getHLSJS() | ||
90 | } | ||
91 | } | ||
92 | |||
93 | videojs.registerComponent('PeerTubeLiveDisplay', PeerTubeLiveDisplay) | ||
diff --git a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts index ec1e1038b..f5b4b3919 100644 --- a/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts +++ b/client/src/assets/player/shared/hotkeys/peertube-hotkeys-plugin.ts | |||
@@ -4,6 +4,10 @@ type KeyHandler = { accept: (event: KeyboardEvent) => boolean, cb: (e: KeyboardE | |||
4 | 4 | ||
5 | const Plugin = videojs.getPlugin('plugin') | 5 | const Plugin = videojs.getPlugin('plugin') |
6 | 6 | ||
7 | export type HotkeysOptions = { | ||
8 | isLive: boolean | ||
9 | } | ||
10 | |||
7 | class PeerTubeHotkeysPlugin extends Plugin { | 11 | class PeerTubeHotkeysPlugin extends Plugin { |
8 | private static readonly VOLUME_STEP = 0.1 | 12 | private static readonly VOLUME_STEP = 0.1 |
9 | private static readonly SEEK_STEP = 5 | 13 | private static readonly SEEK_STEP = 5 |
@@ -12,9 +16,13 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
12 | 16 | ||
13 | private readonly handlers: KeyHandler[] | 17 | private readonly handlers: KeyHandler[] |
14 | 18 | ||
15 | constructor (player: videojs.Player, options: videojs.PlayerOptions) { | 19 | private readonly isLive: boolean |
20 | |||
21 | constructor (player: videojs.Player, options: videojs.PlayerOptions & HotkeysOptions) { | ||
16 | super(player, options) | 22 | super(player, options) |
17 | 23 | ||
24 | this.isLive = options.isLive | ||
25 | |||
18 | this.handlers = this.buildHandlers() | 26 | this.handlers = this.buildHandlers() |
19 | 27 | ||
20 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) | 28 | this.handleKeyFunction = (event: KeyboardEvent) => this.onKeyDown(event) |
@@ -68,28 +76,6 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
68 | } | 76 | } |
69 | }, | 77 | }, |
70 | 78 | ||
71 | // Rewind | ||
72 | { | ||
73 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
74 | cb: e => { | ||
75 | e.preventDefault() | ||
76 | |||
77 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
78 | this.player.currentTime(target) | ||
79 | } | ||
80 | }, | ||
81 | |||
82 | // Forward | ||
83 | { | ||
84 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
85 | cb: e => { | ||
86 | e.preventDefault() | ||
87 | |||
88 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
89 | this.player.currentTime(target) | ||
90 | } | ||
91 | }, | ||
92 | |||
93 | // Fullscreen | 79 | // Fullscreen |
94 | { | 80 | { |
95 | // f key or Ctrl + Enter | 81 | // f key or Ctrl + Enter |
@@ -116,6 +102,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
116 | { | 102 | { |
117 | accept: e => e.key === '>', | 103 | accept: e => e.key === '>', |
118 | cb: () => { | 104 | cb: () => { |
105 | if (this.isLive) return | ||
106 | |||
119 | const target = Math.min(this.player.playbackRate() + 0.1, 5) | 107 | const target = Math.min(this.player.playbackRate() + 0.1, 5) |
120 | 108 | ||
121 | this.player.playbackRate(parseFloat(target.toFixed(2))) | 109 | this.player.playbackRate(parseFloat(target.toFixed(2))) |
@@ -126,6 +114,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
126 | { | 114 | { |
127 | accept: e => e.key === '<', | 115 | accept: e => e.key === '<', |
128 | cb: () => { | 116 | cb: () => { |
117 | if (this.isLive) return | ||
118 | |||
129 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) | 119 | const target = Math.max(this.player.playbackRate() - 0.1, 0.10) |
130 | 120 | ||
131 | this.player.playbackRate(parseFloat(target.toFixed(2))) | 121 | this.player.playbackRate(parseFloat(target.toFixed(2))) |
@@ -136,6 +126,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
136 | { | 126 | { |
137 | accept: e => e.key === ',', | 127 | accept: e => e.key === ',', |
138 | cb: () => { | 128 | cb: () => { |
129 | if (this.isLive) return | ||
130 | |||
139 | this.player.pause() | 131 | this.player.pause() |
140 | 132 | ||
141 | // Calculate movement distance (assuming 30 fps) | 133 | // Calculate movement distance (assuming 30 fps) |
@@ -148,6 +140,8 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
148 | { | 140 | { |
149 | accept: e => e.key === '.', | 141 | accept: e => e.key === '.', |
150 | cb: () => { | 142 | cb: () => { |
143 | if (this.isLive) return | ||
144 | |||
151 | this.player.pause() | 145 | this.player.pause() |
152 | 146 | ||
153 | // Calculate movement distance (assuming 30 fps) | 147 | // Calculate movement distance (assuming 30 fps) |
@@ -157,11 +151,47 @@ class PeerTubeHotkeysPlugin extends Plugin { | |||
157 | } | 151 | } |
158 | ] | 152 | ] |
159 | 153 | ||
154 | if (this.isLive) return handlers | ||
155 | |||
156 | return handlers.concat(this.buildVODHandlers()) | ||
157 | } | ||
158 | |||
159 | private buildVODHandlers () { | ||
160 | const handlers: KeyHandler[] = [ | ||
161 | // Rewind | ||
162 | { | ||
163 | accept: e => this.isNaked(e, 'ArrowLeft') || this.isNaked(e, 'MediaRewind'), | ||
164 | cb: e => { | ||
165 | if (this.isLive) return | ||
166 | |||
167 | e.preventDefault() | ||
168 | |||
169 | const target = Math.max(0, this.player.currentTime() - PeerTubeHotkeysPlugin.SEEK_STEP) | ||
170 | this.player.currentTime(target) | ||
171 | } | ||
172 | }, | ||
173 | |||
174 | // Forward | ||
175 | { | ||
176 | accept: e => this.isNaked(e, 'ArrowRight') || this.isNaked(e, 'MediaForward'), | ||
177 | cb: e => { | ||
178 | if (this.isLive) return | ||
179 | |||
180 | e.preventDefault() | ||
181 | |||
182 | const target = Math.min(this.player.duration(), this.player.currentTime() + PeerTubeHotkeysPlugin.SEEK_STEP) | ||
183 | this.player.currentTime(target) | ||
184 | } | ||
185 | } | ||
186 | ] | ||
187 | |||
160 | // 0-9 key handlers | 188 | // 0-9 key handlers |
161 | for (let i = 0; i < 10; i++) { | 189 | for (let i = 0; i < 10; i++) { |
162 | handlers.push({ | 190 | handlers.push({ |
163 | accept: e => this.isNakedOrShift(e, i + ''), | 191 | accept: e => this.isNakedOrShift(e, i + ''), |
164 | cb: e => { | 192 | cb: e => { |
193 | if (this.isLive) return | ||
194 | |||
165 | e.preventDefault() | 195 | e.preventDefault() |
166 | 196 | ||
167 | this.player.currentTime(this.player.duration() * i * 0.1) | 197 | this.player.currentTime(this.player.duration() * i * 0.1) |
diff --git a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts index 27f366732..26f923e92 100644 --- a/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts +++ b/client/src/assets/player/shared/manager-options/control-bar-options-builder.ts | |||
@@ -30,10 +30,7 @@ export class ControlBarOptionsBuilder { | |||
30 | } | 30 | } |
31 | 31 | ||
32 | Object.assign(children, { | 32 | Object.assign(children, { |
33 | currentTimeDisplay: {}, | 33 | ...this.getTimeControls(), |
34 | timeDivider: {}, | ||
35 | durationDisplay: {}, | ||
36 | liveDisplay: {}, | ||
37 | 34 | ||
38 | flexibleWidthSpacer: {}, | 35 | flexibleWidthSpacer: {}, |
39 | 36 | ||
@@ -74,7 +71,9 @@ export class ControlBarOptionsBuilder { | |||
74 | private getSettingsButton () { | 71 | private getSettingsButton () { |
75 | const settingEntries: string[] = [] | 72 | const settingEntries: string[] = [] |
76 | 73 | ||
77 | settingEntries.push('playbackRateMenuButton') | 74 | if (!this.options.isLive) { |
75 | settingEntries.push('playbackRateMenuButton') | ||
76 | } | ||
78 | 77 | ||
79 | if (this.options.captions === true) settingEntries.push('captionsButton') | 78 | if (this.options.captions === true) settingEntries.push('captionsButton') |
80 | 79 | ||
@@ -90,7 +89,23 @@ export class ControlBarOptionsBuilder { | |||
90 | } | 89 | } |
91 | } | 90 | } |
92 | 91 | ||
92 | private getTimeControls () { | ||
93 | if (this.options.isLive) { | ||
94 | return { | ||
95 | peerTubeLiveDisplay: {} | ||
96 | } | ||
97 | } | ||
98 | |||
99 | return { | ||
100 | currentTimeDisplay: {}, | ||
101 | timeDivider: {}, | ||
102 | durationDisplay: {} | ||
103 | } | ||
104 | } | ||
105 | |||
93 | private getProgressControl () { | 106 | private getProgressControl () { |
107 | if (this.options.isLive) return {} | ||
108 | |||
94 | const loadProgressBar = this.mode === 'webtorrent' | 109 | const loadProgressBar = this.mode === 'webtorrent' |
95 | ? 'peerTubeLoadProgressBar' | 110 | ? 'peerTubeLoadProgressBar' |
96 | : 'loadProgressBar' | 111 | : 'loadProgressBar' |
diff --git a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts index a14beb347..7f7d90ab9 100644 --- a/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts +++ b/client/src/assets/player/shared/p2p-media-loader/hls-plugin.ts | |||
@@ -281,8 +281,8 @@ class Html5Hlsjs { | |||
281 | if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 | 281 | if (this.errorCounts[data.type]) this.errorCounts[data.type] += 1 |
282 | else this.errorCounts[data.type] = 1 | 282 | else this.errorCounts[data.type] = 1 |
283 | 283 | ||
284 | if (data.fatal) logger.warn(error.message) | 284 | if (data.fatal) logger.error(error.message, { currentTime: this.player.currentTime(), data }) |
285 | else logger.error(error.message, { data }) | 285 | else logger.warn(error.message) |
286 | 286 | ||
287 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { | 287 | if (data.type === Hlsjs.ErrorTypes.NETWORK_ERROR) { |
288 | error.code = 2 | 288 | error.code = 2 |
diff --git a/client/src/assets/player/shared/stats/stats-card.ts b/client/src/assets/player/shared/stats/stats-card.ts index f23ae48be..471a5e46c 100644 --- a/client/src/assets/player/shared/stats/stats-card.ts +++ b/client/src/assets/player/shared/stats/stats-card.ts | |||
@@ -182,7 +182,7 @@ class StatsCard extends Component { | |||
182 | let colorSpace = 'unknown' | 182 | let colorSpace = 'unknown' |
183 | let codecs = 'unknown' | 183 | let codecs = 'unknown' |
184 | 184 | ||
185 | if (metadata?.streams[0]) { | 185 | if (metadata?.streams?.[0]) { |
186 | const stream = metadata.streams[0] | 186 | const stream = metadata.streams[0] |
187 | 187 | ||
188 | colorSpace = stream['color_space'] !== 'unknown' | 188 | colorSpace = stream['color_space'] !== 'unknown' |
@@ -193,7 +193,7 @@ class StatsCard extends Component { | |||
193 | } | 193 | } |
194 | 194 | ||
195 | const resolution = videoFile?.resolution.label + videoFile?.fps | 195 | const resolution = videoFile?.resolution.label + videoFile?.fps |
196 | const buffer = this.timeRangesToString(this.player().buffered()) | 196 | const buffer = this.timeRangesToString(this.player_.buffered()) |
197 | const progress = this.player_.webtorrent().getTorrent()?.progress | 197 | const progress = this.player_.webtorrent().getTorrent()?.progress |
198 | 198 | ||
199 | return { | 199 | return { |
diff --git a/client/src/assets/player/types/manager-options.ts b/client/src/assets/player/types/manager-options.ts index 3057a5adb..3fbcec29c 100644 --- a/client/src/assets/player/types/manager-options.ts +++ b/client/src/assets/player/types/manager-options.ts | |||
@@ -29,6 +29,8 @@ export interface CustomizationOptions { | |||
29 | resume?: string | 29 | resume?: string |
30 | 30 | ||
31 | peertubeLink: boolean | 31 | peertubeLink: boolean |
32 | |||
33 | playbackRate?: number | string | ||
32 | } | 34 | } |
33 | 35 | ||
34 | export interface CommonOptions extends CustomizationOptions { | 36 | export interface CommonOptions extends CustomizationOptions { |
diff --git a/client/src/assets/player/types/peertube-videojs-typings.ts b/client/src/assets/player/types/peertube-videojs-typings.ts index c60154f3b..5674f78cb 100644 --- a/client/src/assets/player/types/peertube-videojs-typings.ts +++ b/client/src/assets/player/types/peertube-videojs-typings.ts | |||
@@ -3,6 +3,7 @@ import videojs from 'video.js' | |||
3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' | 3 | import { Engine } from '@peertube/p2p-media-loader-hlsjs' |
4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' | 4 | import { VideoFile, VideoPlaylist, VideoPlaylistElement } from '@shared/models' |
5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' | 5 | import { PeerTubeDockPluginOptions } from '../shared/dock/peertube-dock-plugin' |
6 | import { HotkeysOptions } from '../shared/hotkeys/peertube-hotkeys-plugin' | ||
6 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' | 7 | import { Html5Hlsjs } from '../shared/p2p-media-loader/hls-plugin' |
7 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' | 8 | import { P2pMediaLoaderPlugin } from '../shared/p2p-media-loader/p2p-media-loader-plugin' |
8 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' | 9 | import { RedundancyUrlManager } from '../shared/p2p-media-loader/redundancy-url-manager' |
@@ -44,7 +45,7 @@ declare module 'video.js' { | |||
44 | 45 | ||
45 | bezels (): void | 46 | bezels (): void |
46 | peertubeMobile (): void | 47 | peertubeMobile (): void |
47 | peerTubeHotkeysPlugin (): void | 48 | peerTubeHotkeysPlugin (options?: HotkeysOptions): void |
48 | 49 | ||
49 | stats (options?: StatsCardOptions): StatsForNerdsPlugin | 50 | stats (options?: StatsCardOptions): StatsForNerdsPlugin |
50 | 51 | ||
diff --git a/client/src/root-helpers/logger.ts b/client/src/root-helpers/logger.ts index d1fdf73aa..618be62cd 100644 --- a/client/src/root-helpers/logger.ts +++ b/client/src/root-helpers/logger.ts | |||
@@ -27,6 +27,10 @@ class Logger { | |||
27 | warn (message: LoggerMessage, meta?: LoggerMeta) { | 27 | warn (message: LoggerMessage, meta?: LoggerMeta) { |
28 | this.runHooks('warn', message, meta) | 28 | this.runHooks('warn', message, meta) |
29 | 29 | ||
30 | this.clientWarn(message, meta) | ||
31 | } | ||
32 | |||
33 | clientWarn (message: LoggerMessage, meta?: LoggerMeta) { | ||
30 | if (meta) console.warn(message, meta) | 34 | if (meta) console.warn(message, meta) |
31 | else console.warn(message) | 35 | else console.warn(message) |
32 | } | 36 | } |
@@ -34,6 +38,10 @@ class Logger { | |||
34 | error (message: LoggerMessage, meta?: LoggerMeta) { | 38 | error (message: LoggerMessage, meta?: LoggerMeta) { |
35 | this.runHooks('error', message, meta) | 39 | this.runHooks('error', message, meta) |
36 | 40 | ||
41 | this.clientError(message, meta) | ||
42 | } | ||
43 | |||
44 | clientError (message: LoggerMessage, meta?: LoggerMeta) { | ||
37 | if (meta) console.error(message, meta) | 45 | if (meta) console.error(message, meta) |
38 | else console.error(message) | 46 | else console.error(message) |
39 | } | 47 | } |
diff --git a/client/src/root-helpers/plugins-manager.ts b/client/src/root-helpers/plugins-manager.ts index 6c64e2b01..e5b06a94c 100644 --- a/client/src/root-helpers/plugins-manager.ts +++ b/client/src/root-helpers/plugins-manager.ts | |||
@@ -3,7 +3,7 @@ import * as debug from 'debug' | |||
3 | import { firstValueFrom, ReplaySubject } from 'rxjs' | 3 | import { firstValueFrom, ReplaySubject } from 'rxjs' |
4 | import { first, shareReplay } from 'rxjs/operators' | 4 | import { first, shareReplay } from 'rxjs/operators' |
5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' | 5 | import { RegisterClientHelpers } from 'src/types/register-client-option.model' |
6 | import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' | 6 | import { getExternalAuthHref, getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks' |
7 | import { | 7 | import { |
8 | ClientHookName, | 8 | ClientHookName, |
9 | clientHookObject, | 9 | clientHookObject, |
@@ -16,7 +16,6 @@ import { | |||
16 | RegisterClientRouteOptions, | 16 | RegisterClientRouteOptions, |
17 | RegisterClientSettingsScriptOptions, | 17 | RegisterClientSettingsScriptOptions, |
18 | RegisterClientVideoFieldOptions, | 18 | RegisterClientVideoFieldOptions, |
19 | RegisteredExternalAuthConfig, | ||
20 | ServerConfigPlugin | 19 | ServerConfigPlugin |
21 | } from '@shared/models' | 20 | } from '@shared/models' |
22 | import { environment } from '../environments/environment' | 21 | import { environment } from '../environments/environment' |
@@ -94,9 +93,13 @@ class PluginsManager { | |||
94 | return isTheme ? '/themes' : '/plugins' | 93 | return isTheme ? '/themes' : '/plugins' |
95 | } | 94 | } |
96 | 95 | ||
97 | static getExternalAuthHref (auth: RegisteredExternalAuthConfig) { | 96 | static getDefaultLoginHref (apiUrl: string, serverConfig: HTMLServerConfig) { |
98 | return environment.apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` | 97 | if (!serverConfig || serverConfig.client.menu.login.redirectOnSingleExternalAuth !== true) return undefined |
99 | 98 | ||
99 | const externalAuths = serverConfig.plugin.registeredExternalAuths | ||
100 | if (externalAuths.length !== 1) return undefined | ||
101 | |||
102 | return getExternalAuthHref(apiUrl, externalAuths[0]) | ||
100 | } | 103 | } |
101 | 104 | ||
102 | loadPluginsList (config: HTMLServerConfig) { | 105 | loadPluginsList (config: HTMLServerConfig) { |
diff --git a/client/src/sass/player/control-bar.scss b/client/src/sass/player/control-bar.scss index 0082378e4..96b3adf66 100644 --- a/client/src/sass/player/control-bar.scss +++ b/client/src/sass/player/control-bar.scss | |||
@@ -153,8 +153,25 @@ | |||
153 | } | 153 | } |
154 | 154 | ||
155 | .vjs-live-control { | 155 | .vjs-live-control { |
156 | line-height: $control-bar-height; | 156 | padding: 5px 7px; |
157 | min-width: 4em; | 157 | border-radius: 3px; |
158 | height: fit-content; | ||
159 | margin: auto 10px; | ||
160 | font-weight: bold; | ||
161 | max-width: fit-content; | ||
162 | opacity: 1 !important; | ||
163 | line-height: normal; | ||
164 | position: relative; | ||
165 | top: -1px; | ||
166 | |||
167 | &.synced-with-live-edge { | ||
168 | background: #d7281c; | ||
169 | } | ||
170 | |||
171 | &:not(.synced-with-live-edge) { | ||
172 | cursor: pointer; | ||
173 | background: #80807f; | ||
174 | } | ||
158 | } | 175 | } |
159 | 176 | ||
160 | .vjs-peertube { | 177 | .vjs-peertube { |
diff --git a/client/src/sass/primeng-custom.scss b/client/src/sass/primeng-custom.scss index 88f6efb6a..ee66a9db3 100644 --- a/client/src/sass/primeng-custom.scss +++ b/client/src/sass/primeng-custom.scss | |||
@@ -294,6 +294,7 @@ body .p-datepicker .p-datepicker-header .p-datepicker-title select:focus { | |||
294 | body .p-datepicker table { | 294 | body .p-datepicker table { |
295 | font-size: 14px; | 295 | font-size: 14px; |
296 | margin: 0.857em 0 0 0; | 296 | margin: 0.857em 0 0 0; |
297 | table-layout: fixed; | ||
297 | } | 298 | } |
298 | body .p-datepicker table th { | 299 | body .p-datepicker table th { |
299 | padding: 0.5em; | 300 | padding: 0.5em; |
diff --git a/client/src/standalone/videos/shared/player-manager-options.ts b/client/src/standalone/videos/shared/player-manager-options.ts index b0bdb2dd9..f09c86d14 100644 --- a/client/src/standalone/videos/shared/player-manager-options.ts +++ b/client/src/standalone/videos/shared/player-manager-options.ts | |||
@@ -38,6 +38,7 @@ export class PlayerManagerOptions { | |||
38 | private enableApi = false | 38 | private enableApi = false |
39 | private startTime: number | string = 0 | 39 | private startTime: number | string = 0 |
40 | private stopTime: number | string | 40 | private stopTime: number | string |
41 | private playbackRate: number | string | ||
41 | 42 | ||
42 | private title: boolean | 43 | private title: boolean |
43 | private warningTitle: boolean | 44 | private warningTitle: boolean |
@@ -130,6 +131,7 @@ export class PlayerManagerOptions { | |||
130 | this.subtitle = getParamString(params, 'subtitle') | 131 | this.subtitle = getParamString(params, 'subtitle') |
131 | this.startTime = getParamString(params, 'start') | 132 | this.startTime = getParamString(params, 'start') |
132 | this.stopTime = getParamString(params, 'stop') | 133 | this.stopTime = getParamString(params, 'stop') |
134 | this.playbackRate = getParamString(params, 'playbackRate') | ||
133 | 135 | ||
134 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') | 136 | this.bigPlayBackgroundColor = getParamString(params, 'bigPlayBackgroundColor') |
135 | this.foregroundColor = getParamString(params, 'foregroundColor') | 137 | this.foregroundColor = getParamString(params, 'foregroundColor') |
@@ -210,6 +212,8 @@ export class PlayerManagerOptions { | |||
210 | ? playlistTracker.getCurrentElement().stopTimestamp | 212 | ? playlistTracker.getCurrentElement().stopTimestamp |
211 | : this.stopTime, | 213 | : this.stopTime, |
212 | 214 | ||
215 | playbackRate: this.playbackRate, | ||
216 | |||
213 | videoCaptions, | 217 | videoCaptions, |
214 | inactivityTimeout: 2500, | 218 | inactivityTimeout: 2500, |
215 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), | 219 | videoViewUrl: this.videoFetcher.getVideoViewsUrl(video.uuid), |
diff --git a/client/yarn.lock b/client/yarn.lock index b680bfdfb..6a5456283 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -1789,10 +1789,10 @@ | |||
1789 | read-package-json-fast "^2.0.3" | 1789 | read-package-json-fast "^2.0.3" |
1790 | which "^2.0.2" | 1790 | which "^2.0.2" |
1791 | 1791 | ||
1792 | "@peertube/p2p-media-loader-core@^1.0.13", "@peertube/p2p-media-loader-core@^1.0.8": | 1792 | "@peertube/p2p-media-loader-core@^1.0.14": |
1793 | version "1.0.13" | 1793 | version "1.0.14" |
1794 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.13.tgz#36744a291b69c001b2562c1a93017979f8534ff8" | 1794 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-core/-/p2p-media-loader-core-1.0.14.tgz#b4442dd343d6b30a51502e1240275eb98ef2c788" |
1795 | integrity sha512-ArSAaeuxwwBAG0Xd3Gj0TzKObLfJFYzHz9+fREvmUf+GZQEG6qGwWmrdVWL6xjPiEuo6LdFeCOnHSQzAbj/ptg== | 1795 | integrity sha512-tjQv1CNziNY+zYzcL1h4q6AA2WuBUZnBIeVyjWR/EsO1EEC1VMdvPsL02cqYLz9yvIxgycjeTsWCm6XDqNgXRw== |
1796 | dependencies: | 1796 | dependencies: |
1797 | bittorrent-tracker "^9.19.0" | 1797 | bittorrent-tracker "^9.19.0" |
1798 | debug "^4.3.4" | 1798 | debug "^4.3.4" |
@@ -1800,12 +1800,12 @@ | |||
1800 | sha.js "^2.4.11" | 1800 | sha.js "^2.4.11" |
1801 | simple-peer "^9.11.1" | 1801 | simple-peer "^9.11.1" |
1802 | 1802 | ||
1803 | "@peertube/p2p-media-loader-hlsjs@^1.0.13": | 1803 | "@peertube/p2p-media-loader-hlsjs@^1.0.14": |
1804 | version "1.0.13" | 1804 | version "1.0.14" |
1805 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.13.tgz#5305e2008041d01850802544d1c49298f79dd67a" | 1805 | resolved "https://registry.yarnpkg.com/@peertube/p2p-media-loader-hlsjs/-/p2p-media-loader-hlsjs-1.0.14.tgz#829629a57608b0e30f4b50bc98578e6bee9f8b9b" |
1806 | integrity sha512-2BO2oaRsSHEhLkgi2iw1r4n1Yqq1EnyoOgOZccPDqjmHUsZSV/wNrno8WYr6LsleudrHA26Imu57hVD1jDx7lg== | 1806 | integrity sha512-ySUVgUvAFXCE5E94xxjfywQ8xzk3jy9UGVkgi5Oqq+QeY7uG+o7CZ+LsQ/RjXgWBD70tEnyyfADHtL+9FCnwyQ== |
1807 | dependencies: | 1807 | dependencies: |
1808 | "@peertube/p2p-media-loader-core" "^1.0.8" | 1808 | "@peertube/p2p-media-loader-core" "^1.0.14" |
1809 | debug "^4.3.4" | 1809 | debug "^4.3.4" |
1810 | events "^3.3.0" | 1810 | events "^3.3.0" |
1811 | m3u8-parser "^4.7.1" | 1811 | m3u8-parser "^4.7.1" |
diff --git a/config/default.yaml b/config/default.yaml index 20094ae8f..b2c418a0a 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -37,6 +37,11 @@ rates_limit: | |||
37 | window: 10 minutes | 37 | window: 10 minutes |
38 | max: 10 | 38 | max: 10 |
39 | 39 | ||
40 | oauth2: | ||
41 | token_lifetime: | ||
42 | access_token: '1 day' | ||
43 | refresh_token: '2 weeks' | ||
44 | |||
40 | # Proxies to trust to get real client IP | 45 | # Proxies to trust to get real client IP |
41 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' | 46 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' |
42 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) | 47 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) |
diff --git a/config/production.yaml.example b/config/production.yaml.example index e8b354d01..36fa70417 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -35,6 +35,11 @@ rates_limit: | |||
35 | window: 10 minutes | 35 | window: 10 minutes |
36 | max: 10 | 36 | max: 10 |
37 | 37 | ||
38 | oauth2: | ||
39 | token_lifetime: | ||
40 | access_token: '1 day' | ||
41 | refresh_token: '2 weeks' | ||
42 | |||
38 | # Proxies to trust to get real client IP | 43 | # Proxies to trust to get real client IP |
39 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' | 44 | # If you run PeerTube just behind a local proxy (nginx), keep 'loopback' |
40 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) | 45 | # If you run PeerTube behind a remote proxy, add the proxy IP address (or subnet) |
diff --git a/package.json b/package.json index d7d19afc2..b48f65bbd 100644 --- a/package.json +++ b/package.json | |||
@@ -225,7 +225,7 @@ | |||
225 | "swagger-cli": "^4.0.2", | 225 | "swagger-cli": "^4.0.2", |
226 | "ts-node": "^10.8.1", | 226 | "ts-node": "^10.8.1", |
227 | "tsc-watch": "^5.0.3", | 227 | "tsc-watch": "^5.0.3", |
228 | "typescript": "^4.0.5" | 228 | "typescript": "~4.8" |
229 | }, | 229 | }, |
230 | "bundlewatch": { | 230 | "bundlewatch": { |
231 | "files": [ | 231 | "files": [ |
diff --git a/scripts/i18n/create-custom-files.ts b/scripts/i18n/create-custom-files.ts index bcd7fe2a2..3b5045954 100755 --- a/scripts/i18n/create-custom-files.ts +++ b/scripts/i18n/create-custom-files.ts | |||
@@ -41,6 +41,7 @@ const playerKeys = { | |||
41 | 'Volume': 'Volume', | 41 | 'Volume': 'Volume', |
42 | 'Codecs': 'Codecs', | 42 | 'Codecs': 'Codecs', |
43 | 'Color': 'Color', | 43 | 'Color': 'Color', |
44 | 'Go back to the live': 'Go back to the live', | ||
44 | 'Connection Speed': 'Connection Speed', | 45 | 'Connection Speed': 'Connection Speed', |
45 | 'Network Activity': 'Network Activity', | 46 | 'Network Activity': 'Network Activity', |
46 | 'Total Transfered': 'Total Transfered', | 47 | 'Total Transfered': 'Total Transfered', |
@@ -279,7 +279,7 @@ app.use((err, _req, res: express.Response, _next) => { | |||
279 | }) | 279 | }) |
280 | }) | 280 | }) |
281 | 281 | ||
282 | const server = createWebsocketTrackerServer(app) | 282 | const { server, trackerServer } = createWebsocketTrackerServer(app) |
283 | 283 | ||
284 | // ----------- Run ----------- | 284 | // ----------- Run ----------- |
285 | 285 | ||
@@ -328,7 +328,8 @@ async function startApplication () { | |||
328 | VideoChannelSyncLatestScheduler.Instance.enable() | 328 | VideoChannelSyncLatestScheduler.Instance.enable() |
329 | VideoViewsBufferScheduler.Instance.enable() | 329 | VideoViewsBufferScheduler.Instance.enable() |
330 | GeoIPUpdateScheduler.Instance.enable() | 330 | GeoIPUpdateScheduler.Instance.enable() |
331 | OpenTelemetryMetrics.Instance.registerMetrics() | 331 | |
332 | OpenTelemetryMetrics.Instance.registerMetrics({ trackerServer }) | ||
332 | 333 | ||
333 | PluginManager.Instance.init(server) | 334 | PluginManager.Instance.init(server) |
334 | // Before PeerTubeSocket init | 335 | // Before PeerTubeSocket init |
diff --git a/server/controllers/activitypub/client.ts b/server/controllers/activitypub/client.ts index 8e064fb5b..def320730 100644 --- a/server/controllers/activitypub/client.ts +++ b/server/controllers/activitypub/client.ts | |||
@@ -309,7 +309,7 @@ async function videoCommentsController (req: express.Request, res: express.Respo | |||
309 | if (redirectIfNotOwned(video.url, res)) return | 309 | if (redirectIfNotOwned(video.url, res)) return |
310 | 310 | ||
311 | const handler = async (start: number, count: number) => { | 311 | const handler = async (start: number, count: number) => { |
312 | const result = await VideoCommentModel.listAndCountByVideoForAP(video, start, count) | 312 | const result = await VideoCommentModel.listAndCountByVideoForAP({ video, start, count }) |
313 | 313 | ||
314 | return { | 314 | return { |
315 | total: result.total, | 315 | total: result.total, |
diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index 44d64776c..70ca21500 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts | |||
@@ -1,4 +1,6 @@ | |||
1 | import { MCommentFormattable } from '@server/types/models' | ||
1 | import express from 'express' | 2 | import express from 'express' |
3 | |||
2 | import { ResultList, ThreadsResultList, UserRight, VideoCommentCreate } from '../../../../shared/models' | 4 | import { ResultList, ThreadsResultList, UserRight, VideoCommentCreate } from '../../../../shared/models' |
3 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' | 5 | import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' |
4 | import { VideoCommentThreads } from '../../../../shared/models/videos/comment/video-comment.model' | 6 | import { VideoCommentThreads } from '../../../../shared/models/videos/comment/video-comment.model' |
@@ -109,7 +111,7 @@ async function listVideoThreads (req: express.Request, res: express.Response) { | |||
109 | const video = res.locals.onlyVideo | 111 | const video = res.locals.onlyVideo |
110 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined | 112 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined |
111 | 113 | ||
112 | let resultList: ThreadsResultList<VideoCommentModel> | 114 | let resultList: ThreadsResultList<MCommentFormattable> |
113 | 115 | ||
114 | if (video.commentsEnabled === true) { | 116 | if (video.commentsEnabled === true) { |
115 | const apiOptions = await Hooks.wrapObject({ | 117 | const apiOptions = await Hooks.wrapObject({ |
@@ -144,12 +146,11 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo | |||
144 | const video = res.locals.onlyVideo | 146 | const video = res.locals.onlyVideo |
145 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined | 147 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined |
146 | 148 | ||
147 | let resultList: ResultList<VideoCommentModel> | 149 | let resultList: ResultList<MCommentFormattable> |
148 | 150 | ||
149 | if (video.commentsEnabled === true) { | 151 | if (video.commentsEnabled === true) { |
150 | const apiOptions = await Hooks.wrapObject({ | 152 | const apiOptions = await Hooks.wrapObject({ |
151 | videoId: video.id, | 153 | videoId: video.id, |
152 | isVideoOwned: video.isOwned(), | ||
153 | threadId: res.locals.videoCommentThread.id, | 154 | threadId: res.locals.videoCommentThread.id, |
154 | user | 155 | user |
155 | }, 'filter:api.video-thread-comments.list.params') | 156 | }, 'filter:api.video-thread-comments.list.params') |
diff --git a/server/controllers/api/videos/token.ts b/server/controllers/api/videos/token.ts index 009b6dfb6..22387c3e8 100644 --- a/server/controllers/api/videos/token.ts +++ b/server/controllers/api/videos/token.ts | |||
@@ -22,7 +22,7 @@ export { | |||
22 | function generateToken (req: express.Request, res: express.Response) { | 22 | function generateToken (req: express.Request, res: express.Response) { |
23 | const video = res.locals.onlyVideo | 23 | const video = res.locals.onlyVideo |
24 | 24 | ||
25 | const { token, expires } = VideoTokensManager.Instance.create(video.uuid) | 25 | const { token, expires } = VideoTokensManager.Instance.create({ videoUUID: video.uuid, user: res.locals.oauth.token.User }) |
26 | 26 | ||
27 | return res.json({ | 27 | return res.json({ |
28 | files: { | 28 | files: { |
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 772fe734d..ef810a842 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -285,8 +285,8 @@ function addVideosToFeed (feed: Feed, videos: VideoModel[]) { | |||
285 | content: toSafeHtml(video.description), | 285 | content: toSafeHtml(video.description), |
286 | author: [ | 286 | author: [ |
287 | { | 287 | { |
288 | name: video.VideoChannel.Account.getDisplayName(), | 288 | name: video.VideoChannel.getDisplayName(), |
289 | link: video.VideoChannel.Account.Actor.url | 289 | link: video.VideoChannel.Actor.url |
290 | } | 290 | } |
291 | ], | 291 | ], |
292 | date: video.publishedAt, | 292 | date: video.publishedAt, |
diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts index 19a8b2bc9..c4f3a8889 100644 --- a/server/controllers/tracker.ts +++ b/server/controllers/tracker.ts | |||
@@ -1,17 +1,22 @@ | |||
1 | import { Server as TrackerServer } from 'bittorrent-tracker' | 1 | import { Server as TrackerServer } from 'bittorrent-tracker' |
2 | import express from 'express' | 2 | import express from 'express' |
3 | import { createServer } from 'http' | 3 | import { createServer } from 'http' |
4 | import LRUCache from 'lru-cache' | ||
4 | import proxyAddr from 'proxy-addr' | 5 | import proxyAddr from 'proxy-addr' |
5 | import { WebSocketServer } from 'ws' | 6 | import { WebSocketServer } from 'ws' |
6 | import { Redis } from '@server/lib/redis' | ||
7 | import { logger } from '../helpers/logger' | 7 | import { logger } from '../helpers/logger' |
8 | import { CONFIG } from '../initializers/config' | 8 | import { CONFIG } from '../initializers/config' |
9 | import { TRACKER_RATE_LIMITS } from '../initializers/constants' | 9 | import { LRU_CACHE, TRACKER_RATE_LIMITS } from '../initializers/constants' |
10 | import { VideoFileModel } from '../models/video/video-file' | 10 | import { VideoFileModel } from '../models/video/video-file' |
11 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 11 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' |
12 | 12 | ||
13 | const trackerRouter = express.Router() | 13 | const trackerRouter = express.Router() |
14 | 14 | ||
15 | const blockedIPs = new LRUCache<string, boolean>({ | ||
16 | max: LRU_CACHE.TRACKER_IPS.MAX_SIZE, | ||
17 | ttl: TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME | ||
18 | }) | ||
19 | |||
15 | let peersIps = {} | 20 | let peersIps = {} |
16 | let peersIpInfoHash = {} | 21 | let peersIpInfoHash = {} |
17 | runPeersChecker() | 22 | runPeersChecker() |
@@ -55,8 +60,7 @@ const trackerServer = new TrackerServer({ | |||
55 | 60 | ||
56 | // Close socket connection and block IP for a few time | 61 | // Close socket connection and block IP for a few time |
57 | if (params.type === 'ws') { | 62 | if (params.type === 'ws') { |
58 | Redis.Instance.setTrackerBlockIP(ip) | 63 | blockedIPs.set(ip, true) |
59 | .catch(err => logger.error('Cannot set tracker block ip.', { err })) | ||
60 | 64 | ||
61 | // setTimeout to wait filter response | 65 | // setTimeout to wait filter response |
62 | setTimeout(() => params.socket.close(), 0) | 66 | setTimeout(() => params.socket.close(), 0) |
@@ -102,26 +106,22 @@ function createWebsocketTrackerServer (app: express.Application) { | |||
102 | if (request.url === '/tracker/socket') { | 106 | if (request.url === '/tracker/socket') { |
103 | const ip = proxyAddr(request, CONFIG.TRUST_PROXY) | 107 | const ip = proxyAddr(request, CONFIG.TRUST_PROXY) |
104 | 108 | ||
105 | Redis.Instance.doesTrackerBlockIPExist(ip) | 109 | if (blockedIPs.has(ip)) { |
106 | .then(result => { | 110 | logger.debug('Blocking IP %s from tracker.', ip) |
107 | if (result === true) { | ||
108 | logger.debug('Blocking IP %s from tracker.', ip) | ||
109 | 111 | ||
110 | socket.write('HTTP/1.1 403 Forbidden\r\n\r\n') | 112 | socket.write('HTTP/1.1 403 Forbidden\r\n\r\n') |
111 | socket.destroy() | 113 | socket.destroy() |
112 | return | 114 | return |
113 | } | 115 | } |
114 | 116 | ||
115 | // FIXME: typings | 117 | // FIXME: typings |
116 | return wss.handleUpgrade(request, socket as any, head, ws => wss.emit('connection', ws, request)) | 118 | return wss.handleUpgrade(request, socket as any, head, ws => wss.emit('connection', ws, request)) |
117 | }) | ||
118 | .catch(err => logger.error('Cannot check if tracker block ip exists.', { err })) | ||
119 | } | 119 | } |
120 | 120 | ||
121 | // Don't destroy socket, we have Socket.IO too | 121 | // Don't destroy socket, we have Socket.IO too |
122 | }) | 122 | }) |
123 | 123 | ||
124 | return server | 124 | return { server, trackerServer } |
125 | } | 125 | } |
126 | 126 | ||
127 | // --------------------------------------------------------------------------- | 127 | // --------------------------------------------------------------------------- |
diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 3dc5504e3..b3ab3ac64 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts | |||
@@ -103,7 +103,13 @@ function checkMimetypeRegex (fileMimeType: string, mimeTypeRegex: string) { | |||
103 | // --------------------------------------------------------------------------- | 103 | // --------------------------------------------------------------------------- |
104 | 104 | ||
105 | function toCompleteUUID (value: string) { | 105 | function toCompleteUUID (value: string) { |
106 | if (isShortUUID(value)) return shortToUUID(value) | 106 | if (isShortUUID(value)) { |
107 | try { | ||
108 | return shortToUUID(value) | ||
109 | } catch { | ||
110 | return null | ||
111 | } | ||
112 | } | ||
107 | 113 | ||
108 | return value | 114 | return value |
109 | } | 115 | } |
diff --git a/server/helpers/custom-validators/video-captions.ts b/server/helpers/custom-validators/video-captions.ts index 59ba005fe..d5b09ea03 100644 --- a/server/helpers/custom-validators/video-captions.ts +++ b/server/helpers/custom-validators/video-captions.ts | |||
@@ -8,10 +8,11 @@ function isVideoCaptionLanguageValid (value: any) { | |||
8 | return exists(value) && VIDEO_LANGUAGES[value] !== undefined | 8 | return exists(value) && VIDEO_LANGUAGES[value] !== undefined |
9 | } | 9 | } |
10 | 10 | ||
11 | const videoCaptionTypesRegex = Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT) | 11 | // MacOS sends application/octet-stream |
12 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream | 12 | const videoCaptionTypesRegex = [ ...Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT), 'application/octet-stream' ] |
13 | .map(m => `(${m})`) | 13 | .map(m => `(${m})`) |
14 | .join('|') | 14 | .join('|') |
15 | |||
15 | function isVideoCaptionFile (files: UploadFilesForCheck, field: string) { | 16 | function isVideoCaptionFile (files: UploadFilesForCheck, field: string) { |
16 | return isFileValid({ | 17 | return isFileValid({ |
17 | files, | 18 | files, |
diff --git a/server/helpers/custom-validators/video-imports.ts b/server/helpers/custom-validators/video-imports.ts index af93aea56..da8962cb6 100644 --- a/server/helpers/custom-validators/video-imports.ts +++ b/server/helpers/custom-validators/video-imports.ts | |||
@@ -22,10 +22,11 @@ function isVideoImportStateValid (value: any) { | |||
22 | return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined | 22 | return exists(value) && VIDEO_IMPORT_STATES[value] !== undefined |
23 | } | 23 | } |
24 | 24 | ||
25 | const videoTorrentImportRegex = Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT) | 25 | // MacOS sends application/octet-stream |
26 | .concat([ 'application/octet-stream' ]) // MacOS sends application/octet-stream | 26 | const videoTorrentImportRegex = [ ...Object.keys(MIMETYPES.TORRENT.MIMETYPE_EXT), 'application/octet-stream' ] |
27 | .map(m => `(${m})`) | 27 | .map(m => `(${m})`) |
28 | .join('|') | 28 | .join('|') |
29 | |||
29 | function isVideoImportTorrentFile (files: UploadFilesForCheck) { | 30 | function isVideoImportTorrentFile (files: UploadFilesForCheck) { |
30 | return isFileValid({ | 31 | return isFileValid({ |
31 | files, | 32 | files, |
diff --git a/server/helpers/decache.ts b/server/helpers/decache.ts index e31973b7a..08ab545e4 100644 --- a/server/helpers/decache.ts +++ b/server/helpers/decache.ts | |||
@@ -68,7 +68,7 @@ function searchCache (moduleName: string, callback: (current: NodeModule) => voi | |||
68 | }; | 68 | }; |
69 | 69 | ||
70 | function removeCachedPath (pluginPath: string) { | 70 | function removeCachedPath (pluginPath: string) { |
71 | const pathCache = (module.constructor as any)._pathCache | 71 | const pathCache = (module.constructor as any)._pathCache as { [ id: string ]: string[] } |
72 | 72 | ||
73 | Object.keys(pathCache).forEach(function (cacheKey) { | 73 | Object.keys(pathCache).forEach(function (cacheKey) { |
74 | if (cacheKey.includes(pluginPath)) { | 74 | if (cacheKey.includes(pluginPath)) { |
diff --git a/server/helpers/memoize.ts b/server/helpers/memoize.ts new file mode 100644 index 000000000..aa20e7d73 --- /dev/null +++ b/server/helpers/memoize.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | import memoizee from 'memoizee' | ||
2 | |||
3 | export function Memoize (config?: memoizee.Options<any>) { | ||
4 | return function (_target, _key, descriptor: PropertyDescriptor) { | ||
5 | const oldFunction = descriptor.value | ||
6 | const newFunction = memoizee(oldFunction, config) | ||
7 | |||
8 | descriptor.value = function () { | ||
9 | return newFunction.apply(this, arguments) | ||
10 | } | ||
11 | } | ||
12 | } | ||
diff --git a/server/helpers/youtube-dl/youtube-dl-cli.ts b/server/helpers/youtube-dl/youtube-dl-cli.ts index a2f630953..765038cea 100644 --- a/server/helpers/youtube-dl/youtube-dl-cli.ts +++ b/server/helpers/youtube-dl/youtube-dl-cli.ts | |||
@@ -6,6 +6,7 @@ import { VideoResolution } from '@shared/models' | |||
6 | import { logger, loggerTagsFactory } from '../logger' | 6 | import { logger, loggerTagsFactory } from '../logger' |
7 | import { getProxy, isProxyEnabled } from '../proxy' | 7 | import { getProxy, isProxyEnabled } from '../proxy' |
8 | import { isBinaryResponse, peertubeGot } from '../requests' | 8 | import { isBinaryResponse, peertubeGot } from '../requests' |
9 | import { OptionsOfBufferResponseBody } from 'got/dist/source' | ||
9 | 10 | ||
10 | const lTags = loggerTagsFactory('youtube-dl') | 11 | const lTags = loggerTagsFactory('youtube-dl') |
11 | 12 | ||
@@ -28,7 +29,16 @@ export class YoutubeDLCLI { | |||
28 | 29 | ||
29 | logger.info('Updating youtubeDL binary from %s.', url, lTags()) | 30 | logger.info('Updating youtubeDL binary from %s.', url, lTags()) |
30 | 31 | ||
31 | const gotOptions = { context: { bodyKBLimit: 20_000 }, responseType: 'buffer' as 'buffer' } | 32 | const gotOptions: OptionsOfBufferResponseBody = { |
33 | context: { bodyKBLimit: 20_000 }, | ||
34 | responseType: 'buffer' as 'buffer' | ||
35 | } | ||
36 | |||
37 | if (process.env.YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN) { | ||
38 | gotOptions.headers = { | ||
39 | authorization: 'Bearer ' + process.env.YOUTUBE_DL_DOWNLOAD_BEARER_TOKEN | ||
40 | } | ||
41 | } | ||
32 | 42 | ||
33 | try { | 43 | try { |
34 | let gotResult = await peertubeGot(url, gotOptions) | 44 | let gotResult = await peertubeGot(url, gotOptions) |
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts index c83fef425..dc46b5126 100644 --- a/server/initializers/checker-after-init.ts +++ b/server/initializers/checker-after-init.ts | |||
@@ -174,7 +174,8 @@ function checkRemoteRedundancyConfig () { | |||
174 | function checkStorageConfig () { | 174 | function checkStorageConfig () { |
175 | // Check storage directory locations | 175 | // Check storage directory locations |
176 | if (isProdInstance()) { | 176 | if (isProdInstance()) { |
177 | const configStorage = config.get('storage') | 177 | const configStorage = config.get<{ [ name: string ]: string }>('storage') |
178 | |||
178 | for (const key of Object.keys(configStorage)) { | 179 | for (const key of Object.keys(configStorage)) { |
179 | if (configStorage[key].startsWith('storage/')) { | 180 | if (configStorage[key].startsWith('storage/')) { |
180 | logger.warn( | 181 | logger.warn( |
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 39713a266..57852241c 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts | |||
@@ -13,6 +13,7 @@ function checkMissedConfig () { | |||
13 | 'webserver.https', 'webserver.hostname', 'webserver.port', | 13 | 'webserver.https', 'webserver.hostname', 'webserver.port', |
14 | 'secrets.peertube', | 14 | 'secrets.peertube', |
15 | 'trust_proxy', | 15 | 'trust_proxy', |
16 | 'oauth2.token_lifetime.access_token', 'oauth2.token_lifetime.refresh_token', | ||
16 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', | 17 | 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', |
17 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', | 18 | 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', |
18 | 'email.body.signature', 'email.subject.prefix', | 19 | 'email.body.signature', 'email.subject.prefix', |
diff --git a/server/initializers/config.ts b/server/initializers/config.ts index c2f8b19fd..28aaf36a9 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts | |||
@@ -149,6 +149,12 @@ const CONFIG = { | |||
149 | HOSTNAME: config.get<string>('webserver.hostname'), | 149 | HOSTNAME: config.get<string>('webserver.hostname'), |
150 | PORT: config.get<number>('webserver.port') | 150 | PORT: config.get<number>('webserver.port') |
151 | }, | 151 | }, |
152 | OAUTH2: { | ||
153 | TOKEN_LIFETIME: { | ||
154 | ACCESS_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.access_token')), | ||
155 | REFRESH_TOKEN: parseDurationToMs(config.get<string>('oauth2.token_lifetime.refresh_token')) | ||
156 | } | ||
157 | }, | ||
152 | RATES_LIMIT: { | 158 | RATES_LIMIT: { |
153 | API: { | 159 | API: { |
154 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')), | 160 | WINDOW_MS: parseDurationToMs(config.get<string>('rates_limit.api.window')), |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 0e56f0c9f..0dab524d9 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -101,11 +101,6 @@ const SORTABLE_COLUMNS = { | |||
101 | VIDEO_REDUNDANCIES: [ 'name' ] | 101 | VIDEO_REDUNDANCIES: [ 'name' ] |
102 | } | 102 | } |
103 | 103 | ||
104 | const OAUTH_LIFETIME = { | ||
105 | ACCESS_TOKEN: 3600 * 24, // 1 day, for upload | ||
106 | REFRESH_TOKEN: 1209600 // 2 weeks | ||
107 | } | ||
108 | |||
109 | const ROUTE_CACHE_LIFETIME = { | 104 | const ROUTE_CACHE_LIFETIME = { |
110 | FEEDS: '15 minutes', | 105 | FEEDS: '15 minutes', |
111 | ROBOTS: '2 hours', | 106 | ROBOTS: '2 hours', |
@@ -781,6 +776,9 @@ const LRU_CACHE = { | |||
781 | VIDEO_TOKENS: { | 776 | VIDEO_TOKENS: { |
782 | MAX_SIZE: 100_000, | 777 | MAX_SIZE: 100_000, |
783 | TTL: parseDurationToMs('8 hours') | 778 | TTL: parseDurationToMs('8 hours') |
779 | }, | ||
780 | TRACKER_IPS: { | ||
781 | MAX_SIZE: 100_000 | ||
784 | } | 782 | } |
785 | } | 783 | } |
786 | 784 | ||
@@ -884,7 +882,7 @@ const TRACKER_RATE_LIMITS = { | |||
884 | INTERVAL: 60000 * 5, // 5 minutes | 882 | INTERVAL: 60000 * 5, // 5 minutes |
885 | ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval | 883 | ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval |
886 | ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval | 884 | ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval |
887 | BLOCK_IP_LIFETIME: 60000 * 3 // 3 minutes | 885 | BLOCK_IP_LIFETIME: parseDurationToMs('3 minutes') |
888 | } | 886 | } |
889 | 887 | ||
890 | const P2P_MEDIA_LOADER_PEER_VERSION = 2 | 888 | const P2P_MEDIA_LOADER_PEER_VERSION = 2 |
@@ -1030,7 +1028,6 @@ export { | |||
1030 | JOB_ATTEMPTS, | 1028 | JOB_ATTEMPTS, |
1031 | AP_CLEANER, | 1029 | AP_CLEANER, |
1032 | LAST_MIGRATION_VERSION, | 1030 | LAST_MIGRATION_VERSION, |
1033 | OAUTH_LIFETIME, | ||
1034 | CUSTOM_HTML_TAG_COMMENTS, | 1031 | CUSTOM_HTML_TAG_COMMENTS, |
1035 | STATS_TIMESERIE, | 1032 | STATS_TIMESERIE, |
1036 | BROADCAST_CONCURRENCY, | 1033 | BROADCAST_CONCURRENCY, |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index f5d8eedf1..f48f348a7 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -51,8 +51,7 @@ function removeCacheAndTmpDirectories () { | |||
51 | const tasks: Promise<any>[] = [] | 51 | const tasks: Promise<any>[] = [] |
52 | 52 | ||
53 | // Cache directories | 53 | // Cache directories |
54 | for (const key of Object.keys(cacheDirectories)) { | 54 | for (const dir of cacheDirectories) { |
55 | const dir = cacheDirectories[key] | ||
56 | tasks.push(removeDirectoryOrContent(dir)) | 55 | tasks.push(removeDirectoryOrContent(dir)) |
57 | } | 56 | } |
58 | 57 | ||
@@ -87,8 +86,7 @@ function createDirectoriesIfNotExist () { | |||
87 | } | 86 | } |
88 | 87 | ||
89 | // Cache directories | 88 | // Cache directories |
90 | for (const key of Object.keys(cacheDirectories)) { | 89 | for (const dir of cacheDirectories) { |
91 | const dir = cacheDirectories[key] | ||
92 | tasks.push(ensureDir(dir)) | 90 | tasks.push(ensureDir(dir)) |
93 | } | 91 | } |
94 | 92 | ||
diff --git a/server/lib/auth/external-auth.ts b/server/lib/auth/external-auth.ts index 053112801..bc5b74257 100644 --- a/server/lib/auth/external-auth.ts +++ b/server/lib/auth/external-auth.ts | |||
@@ -1,26 +1,35 @@ | |||
1 | 1 | ||
2 | import { isUserDisplayNameValid, isUserRoleValid, isUserUsernameValid } from '@server/helpers/custom-validators/users' | 2 | import { |
3 | isUserAdminFlagsValid, | ||
4 | isUserDisplayNameValid, | ||
5 | isUserRoleValid, | ||
6 | isUserUsernameValid, | ||
7 | isUserVideoQuotaDailyValid, | ||
8 | isUserVideoQuotaValid | ||
9 | } from '@server/helpers/custom-validators/users' | ||
3 | import { logger } from '@server/helpers/logger' | 10 | import { logger } from '@server/helpers/logger' |
4 | import { generateRandomString } from '@server/helpers/utils' | 11 | import { generateRandomString } from '@server/helpers/utils' |
5 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' | 12 | import { PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME } from '@server/initializers/constants' |
6 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 13 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
7 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' | 14 | import { OAuthTokenModel } from '@server/models/oauth/oauth-token' |
15 | import { MUser } from '@server/types/models' | ||
8 | import { | 16 | import { |
9 | RegisterServerAuthenticatedResult, | 17 | RegisterServerAuthenticatedResult, |
10 | RegisterServerAuthPassOptions, | 18 | RegisterServerAuthPassOptions, |
11 | RegisterServerExternalAuthenticatedResult | 19 | RegisterServerExternalAuthenticatedResult |
12 | } from '@server/types/plugins/register-server-auth.model' | 20 | } from '@server/types/plugins/register-server-auth.model' |
13 | import { UserRole } from '@shared/models' | 21 | import { UserAdminFlag, UserRole } from '@shared/models' |
22 | import { BypassLogin } from './oauth-model' | ||
23 | |||
24 | export type ExternalUser = | ||
25 | Pick<MUser, 'username' | 'email' | 'role' | 'adminFlags' | 'videoQuotaDaily' | 'videoQuota'> & | ||
26 | { displayName: string } | ||
14 | 27 | ||
15 | // Token is the key, expiration date is the value | 28 | // Token is the key, expiration date is the value |
16 | const authBypassTokens = new Map<string, { | 29 | const authBypassTokens = new Map<string, { |
17 | expires: Date | 30 | expires: Date |
18 | user: { | 31 | user: ExternalUser |
19 | username: string | 32 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] |
20 | email: string | ||
21 | displayName: string | ||
22 | role: UserRole | ||
23 | } | ||
24 | authName: string | 33 | authName: string |
25 | npmName: string | 34 | npmName: string |
26 | }>() | 35 | }>() |
@@ -56,7 +65,8 @@ async function onExternalUserAuthenticated (options: { | |||
56 | expires, | 65 | expires, |
57 | user, | 66 | user, |
58 | npmName, | 67 | npmName, |
59 | authName | 68 | authName, |
69 | userUpdater: authResult.userUpdater | ||
60 | }) | 70 | }) |
61 | 71 | ||
62 | // Cleanup expired tokens | 72 | // Cleanup expired tokens |
@@ -78,7 +88,7 @@ async function getAuthNameFromRefreshGrant (refreshToken?: string) { | |||
78 | return tokenModel?.authName | 88 | return tokenModel?.authName |
79 | } | 89 | } |
80 | 90 | ||
81 | async function getBypassFromPasswordGrant (username: string, password: string) { | 91 | async function getBypassFromPasswordGrant (username: string, password: string): Promise<BypassLogin> { |
82 | const plugins = PluginManager.Instance.getIdAndPassAuths() | 92 | const plugins = PluginManager.Instance.getIdAndPassAuths() |
83 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] | 93 | const pluginAuths: { npmName?: string, registerAuthOptions: RegisterServerAuthPassOptions }[] = [] |
84 | 94 | ||
@@ -133,7 +143,8 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
133 | bypass: true, | 143 | bypass: true, |
134 | pluginName: pluginAuth.npmName, | 144 | pluginName: pluginAuth.npmName, |
135 | authName: authOptions.authName, | 145 | authName: authOptions.authName, |
136 | user: buildUserResult(loginResult) | 146 | user: buildUserResult(loginResult), |
147 | userUpdater: loginResult.userUpdater | ||
137 | } | 148 | } |
138 | } catch (err) { | 149 | } catch (err) { |
139 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) | 150 | logger.error('Error in auth method %s of plugin %s', authOptions.authName, pluginAuth.npmName, { err }) |
@@ -143,7 +154,7 @@ async function getBypassFromPasswordGrant (username: string, password: string) { | |||
143 | return undefined | 154 | return undefined |
144 | } | 155 | } |
145 | 156 | ||
146 | function getBypassFromExternalAuth (username: string, externalAuthToken: string) { | 157 | function getBypassFromExternalAuth (username: string, externalAuthToken: string): BypassLogin { |
147 | const obj = authBypassTokens.get(externalAuthToken) | 158 | const obj = authBypassTokens.get(externalAuthToken) |
148 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') | 159 | if (!obj) throw new Error('Cannot authenticate user with unknown bypass token') |
149 | 160 | ||
@@ -167,33 +178,29 @@ function getBypassFromExternalAuth (username: string, externalAuthToken: string) | |||
167 | bypass: true, | 178 | bypass: true, |
168 | pluginName: npmName, | 179 | pluginName: npmName, |
169 | authName, | 180 | authName, |
181 | userUpdater: obj.userUpdater, | ||
170 | user | 182 | user |
171 | } | 183 | } |
172 | } | 184 | } |
173 | 185 | ||
174 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { | 186 | function isAuthResultValid (npmName: string, authName: string, result: RegisterServerAuthenticatedResult) { |
175 | if (!isUserUsernameValid(result.username)) { | 187 | const returnError = (field: string) => { |
176 | logger.error('Auth method %s of plugin %s did not provide a valid username.', authName, npmName, { username: result.username }) | 188 | logger.error('Auth method %s of plugin %s did not provide a valid %s.', authName, npmName, field, { [field]: result[field] }) |
177 | return false | 189 | return false |
178 | } | 190 | } |
179 | 191 | ||
180 | if (!result.email) { | 192 | if (!isUserUsernameValid(result.username)) return returnError('username') |
181 | logger.error('Auth method %s of plugin %s did not provide a valid email.', authName, npmName, { email: result.email }) | 193 | if (!result.email) return returnError('email') |
182 | return false | ||
183 | } | ||
184 | 194 | ||
185 | // role is optional | 195 | // Following fields are optional |
186 | if (result.role && !isUserRoleValid(result.role)) { | 196 | if (result.role && !isUserRoleValid(result.role)) return returnError('role') |
187 | logger.error('Auth method %s of plugin %s did not provide a valid role.', authName, npmName, { role: result.role }) | 197 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) return returnError('displayName') |
188 | return false | 198 | if (result.adminFlags && !isUserAdminFlagsValid(result.adminFlags)) return returnError('adminFlags') |
189 | } | 199 | if (result.videoQuota && !isUserVideoQuotaValid(result.videoQuota + '')) return returnError('videoQuota') |
200 | if (result.videoQuotaDaily && !isUserVideoQuotaDailyValid(result.videoQuotaDaily + '')) return returnError('videoQuotaDaily') | ||
190 | 201 | ||
191 | // display name is optional | 202 | if (result.userUpdater && typeof result.userUpdater !== 'function') { |
192 | if (result.displayName && !isUserDisplayNameValid(result.displayName)) { | 203 | logger.error('Auth method %s of plugin %s did not provide a valid user updater function.', authName, npmName) |
193 | logger.error( | ||
194 | 'Auth method %s of plugin %s did not provide a valid display name.', | ||
195 | authName, npmName, { displayName: result.displayName } | ||
196 | ) | ||
197 | return false | 204 | return false |
198 | } | 205 | } |
199 | 206 | ||
@@ -205,7 +212,12 @@ function buildUserResult (pluginResult: RegisterServerAuthenticatedResult) { | |||
205 | username: pluginResult.username, | 212 | username: pluginResult.username, |
206 | email: pluginResult.email, | 213 | email: pluginResult.email, |
207 | role: pluginResult.role ?? UserRole.USER, | 214 | role: pluginResult.role ?? UserRole.USER, |
208 | displayName: pluginResult.displayName || pluginResult.username | 215 | displayName: pluginResult.displayName || pluginResult.username, |
216 | |||
217 | adminFlags: pluginResult.adminFlags ?? UserAdminFlag.NONE, | ||
218 | |||
219 | videoQuota: pluginResult.videoQuota, | ||
220 | videoQuotaDaily: pluginResult.videoQuotaDaily | ||
209 | } | 221 | } |
210 | } | 222 | } |
211 | 223 | ||
diff --git a/server/lib/auth/oauth-model.ts b/server/lib/auth/oauth-model.ts index 322b69e3a..43909284f 100644 --- a/server/lib/auth/oauth-model.ts +++ b/server/lib/auth/oauth-model.ts | |||
@@ -1,11 +1,13 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' | 2 | import { AccessDeniedError } from '@node-oauth/oauth2-server' |
3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' | 3 | import { PluginManager } from '@server/lib/plugins/plugin-manager' |
4 | import { AccountModel } from '@server/models/account/account' | ||
5 | import { AuthenticatedResultUpdaterFieldName, RegisterServerAuthenticatedResult } from '@server/types' | ||
4 | import { MOAuthClient } from '@server/types/models' | 6 | import { MOAuthClient } from '@server/types/models' |
5 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' | 7 | import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token' |
6 | import { MUser } from '@server/types/models/user/user' | 8 | import { MUser, MUserDefault } from '@server/types/models/user/user' |
7 | import { pick } from '@shared/core-utils' | 9 | import { pick } from '@shared/core-utils' |
8 | import { UserRole } from '@shared/models/users/user-role' | 10 | import { AttributesOnly } from '@shared/typescript-utils' |
9 | import { logger } from '../../helpers/logger' | 11 | import { logger } from '../../helpers/logger' |
10 | import { CONFIG } from '../../initializers/config' | 12 | import { CONFIG } from '../../initializers/config' |
11 | import { OAuthClientModel } from '../../models/oauth/oauth-client' | 13 | import { OAuthClientModel } from '../../models/oauth/oauth-client' |
@@ -13,6 +15,7 @@ import { OAuthTokenModel } from '../../models/oauth/oauth-token' | |||
13 | import { UserModel } from '../../models/user/user' | 15 | import { UserModel } from '../../models/user/user' |
14 | import { findAvailableLocalActorName } from '../local-actor' | 16 | import { findAvailableLocalActorName } from '../local-actor' |
15 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' | 17 | import { buildUser, createUserAccountAndChannelAndPlaylist } from '../user' |
18 | import { ExternalUser } from './external-auth' | ||
16 | import { TokensCache } from './tokens-cache' | 19 | import { TokensCache } from './tokens-cache' |
17 | 20 | ||
18 | type TokenInfo = { | 21 | type TokenInfo = { |
@@ -26,12 +29,8 @@ export type BypassLogin = { | |||
26 | bypass: boolean | 29 | bypass: boolean |
27 | pluginName: string | 30 | pluginName: string |
28 | authName?: string | 31 | authName?: string |
29 | user: { | 32 | user: ExternalUser |
30 | username: string | 33 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] |
31 | email: string | ||
32 | displayName: string | ||
33 | role: UserRole | ||
34 | } | ||
35 | } | 34 | } |
36 | 35 | ||
37 | async function getAccessToken (bearerToken: string) { | 36 | async function getAccessToken (bearerToken: string) { |
@@ -89,7 +88,9 @@ async function getUser (usernameOrEmail?: string, password?: string, bypassLogin | |||
89 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) | 88 | logger.info('Bypassing oauth login by plugin %s.', bypassLogin.pluginName) |
90 | 89 | ||
91 | let user = await UserModel.loadByEmail(bypassLogin.user.email) | 90 | let user = await UserModel.loadByEmail(bypassLogin.user.email) |
91 | |||
92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) | 92 | if (!user) user = await createUserFromExternal(bypassLogin.pluginName, bypassLogin.user) |
93 | else user = await updateUserFromExternal(user, bypassLogin.user, bypassLogin.userUpdater) | ||
93 | 94 | ||
94 | // Cannot create a user | 95 | // Cannot create a user |
95 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') | 96 | if (!user) throw new AccessDeniedError('Cannot create such user: an actor with that name already exists.') |
@@ -219,16 +220,11 @@ export { | |||
219 | 220 | ||
220 | // --------------------------------------------------------------------------- | 221 | // --------------------------------------------------------------------------- |
221 | 222 | ||
222 | async function createUserFromExternal (pluginAuth: string, options: { | 223 | async function createUserFromExternal (pluginAuth: string, userOptions: ExternalUser) { |
223 | username: string | 224 | const username = await findAvailableLocalActorName(userOptions.username) |
224 | email: string | ||
225 | role: UserRole | ||
226 | displayName: string | ||
227 | }) { | ||
228 | const username = await findAvailableLocalActorName(options.username) | ||
229 | 225 | ||
230 | const userToCreate = buildUser({ | 226 | const userToCreate = buildUser({ |
231 | ...pick(options, [ 'email', 'role' ]), | 227 | ...pick(userOptions, [ 'email', 'role', 'adminFlags', 'videoQuota', 'videoQuotaDaily' ]), |
232 | 228 | ||
233 | username, | 229 | username, |
234 | emailVerified: null, | 230 | emailVerified: null, |
@@ -238,12 +234,57 @@ async function createUserFromExternal (pluginAuth: string, options: { | |||
238 | 234 | ||
239 | const { user } = await createUserAccountAndChannelAndPlaylist({ | 235 | const { user } = await createUserAccountAndChannelAndPlaylist({ |
240 | userToCreate, | 236 | userToCreate, |
241 | userDisplayName: options.displayName | 237 | userDisplayName: userOptions.displayName |
242 | }) | 238 | }) |
243 | 239 | ||
244 | return user | 240 | return user |
245 | } | 241 | } |
246 | 242 | ||
243 | async function updateUserFromExternal ( | ||
244 | user: MUserDefault, | ||
245 | userOptions: ExternalUser, | ||
246 | userUpdater: RegisterServerAuthenticatedResult['userUpdater'] | ||
247 | ) { | ||
248 | if (!userUpdater) return user | ||
249 | |||
250 | { | ||
251 | type UserAttributeKeys = keyof AttributesOnly<UserModel> | ||
252 | const mappingKeys: { [ id in UserAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { | ||
253 | role: 'role', | ||
254 | adminFlags: 'adminFlags', | ||
255 | videoQuota: 'videoQuota', | ||
256 | videoQuotaDaily: 'videoQuotaDaily' | ||
257 | } | ||
258 | |||
259 | for (const modelKey of Object.keys(mappingKeys)) { | ||
260 | const pluginOptionKey = mappingKeys[modelKey] | ||
261 | |||
262 | const newValue = userUpdater({ fieldName: pluginOptionKey, currentValue: user[modelKey], newValue: userOptions[pluginOptionKey] }) | ||
263 | user.set(modelKey, newValue) | ||
264 | } | ||
265 | } | ||
266 | |||
267 | { | ||
268 | type AccountAttributeKeys = keyof Partial<AttributesOnly<AccountModel>> | ||
269 | const mappingKeys: { [ id in AccountAttributeKeys ]?: AuthenticatedResultUpdaterFieldName } = { | ||
270 | name: 'displayName' | ||
271 | } | ||
272 | |||
273 | for (const modelKey of Object.keys(mappingKeys)) { | ||
274 | const optionKey = mappingKeys[modelKey] | ||
275 | |||
276 | const newValue = userUpdater({ fieldName: optionKey, currentValue: user.Account[modelKey], newValue: userOptions[optionKey] }) | ||
277 | user.Account.set(modelKey, newValue) | ||
278 | } | ||
279 | } | ||
280 | |||
281 | logger.debug('Updated user %s with plugin userUpdated function.', user.email, { user, userOptions }) | ||
282 | |||
283 | user.Account = await user.Account.save() | ||
284 | |||
285 | return user.save() | ||
286 | } | ||
287 | |||
247 | function checkUserValidityOrThrow (user: MUser) { | 288 | function checkUserValidityOrThrow (user: MUser) { |
248 | if (user.blocked) throw new AccessDeniedError('User is blocked.') | 289 | if (user.blocked) throw new AccessDeniedError('User is blocked.') |
249 | } | 290 | } |
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts index bc0d4301f..2905c79a2 100644 --- a/server/lib/auth/oauth.ts +++ b/server/lib/auth/oauth.ts | |||
@@ -10,10 +10,11 @@ import OAuth2Server, { | |||
10 | } from '@node-oauth/oauth2-server' | 10 | } from '@node-oauth/oauth2-server' |
11 | import { randomBytesPromise } from '@server/helpers/core-utils' | 11 | import { randomBytesPromise } from '@server/helpers/core-utils' |
12 | import { isOTPValid } from '@server/helpers/otp' | 12 | import { isOTPValid } from '@server/helpers/otp' |
13 | import { CONFIG } from '@server/initializers/config' | ||
13 | import { MOAuthClient } from '@server/types/models' | 14 | import { MOAuthClient } from '@server/types/models' |
14 | import { sha1 } from '@shared/extra-utils' | 15 | import { sha1 } from '@shared/extra-utils' |
15 | import { HttpStatusCode } from '@shared/models' | 16 | import { HttpStatusCode } from '@shared/models' |
16 | import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' | 17 | import { OTP } from '../../initializers/constants' |
17 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | 18 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' |
18 | 19 | ||
19 | class MissingTwoFactorError extends Error { | 20 | class MissingTwoFactorError extends Error { |
@@ -32,8 +33,9 @@ class InvalidTwoFactorError extends Error { | |||
32 | * | 33 | * |
33 | */ | 34 | */ |
34 | const oAuthServer = new OAuth2Server({ | 35 | const oAuthServer = new OAuth2Server({ |
35 | accessTokenLifetime: OAUTH_LIFETIME.ACCESS_TOKEN, | 36 | // Wants seconds |
36 | refreshTokenLifetime: OAUTH_LIFETIME.REFRESH_TOKEN, | 37 | accessTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN / 1000, |
38 | refreshTokenLifetime: CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN / 1000, | ||
37 | 39 | ||
38 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications | 40 | // See https://github.com/oauthjs/node-oauth2-server/wiki/Model-specification for the model specifications |
39 | model: require('./oauth-model') | 41 | model: require('./oauth-model') |
@@ -182,10 +184,10 @@ function generateRandomToken () { | |||
182 | 184 | ||
183 | function getTokenExpiresAt (type: 'access' | 'refresh') { | 185 | function getTokenExpiresAt (type: 'access' | 'refresh') { |
184 | const lifetime = type === 'access' | 186 | const lifetime = type === 'access' |
185 | ? OAUTH_LIFETIME.ACCESS_TOKEN | 187 | ? CONFIG.OAUTH2.TOKEN_LIFETIME.ACCESS_TOKEN |
186 | : OAUTH_LIFETIME.REFRESH_TOKEN | 188 | : CONFIG.OAUTH2.TOKEN_LIFETIME.REFRESH_TOKEN |
187 | 189 | ||
188 | return new Date(Date.now() + lifetime * 1000) | 190 | return new Date(Date.now() + lifetime) |
189 | } | 191 | } |
190 | 192 | ||
191 | async function buildToken () { | 193 | async function buildToken () { |
diff --git a/server/lib/auth/tokens-cache.ts b/server/lib/auth/tokens-cache.ts index 410708a35..43efc7d02 100644 --- a/server/lib/auth/tokens-cache.ts +++ b/server/lib/auth/tokens-cache.ts | |||
@@ -36,8 +36,8 @@ export class TokensCache { | |||
36 | const token = this.userHavingToken.get(userId) | 36 | const token = this.userHavingToken.get(userId) |
37 | 37 | ||
38 | if (token !== undefined) { | 38 | if (token !== undefined) { |
39 | this.accessTokenCache.del(token) | 39 | this.accessTokenCache.delete(token) |
40 | this.userHavingToken.del(userId) | 40 | this.userHavingToken.delete(userId) |
41 | } | 41 | } |
42 | } | 42 | } |
43 | 43 | ||
@@ -45,8 +45,8 @@ export class TokensCache { | |||
45 | const tokenModel = this.accessTokenCache.get(token) | 45 | const tokenModel = this.accessTokenCache.get(token) |
46 | 46 | ||
47 | if (tokenModel !== undefined) { | 47 | if (tokenModel !== undefined) { |
48 | this.userHavingToken.del(tokenModel.userId) | 48 | this.userHavingToken.delete(tokenModel.userId) |
49 | this.accessTokenCache.del(token) | 49 | this.accessTokenCache.delete(token) |
50 | } | 50 | } |
51 | } | 51 | } |
52 | } | 52 | } |
diff --git a/server/lib/job-queue/job-queue.ts b/server/lib/job-queue/job-queue.ts index 866aa1ed0..8597eb000 100644 --- a/server/lib/job-queue/job-queue.ts +++ b/server/lib/job-queue/job-queue.ts | |||
@@ -184,7 +184,7 @@ class JobQueue { | |||
184 | 184 | ||
185 | this.jobRedisPrefix = 'bull-' + WEBSERVER.HOST | 185 | this.jobRedisPrefix = 'bull-' + WEBSERVER.HOST |
186 | 186 | ||
187 | for (const handlerName of (Object.keys(handlers) as JobType[])) { | 187 | for (const handlerName of Object.keys(handlers)) { |
188 | this.buildWorker(handlerName) | 188 | this.buildWorker(handlerName) |
189 | this.buildQueue(handlerName) | 189 | this.buildQueue(handlerName) |
190 | this.buildQueueScheduler(handlerName) | 190 | this.buildQueueScheduler(handlerName) |
diff --git a/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts b/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts new file mode 100644 index 000000000..ef40c0fa9 --- /dev/null +++ b/server/lib/opentelemetry/metric-helpers/bittorrent-tracker-observers-builder.ts | |||
@@ -0,0 +1,51 @@ | |||
1 | import { Meter } from '@opentelemetry/api' | ||
2 | |||
3 | export class BittorrentTrackerObserversBuilder { | ||
4 | |||
5 | constructor (private readonly meter: Meter, private readonly trackerServer: any) { | ||
6 | |||
7 | } | ||
8 | |||
9 | buildObservers () { | ||
10 | const activeInfohashes = this.meter.createObservableGauge('peertube_bittorrent_tracker_active_infohashes_total', { | ||
11 | description: 'Total active infohashes in the PeerTube BitTorrent Tracker' | ||
12 | }) | ||
13 | const inactiveInfohashes = this.meter.createObservableGauge('peertube_bittorrent_tracker_inactive_infohashes_total', { | ||
14 | description: 'Total inactive infohashes in the PeerTube BitTorrent Tracker' | ||
15 | }) | ||
16 | const peers = this.meter.createObservableGauge('peertube_bittorrent_tracker_peers_total', { | ||
17 | description: 'Total peers in the PeerTube BitTorrent Tracker' | ||
18 | }) | ||
19 | |||
20 | this.meter.addBatchObservableCallback(observableResult => { | ||
21 | const infohashes = Object.keys(this.trackerServer.torrents) | ||
22 | |||
23 | const counters = { | ||
24 | activeInfohashes: 0, | ||
25 | inactiveInfohashes: 0, | ||
26 | peers: 0, | ||
27 | uncompletedPeers: 0 | ||
28 | } | ||
29 | |||
30 | for (const infohash of infohashes) { | ||
31 | const content = this.trackerServer.torrents[infohash] | ||
32 | |||
33 | const peers = content.peers | ||
34 | if (peers.keys.length !== 0) counters.activeInfohashes++ | ||
35 | else counters.inactiveInfohashes++ | ||
36 | |||
37 | for (const peerId of peers.keys) { | ||
38 | const peer = peers.peek(peerId) | ||
39 | if (peer == null) return | ||
40 | |||
41 | counters.peers++ | ||
42 | } | ||
43 | } | ||
44 | |||
45 | observableResult.observe(activeInfohashes, counters.activeInfohashes) | ||
46 | observableResult.observe(inactiveInfohashes, counters.inactiveInfohashes) | ||
47 | observableResult.observe(peers, counters.peers) | ||
48 | }, [ activeInfohashes, inactiveInfohashes, peers ]) | ||
49 | } | ||
50 | |||
51 | } | ||
diff --git a/server/lib/opentelemetry/metric-helpers/index.ts b/server/lib/opentelemetry/metric-helpers/index.ts index 775d954ba..47b24a54f 100644 --- a/server/lib/opentelemetry/metric-helpers/index.ts +++ b/server/lib/opentelemetry/metric-helpers/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | export * from './bittorrent-tracker-observers-builder' | ||
1 | export * from './lives-observers-builder' | 2 | export * from './lives-observers-builder' |
2 | export * from './job-queue-observers-builder' | 3 | export * from './job-queue-observers-builder' |
3 | export * from './nodejs-observers-builder' | 4 | export * from './nodejs-observers-builder' |
diff --git a/server/lib/opentelemetry/metrics.ts b/server/lib/opentelemetry/metrics.ts index 226d514c0..9cc067e4a 100644 --- a/server/lib/opentelemetry/metrics.ts +++ b/server/lib/opentelemetry/metrics.ts | |||
@@ -7,6 +7,7 @@ import { CONFIG } from '@server/initializers/config' | |||
7 | import { MVideoImmutable } from '@server/types/models' | 7 | import { MVideoImmutable } from '@server/types/models' |
8 | import { PlaybackMetricCreate } from '@shared/models' | 8 | import { PlaybackMetricCreate } from '@shared/models' |
9 | import { | 9 | import { |
10 | BittorrentTrackerObserversBuilder, | ||
10 | JobQueueObserversBuilder, | 11 | JobQueueObserversBuilder, |
11 | LivesObserversBuilder, | 12 | LivesObserversBuilder, |
12 | NodeJSObserversBuilder, | 13 | NodeJSObserversBuilder, |
@@ -41,7 +42,7 @@ class OpenTelemetryMetrics { | |||
41 | }) | 42 | }) |
42 | } | 43 | } |
43 | 44 | ||
44 | registerMetrics () { | 45 | registerMetrics (options: { trackerServer: any }) { |
45 | if (CONFIG.OPEN_TELEMETRY.METRICS.ENABLED !== true) return | 46 | if (CONFIG.OPEN_TELEMETRY.METRICS.ENABLED !== true) return |
46 | 47 | ||
47 | logger.info('Registering Open Telemetry metrics') | 48 | logger.info('Registering Open Telemetry metrics') |
@@ -80,6 +81,9 @@ class OpenTelemetryMetrics { | |||
80 | 81 | ||
81 | const viewersObserversBuilder = new ViewersObserversBuilder(this.meter) | 82 | const viewersObserversBuilder = new ViewersObserversBuilder(this.meter) |
82 | viewersObserversBuilder.buildObservers() | 83 | viewersObserversBuilder.buildObservers() |
84 | |||
85 | const bittorrentTrackerObserversBuilder = new BittorrentTrackerObserversBuilder(this.meter, options.trackerServer) | ||
86 | bittorrentTrackerObserversBuilder.buildObservers() | ||
83 | } | 87 | } |
84 | 88 | ||
85 | observePlaybackMetric (video: MVideoImmutable, metrics: PlaybackMetricCreate) { | 89 | observePlaybackMetric (video: MVideoImmutable, metrics: PlaybackMetricCreate) { |
diff --git a/server/lib/plugins/plugin-helpers-builder.ts b/server/lib/plugins/plugin-helpers-builder.ts index 7b1def6e3..66383af46 100644 --- a/server/lib/plugins/plugin-helpers-builder.ts +++ b/server/lib/plugins/plugin-helpers-builder.ts | |||
@@ -209,6 +209,10 @@ function buildConfigHelpers () { | |||
209 | return WEBSERVER.URL | 209 | return WEBSERVER.URL |
210 | }, | 210 | }, |
211 | 211 | ||
212 | getServerListeningConfig () { | ||
213 | return { hostname: CONFIG.LISTEN.HOSTNAME, port: CONFIG.LISTEN.PORT } | ||
214 | }, | ||
215 | |||
212 | getServerConfig () { | 216 | getServerConfig () { |
213 | return ServerConfigManager.Instance.getServerConfig() | 217 | return ServerConfigManager.Instance.getServerConfig() |
214 | } | 218 | } |
@@ -245,7 +249,7 @@ function buildUserHelpers () { | |||
245 | }, | 249 | }, |
246 | 250 | ||
247 | getAuthUser: (res: express.Response) => { | 251 | getAuthUser: (res: express.Response) => { |
248 | const user = res.locals.oauth?.token?.User | 252 | const user = res.locals.oauth?.token?.User || res.locals.videoFileToken?.user |
249 | if (!user) return undefined | 253 | if (!user) return undefined |
250 | 254 | ||
251 | return UserModel.loadByIdFull(user.id) | 255 | return UserModel.loadByIdFull(user.id) |
diff --git a/server/lib/redis.ts b/server/lib/redis.ts index c0e9aece7..451ddd0b6 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts | |||
@@ -8,7 +8,6 @@ import { | |||
8 | AP_CLEANER, | 8 | AP_CLEANER, |
9 | CONTACT_FORM_LIFETIME, | 9 | CONTACT_FORM_LIFETIME, |
10 | RESUMABLE_UPLOAD_SESSION_LIFETIME, | 10 | RESUMABLE_UPLOAD_SESSION_LIFETIME, |
11 | TRACKER_RATE_LIMITS, | ||
12 | TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME, | 11 | TWO_FACTOR_AUTH_REQUEST_TOKEN_LIFETIME, |
13 | USER_EMAIL_VERIFY_LIFETIME, | 12 | USER_EMAIL_VERIFY_LIFETIME, |
14 | USER_PASSWORD_CREATE_LIFETIME, | 13 | USER_PASSWORD_CREATE_LIFETIME, |
@@ -157,16 +156,6 @@ class Redis { | |||
157 | return this.exists(this.generateIPViewKey(ip, videoUUID)) | 156 | return this.exists(this.generateIPViewKey(ip, videoUUID)) |
158 | } | 157 | } |
159 | 158 | ||
160 | /* ************ Tracker IP block ************ */ | ||
161 | |||
162 | setTrackerBlockIP (ip: string) { | ||
163 | return this.setValue(this.generateTrackerBlockIPKey(ip), '1', TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME) | ||
164 | } | ||
165 | |||
166 | async doesTrackerBlockIPExist (ip: string) { | ||
167 | return this.exists(this.generateTrackerBlockIPKey(ip)) | ||
168 | } | ||
169 | |||
170 | /* ************ Video views stats ************ */ | 159 | /* ************ Video views stats ************ */ |
171 | 160 | ||
172 | addVideoViewStats (videoId: number) { | 161 | addVideoViewStats (videoId: number) { |
@@ -365,10 +354,6 @@ class Redis { | |||
365 | return `views-${videoUUID}-${ip}` | 354 | return `views-${videoUUID}-${ip}` |
366 | } | 355 | } |
367 | 356 | ||
368 | private generateTrackerBlockIPKey (ip: string) { | ||
369 | return `tracker-block-ip-${ip}` | ||
370 | } | ||
371 | |||
372 | private generateContactFormKey (ip: string) { | 357 | private generateContactFormKey (ip: string) { |
373 | return 'contact-form-' + ip | 358 | return 'contact-form-' + ip |
374 | } | 359 | } |
diff --git a/server/lib/sync-channel.ts b/server/lib/sync-channel.ts index 10167ee38..3a805a943 100644 --- a/server/lib/sync-channel.ts +++ b/server/lib/sync-channel.ts | |||
@@ -76,7 +76,7 @@ export async function synchronizeChannel (options: { | |||
76 | 76 | ||
77 | await JobQueue.Instance.createJobWithChildren(parent, children) | 77 | await JobQueue.Instance.createJobWithChildren(parent, children) |
78 | } catch (err) { | 78 | } catch (err) { |
79 | logger.error(`Failed to import channel ${channel.name}`, { err }) | 79 | logger.error(`Failed to import ${externalChannelUrl} in channel ${channel.name}`, { err }) |
80 | channelSync.state = VideoChannelSyncState.FAILED | 80 | channelSync.state = VideoChannelSyncState.FAILED |
81 | await channelSync.save() | 81 | await channelSync.save() |
82 | } | 82 | } |
diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts index 02f160fe8..6eb865f7f 100644 --- a/server/lib/video-comment.ts +++ b/server/lib/video-comment.ts | |||
@@ -1,30 +1,41 @@ | |||
1 | import express from 'express' | ||
1 | import { cloneDeep } from 'lodash' | 2 | import { cloneDeep } from 'lodash' |
2 | import * as Sequelize from 'sequelize' | 3 | import * as Sequelize from 'sequelize' |
3 | import express from 'express' | ||
4 | import { logger } from '@server/helpers/logger' | 4 | import { logger } from '@server/helpers/logger' |
5 | import { sequelizeTypescript } from '@server/initializers/database' | 5 | import { sequelizeTypescript } from '@server/initializers/database' |
6 | import { ResultList } from '../../shared/models' | 6 | import { ResultList } from '../../shared/models' |
7 | import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model' | 7 | import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model' |
8 | import { VideoCommentModel } from '../models/video/video-comment' | 8 | import { VideoCommentModel } from '../models/video/video-comment' |
9 | import { MAccountDefault, MComment, MCommentOwnerVideo, MCommentOwnerVideoReply, MVideoFullLight } from '../types/models' | 9 | import { |
10 | MAccountDefault, | ||
11 | MComment, | ||
12 | MCommentFormattable, | ||
13 | MCommentOwnerVideo, | ||
14 | MCommentOwnerVideoReply, | ||
15 | MVideoFullLight | ||
16 | } from '../types/models' | ||
10 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' | 17 | import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send' |
11 | import { getLocalVideoCommentActivityPubUrl } from './activitypub/url' | 18 | import { getLocalVideoCommentActivityPubUrl } from './activitypub/url' |
12 | import { Hooks } from './plugins/hooks' | 19 | import { Hooks } from './plugins/hooks' |
13 | 20 | ||
14 | async function removeComment (videoCommentInstance: MCommentOwnerVideo, req: express.Request, res: express.Response) { | 21 | async function removeComment (commentArg: MComment, req: express.Request, res: express.Response) { |
15 | const videoCommentInstanceBefore = cloneDeep(videoCommentInstance) | 22 | let videoCommentInstanceBefore: MCommentOwnerVideo |
16 | 23 | ||
17 | await sequelizeTypescript.transaction(async t => { | 24 | await sequelizeTypescript.transaction(async t => { |
18 | if (videoCommentInstance.isOwned() || videoCommentInstance.Video.isOwned()) { | 25 | const comment = await VideoCommentModel.loadByUrlAndPopulateAccountAndVideo(commentArg.url, t) |
19 | await sendDeleteVideoComment(videoCommentInstance, t) | 26 | |
27 | videoCommentInstanceBefore = cloneDeep(comment) | ||
28 | |||
29 | if (comment.isOwned() || comment.Video.isOwned()) { | ||
30 | await sendDeleteVideoComment(comment, t) | ||
20 | } | 31 | } |
21 | 32 | ||
22 | videoCommentInstance.markAsDeleted() | 33 | comment.markAsDeleted() |
23 | 34 | ||
24 | await videoCommentInstance.save({ transaction: t }) | 35 | await comment.save({ transaction: t }) |
25 | }) | ||
26 | 36 | ||
27 | logger.info('Video comment %d deleted.', videoCommentInstance.id) | 37 | logger.info('Video comment %d deleted.', comment.id) |
38 | }) | ||
28 | 39 | ||
29 | Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore, req, res }) | 40 | Hooks.runAction('action:api.video-comment.deleted', { comment: videoCommentInstanceBefore, req, res }) |
30 | } | 41 | } |
@@ -64,7 +75,7 @@ async function createVideoComment (obj: { | |||
64 | return savedComment | 75 | return savedComment |
65 | } | 76 | } |
66 | 77 | ||
67 | function buildFormattedCommentTree (resultList: ResultList<VideoCommentModel>): VideoCommentThreadTree { | 78 | function buildFormattedCommentTree (resultList: ResultList<MCommentFormattable>): VideoCommentThreadTree { |
68 | // Comments are sorted by id ASC | 79 | // Comments are sorted by id ASC |
69 | const comments = resultList.data | 80 | const comments = resultList.data |
70 | 81 | ||
diff --git a/server/lib/video-tokens-manager.ts b/server/lib/video-tokens-manager.ts index c43085d16..17aa29cdd 100644 --- a/server/lib/video-tokens-manager.ts +++ b/server/lib/video-tokens-manager.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import LRUCache from 'lru-cache' | 1 | import LRUCache from 'lru-cache' |
2 | import { LRU_CACHE } from '@server/initializers/constants' | 2 | import { LRU_CACHE } from '@server/initializers/constants' |
3 | import { MUserAccountUrl } from '@server/types/models' | ||
4 | import { pick } from '@shared/core-utils' | ||
3 | import { buildUUID } from '@shared/extra-utils' | 5 | import { buildUUID } from '@shared/extra-utils' |
4 | 6 | ||
5 | // --------------------------------------------------------------------------- | 7 | // --------------------------------------------------------------------------- |
@@ -10,19 +12,22 @@ class VideoTokensManager { | |||
10 | 12 | ||
11 | private static instance: VideoTokensManager | 13 | private static instance: VideoTokensManager |
12 | 14 | ||
13 | private readonly lruCache = new LRUCache<string, string>({ | 15 | private readonly lruCache = new LRUCache<string, { videoUUID: string, user: MUserAccountUrl }>({ |
14 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, | 16 | max: LRU_CACHE.VIDEO_TOKENS.MAX_SIZE, |
15 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL | 17 | ttl: LRU_CACHE.VIDEO_TOKENS.TTL |
16 | }) | 18 | }) |
17 | 19 | ||
18 | private constructor () {} | 20 | private constructor () {} |
19 | 21 | ||
20 | create (videoUUID: string) { | 22 | create (options: { |
23 | user: MUserAccountUrl | ||
24 | videoUUID: string | ||
25 | }) { | ||
21 | const token = buildUUID() | 26 | const token = buildUUID() |
22 | 27 | ||
23 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) | 28 | const expires = new Date(new Date().getTime() + LRU_CACHE.VIDEO_TOKENS.TTL) |
24 | 29 | ||
25 | this.lruCache.set(token, videoUUID) | 30 | this.lruCache.set(token, pick(options, [ 'user', 'videoUUID' ])) |
26 | 31 | ||
27 | return { token, expires } | 32 | return { token, expires } |
28 | } | 33 | } |
@@ -34,7 +39,16 @@ class VideoTokensManager { | |||
34 | const value = this.lruCache.get(options.token) | 39 | const value = this.lruCache.get(options.token) |
35 | if (!value) return false | 40 | if (!value) return false |
36 | 41 | ||
37 | return value === options.videoUUID | 42 | return value.videoUUID === options.videoUUID |
43 | } | ||
44 | |||
45 | getUserFromToken (options: { | ||
46 | token: string | ||
47 | }) { | ||
48 | const value = this.lruCache.get(options.token) | ||
49 | if (!value) return undefined | ||
50 | |||
51 | return value.user | ||
38 | } | 52 | } |
39 | 53 | ||
40 | static get Instance () { | 54 | static get Instance () { |
diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts index 458895898..77a532276 100644 --- a/server/middlewares/sort.ts +++ b/server/middlewares/sort.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { SortType } from '../models/utils' | ||
3 | 2 | ||
4 | const setDefaultSort = setDefaultSortFactory('-createdAt') | 3 | const setDefaultSort = setDefaultSortFactory('-createdAt') |
5 | const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') | 4 | const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') |
@@ -7,27 +6,7 @@ const setDefaultVideosSort = setDefaultSortFactory('-publishedAt') | |||
7 | const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name') | 6 | const setDefaultVideoRedundanciesSort = setDefaultSortFactory('name') |
8 | 7 | ||
9 | const setDefaultSearchSort = setDefaultSortFactory('-match') | 8 | const setDefaultSearchSort = setDefaultSortFactory('-match') |
10 | 9 | const setBlacklistSort = setDefaultSortFactory('-createdAt') | |
11 | function setBlacklistSort (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
12 | const newSort: SortType = { sortModel: undefined, sortValue: '' } | ||
13 | |||
14 | if (!req.query.sort) req.query.sort = '-createdAt' | ||
15 | |||
16 | // Set model we want to sort onto | ||
17 | if (req.query.sort === '-createdAt' || req.query.sort === 'createdAt' || | ||
18 | req.query.sort === '-id' || req.query.sort === 'id') { | ||
19 | // If we want to sort onto the BlacklistedVideos relation, we won't specify it in the query parameter... | ||
20 | newSort.sortModel = undefined | ||
21 | } else { | ||
22 | newSort.sortModel = 'Video' | ||
23 | } | ||
24 | |||
25 | newSort.sortValue = req.query.sort | ||
26 | |||
27 | req.query.sort = newSort | ||
28 | |||
29 | return next() | ||
30 | } | ||
31 | 10 | ||
32 | // --------------------------------------------------------------------------- | 11 | // --------------------------------------------------------------------------- |
33 | 12 | ||
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index ebbfc0a0a..0033a32ff 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -180,18 +180,16 @@ async function checkCanAccessVideoStaticFiles (options: { | |||
180 | return checkCanSeeVideo(options) | 180 | return checkCanSeeVideo(options) |
181 | } | 181 | } |
182 | 182 | ||
183 | if (!video.hasPrivateStaticPath()) return true | ||
184 | |||
185 | const videoFileToken = req.query.videoFileToken | 183 | const videoFileToken = req.query.videoFileToken |
186 | if (!videoFileToken) { | 184 | if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) { |
187 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 185 | const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken }) |
188 | return false | ||
189 | } | ||
190 | 186 | ||
191 | if (VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) { | 187 | res.locals.videoFileToken = { user } |
192 | return true | 188 | return true |
193 | } | 189 | } |
194 | 190 | ||
191 | if (!video.hasPrivateStaticPath()) return true | ||
192 | |||
195 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | 193 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) |
196 | return false | 194 | return false |
197 | } | 195 | } |
diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts index 20008768b..14a5bffa2 100644 --- a/server/models/abuse/abuse-message.ts +++ b/server/models/abuse/abuse-message.ts | |||
@@ -5,7 +5,7 @@ import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models' | |||
5 | import { AbuseMessage } from '@shared/models' | 5 | import { AbuseMessage } from '@shared/models' |
6 | import { AttributesOnly } from '@shared/typescript-utils' | 6 | import { AttributesOnly } from '@shared/typescript-utils' |
7 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 7 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' |
8 | import { getSort, throwIfNotValid } from '../utils' | 8 | import { getSort, throwIfNotValid } from '../shared' |
9 | import { AbuseModel } from './abuse' | 9 | import { AbuseModel } from './abuse' |
10 | 10 | ||
11 | @Table({ | 11 | @Table({ |
diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index 4c6a96a86..4ce40bf2f 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts | |||
@@ -34,13 +34,13 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' | 34 | import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' |
35 | import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' | 35 | import { MAbuseAdminFormattable, MAbuseAP, MAbuseFull, MAbuseReporter, MAbuseUserFormattable, MUserAccountId } from '../../types/models' |
36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' | 36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
37 | import { getSort, throwIfNotValid } from '../utils' | 37 | import { getSort, throwIfNotValid } from '../shared' |
38 | import { ThumbnailModel } from '../video/thumbnail' | 38 | import { ThumbnailModel } from '../video/thumbnail' |
39 | import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' | 39 | import { ScopeNames as VideoScopeNames, VideoModel } from '../video/video' |
40 | import { VideoBlacklistModel } from '../video/video-blacklist' | 40 | import { VideoBlacklistModel } from '../video/video-blacklist' |
41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' | 41 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions as ChannelSummaryOptions, VideoChannelModel } from '../video/video-channel' |
42 | import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment' | 42 | import { ScopeNames as CommentScopeNames, VideoCommentModel } from '../video/video-comment' |
43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './abuse-query-builder' | 43 | import { buildAbuseListQuery, BuildAbusesQueryOptions } from './sql/abuse-query-builder' |
44 | import { VideoAbuseModel } from './video-abuse' | 44 | import { VideoAbuseModel } from './video-abuse' |
45 | import { VideoCommentAbuseModel } from './video-comment-abuse' | 45 | import { VideoCommentAbuseModel } from './video-comment-abuse' |
46 | 46 | ||
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/sql/abuse-query-builder.ts index 74f4542e5..282d4541a 100644 --- a/server/models/abuse/abuse-query-builder.ts +++ b/server/models/abuse/sql/abuse-query-builder.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | import { exists } from '@server/helpers/custom-validators/misc' | 2 | import { exists } from '@server/helpers/custom-validators/misc' |
3 | import { forceNumber } from '@shared/core-utils' | 3 | import { forceNumber } from '@shared/core-utils' |
4 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' | 4 | import { AbuseFilter, AbuseState, AbuseVideoIs } from '@shared/models' |
5 | import { buildBlockedAccountSQL, buildDirectionAndField } from '../utils' | 5 | import { buildBlockedAccountSQL, buildSortDirectionAndField } from '../../shared' |
6 | 6 | ||
7 | export type BuildAbusesQueryOptions = { | 7 | export type BuildAbusesQueryOptions = { |
8 | start: number | 8 | start: number |
@@ -157,7 +157,7 @@ function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | | |||
157 | } | 157 | } |
158 | 158 | ||
159 | function buildAbuseOrder (value: string) { | 159 | function buildAbuseOrder (value: string) { |
160 | const { direction, field } = buildDirectionAndField(value) | 160 | const { direction, field } = buildSortDirectionAndField(value) |
161 | 161 | ||
162 | return `ORDER BY "abuse"."${field}" ${direction}` | 162 | return `ORDER BY "abuse"."${field}" ${direction}` |
163 | } | 163 | } |
diff --git a/server/models/account/account-blocklist.ts b/server/models/account/account-blocklist.ts index 377249b38..f6212ff6e 100644 --- a/server/models/account/account-blocklist.ts +++ b/server/models/account/account-blocklist.ts | |||
@@ -6,7 +6,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
6 | import { AccountBlock } from '../../../shared/models' | 6 | import { AccountBlock } from '../../../shared/models' |
7 | import { ActorModel } from '../actor/actor' | 7 | import { ActorModel } from '../actor/actor' |
8 | import { ServerModel } from '../server/server' | 8 | import { ServerModel } from '../server/server' |
9 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 9 | import { createSafeIn, getSort, searchAttribute } from '../shared' |
10 | import { AccountModel } from './account' | 10 | import { AccountModel } from './account' |
11 | 11 | ||
12 | @Table({ | 12 | @Table({ |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 7afc907da..9e7ef4394 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -11,7 +11,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 11 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
12 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' | 12 | import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants' |
13 | import { ActorModel } from '../actor/actor' | 13 | import { ActorModel } from '../actor/actor' |
14 | import { getSort, throwIfNotValid } from '../utils' | 14 | import { getSort, throwIfNotValid } from '../shared' |
15 | import { VideoModel } from '../video/video' | 15 | import { VideoModel } from '../video/video' |
16 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' | 16 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' |
17 | import { AccountModel } from './account' | 17 | import { AccountModel } from './account' |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 8a7dfba94..dc989417b 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -16,7 +16,7 @@ import { | |||
16 | Table, | 16 | Table, |
17 | UpdatedAt | 17 | UpdatedAt |
18 | } from 'sequelize-typescript' | 18 | } from 'sequelize-typescript' |
19 | import { ModelCache } from '@server/models/model-cache' | 19 | import { ModelCache } from '@server/models/shared/model-cache' |
20 | import { AttributesOnly } from '@shared/typescript-utils' | 20 | import { AttributesOnly } from '@shared/typescript-utils' |
21 | import { Account, AccountSummary } from '../../../shared/models/actors' | 21 | import { Account, AccountSummary } from '../../../shared/models/actors' |
22 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' | 22 | import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts' |
@@ -38,7 +38,7 @@ import { ApplicationModel } from '../application/application' | |||
38 | import { ServerModel } from '../server/server' | 38 | import { ServerModel } from '../server/server' |
39 | import { ServerBlocklistModel } from '../server/server-blocklist' | 39 | import { ServerBlocklistModel } from '../server/server-blocklist' |
40 | import { UserModel } from '../user/user' | 40 | import { UserModel } from '../user/user' |
41 | import { getSort, throwIfNotValid } from '../utils' | 41 | import { buildSQLAttributes, getSort, throwIfNotValid } from '../shared' |
42 | import { VideoModel } from '../video/video' | 42 | import { VideoModel } from '../video/video' |
43 | import { VideoChannelModel } from '../video/video-channel' | 43 | import { VideoChannelModel } from '../video/video-channel' |
44 | import { VideoCommentModel } from '../video/video-comment' | 44 | import { VideoCommentModel } from '../video/video-comment' |
@@ -251,6 +251,18 @@ export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> { | |||
251 | return undefined | 251 | return undefined |
252 | } | 252 | } |
253 | 253 | ||
254 | // --------------------------------------------------------------------------- | ||
255 | |||
256 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
257 | return buildSQLAttributes({ | ||
258 | model: this, | ||
259 | tableName, | ||
260 | aliasPrefix | ||
261 | }) | ||
262 | } | ||
263 | |||
264 | // --------------------------------------------------------------------------- | ||
265 | |||
254 | static load (id: number, transaction?: Transaction): Promise<MAccountDefault> { | 266 | static load (id: number, transaction?: Transaction): Promise<MAccountDefault> { |
255 | return AccountModel.findByPk(id, { transaction }) | 267 | return AccountModel.findByPk(id, { transaction }) |
256 | } | 268 | } |
diff --git a/server/models/actor/actor-follow.ts b/server/models/actor/actor-follow.ts index 9615229dd..32e5d78b0 100644 --- a/server/models/actor/actor-follow.ts +++ b/server/models/actor/actor-follow.ts | |||
@@ -38,7 +38,7 @@ import { ACTOR_FOLLOW_SCORE, CONSTRAINTS_FIELDS, FOLLOW_STATES, SERVER_ACTOR_NAM | |||
38 | import { AccountModel } from '../account/account' | 38 | import { AccountModel } from '../account/account' |
39 | import { ServerModel } from '../server/server' | 39 | import { ServerModel } from '../server/server' |
40 | import { doesExist } from '../shared/query' | 40 | import { doesExist } from '../shared/query' |
41 | import { createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../utils' | 41 | import { buildSQLAttributes, createSafeIn, getSort, searchAttribute, throwIfNotValid } from '../shared' |
42 | import { VideoChannelModel } from '../video/video-channel' | 42 | import { VideoChannelModel } from '../video/video-channel' |
43 | import { ActorModel, unusedActorAttributesForAPI } from './actor' | 43 | import { ActorModel, unusedActorAttributesForAPI } from './actor' |
44 | import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' | 44 | import { InstanceListFollowersQueryBuilder, ListFollowersOptions } from './sql/instance-list-followers-query-builder' |
@@ -140,6 +140,18 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
140 | }) | 140 | }) |
141 | } | 141 | } |
142 | 142 | ||
143 | // --------------------------------------------------------------------------- | ||
144 | |||
145 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
146 | return buildSQLAttributes({ | ||
147 | model: this, | ||
148 | tableName, | ||
149 | aliasPrefix | ||
150 | }) | ||
151 | } | ||
152 | |||
153 | // --------------------------------------------------------------------------- | ||
154 | |||
143 | /* | 155 | /* |
144 | * @deprecated Use `findOrCreateCustom` instead | 156 | * @deprecated Use `findOrCreateCustom` instead |
145 | */ | 157 | */ |
@@ -213,7 +225,7 @@ export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowMo | |||
213 | `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` + | 225 | `WHERE "actorId" = $followerActorId AND "targetActorId" = $actorId AND "state" = 'accepted' ` + |
214 | `LIMIT 1` | 226 | `LIMIT 1` |
215 | 227 | ||
216 | return doesExist(query, { actorId, followerActorId }) | 228 | return doesExist(this.sequelize, query, { actorId, followerActorId }) |
217 | } | 229 | } |
218 | 230 | ||
219 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { | 231 | static loadByActorAndTarget (actorId: number, targetActorId: number, t?: Transaction): Promise<MActorFollowActorsDefault> { |
diff --git a/server/models/actor/actor-image.ts b/server/models/actor/actor-image.ts index f2b3b2f4b..9c34a0101 100644 --- a/server/models/actor/actor-image.ts +++ b/server/models/actor/actor-image.ts | |||
@@ -22,7 +22,7 @@ import { isActivityPubUrlValid } from '../../helpers/custom-validators/activityp | |||
22 | import { logger } from '../../helpers/logger' | 22 | import { logger } from '../../helpers/logger' |
23 | import { CONFIG } from '../../initializers/config' | 23 | import { CONFIG } from '../../initializers/config' |
24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' | 24 | import { LAZY_STATIC_PATHS, MIMETYPES, WEBSERVER } from '../../initializers/constants' |
25 | import { throwIfNotValid } from '../utils' | 25 | import { buildSQLAttributes, throwIfNotValid } from '../shared' |
26 | import { ActorModel } from './actor' | 26 | import { ActorModel } from './actor' |
27 | 27 | ||
28 | @Table({ | 28 | @Table({ |
@@ -94,6 +94,18 @@ export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageMode | |||
94 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, { err })) | 94 | .catch(err => logger.error('Cannot remove actor image file %s.', instance.filename, { err })) |
95 | } | 95 | } |
96 | 96 | ||
97 | // --------------------------------------------------------------------------- | ||
98 | |||
99 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
100 | return buildSQLAttributes({ | ||
101 | model: this, | ||
102 | tableName, | ||
103 | aliasPrefix | ||
104 | }) | ||
105 | } | ||
106 | |||
107 | // --------------------------------------------------------------------------- | ||
108 | |||
97 | static loadByName (filename: string) { | 109 | static loadByName (filename: string) { |
98 | const query = { | 110 | const query = { |
99 | where: { | 111 | where: { |
diff --git a/server/models/actor/actor.ts b/server/models/actor/actor.ts index d7afa727d..1432e8757 100644 --- a/server/models/actor/actor.ts +++ b/server/models/actor/actor.ts | |||
@@ -17,7 +17,7 @@ import { | |||
17 | } from 'sequelize-typescript' | 17 | } from 'sequelize-typescript' |
18 | import { activityPubContextify } from '@server/lib/activitypub/context' | 18 | import { activityPubContextify } from '@server/lib/activitypub/context' |
19 | import { getBiggestActorImage } from '@server/lib/actor-image' | 19 | import { getBiggestActorImage } from '@server/lib/actor-image' |
20 | import { ModelCache } from '@server/models/model-cache' | 20 | import { ModelCache } from '@server/models/shared/model-cache' |
21 | import { forceNumber, getLowercaseExtension } from '@shared/core-utils' | 21 | import { forceNumber, getLowercaseExtension } from '@shared/core-utils' |
22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' | 22 | import { ActivityIconObject, ActivityPubActorType, ActorImageType } from '@shared/models' |
23 | import { AttributesOnly } from '@shared/typescript-utils' | 23 | import { AttributesOnly } from '@shared/typescript-utils' |
@@ -55,7 +55,7 @@ import { | |||
55 | import { AccountModel } from '../account/account' | 55 | import { AccountModel } from '../account/account' |
56 | import { getServerActor } from '../application/application' | 56 | import { getServerActor } from '../application/application' |
57 | import { ServerModel } from '../server/server' | 57 | import { ServerModel } from '../server/server' |
58 | import { isOutdated, throwIfNotValid } from '../utils' | 58 | import { buildSQLAttributes, isOutdated, throwIfNotValid } from '../shared' |
59 | import { VideoModel } from '../video/video' | 59 | import { VideoModel } from '../video/video' |
60 | import { VideoChannelModel } from '../video/video-channel' | 60 | import { VideoChannelModel } from '../video/video-channel' |
61 | import { ActorFollowModel } from './actor-follow' | 61 | import { ActorFollowModel } from './actor-follow' |
@@ -65,7 +65,7 @@ enum ScopeNames { | |||
65 | FULL = 'FULL' | 65 | FULL = 'FULL' |
66 | } | 66 | } |
67 | 67 | ||
68 | export const unusedActorAttributesForAPI = [ | 68 | export const unusedActorAttributesForAPI: (keyof AttributesOnly<ActorModel>)[] = [ |
69 | 'publicKey', | 69 | 'publicKey', |
70 | 'privateKey', | 70 | 'privateKey', |
71 | 'inboxUrl', | 71 | 'inboxUrl', |
@@ -306,6 +306,27 @@ export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> { | |||
306 | }) | 306 | }) |
307 | VideoChannel: VideoChannelModel | 307 | VideoChannel: VideoChannelModel |
308 | 308 | ||
309 | // --------------------------------------------------------------------------- | ||
310 | |||
311 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
312 | return buildSQLAttributes({ | ||
313 | model: this, | ||
314 | tableName, | ||
315 | aliasPrefix | ||
316 | }) | ||
317 | } | ||
318 | |||
319 | static getSQLAPIAttributes (tableName: string, aliasPrefix = '') { | ||
320 | return buildSQLAttributes({ | ||
321 | model: this, | ||
322 | tableName, | ||
323 | aliasPrefix, | ||
324 | excludeAttributes: unusedActorAttributesForAPI | ||
325 | }) | ||
326 | } | ||
327 | |||
328 | // --------------------------------------------------------------------------- | ||
329 | |||
309 | static async load (id: number): Promise<MActor> { | 330 | static async load (id: number): Promise<MActor> { |
310 | const actorServer = await getServerActor() | 331 | const actorServer = await getServerActor() |
311 | if (id === actorServer.id) return actorServer | 332 | if (id === actorServer.id) return actorServer |
diff --git a/server/models/actor/sql/instance-list-followers-query-builder.ts b/server/models/actor/sql/instance-list-followers-query-builder.ts index 4a17a8f11..34ce29b5d 100644 --- a/server/models/actor/sql/instance-list-followers-query-builder.ts +++ b/server/models/actor/sql/instance-list-followers-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { ModelBuilder } from '@server/models/shared' | 2 | import { ModelBuilder } from '@server/models/shared' |
3 | import { parseRowCountResult } from '@server/models/utils' | ||
4 | import { MActorFollowActorsDefault } from '@server/types/models' | 3 | import { MActorFollowActorsDefault } from '@server/types/models' |
5 | import { ActivityPubActorType, FollowState } from '@shared/models' | 4 | import { ActivityPubActorType, FollowState } from '@shared/models' |
5 | import { parseRowCountResult } from '../../shared' | ||
6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' | 6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' |
7 | 7 | ||
8 | export interface ListFollowersOptions { | 8 | export interface ListFollowersOptions { |
diff --git a/server/models/actor/sql/instance-list-following-query-builder.ts b/server/models/actor/sql/instance-list-following-query-builder.ts index 880170b85..77b4e3dce 100644 --- a/server/models/actor/sql/instance-list-following-query-builder.ts +++ b/server/models/actor/sql/instance-list-following-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { ModelBuilder } from '@server/models/shared' | 2 | import { ModelBuilder } from '@server/models/shared' |
3 | import { parseRowCountResult } from '@server/models/utils' | ||
4 | import { MActorFollowActorsDefault } from '@server/types/models' | 3 | import { MActorFollowActorsDefault } from '@server/types/models' |
5 | import { ActivityPubActorType, FollowState } from '@shared/models' | 4 | import { ActivityPubActorType, FollowState } from '@shared/models' |
5 | import { parseRowCountResult } from '../../shared' | ||
6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' | 6 | import { InstanceListFollowsQueryBuilder } from './shared/instance-list-follows-query-builder' |
7 | 7 | ||
8 | export interface ListFollowingOptions { | 8 | export interface ListFollowingOptions { |
diff --git a/server/models/actor/sql/shared/actor-follow-table-attributes.ts b/server/models/actor/sql/shared/actor-follow-table-attributes.ts index 156b37d44..7dd908ece 100644 --- a/server/models/actor/sql/shared/actor-follow-table-attributes.ts +++ b/server/models/actor/sql/shared/actor-follow-table-attributes.ts | |||
@@ -1,62 +1,31 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { Memoize } from '@server/helpers/memoize' | ||
3 | import { ServerModel } from '@server/models/server/server' | ||
4 | import { ActorModel } from '../../actor' | ||
5 | import { ActorFollowModel } from '../../actor-follow' | ||
6 | import { ActorImageModel } from '../../actor-image' | ||
7 | |||
1 | export class ActorFollowTableAttributes { | 8 | export class ActorFollowTableAttributes { |
2 | 9 | ||
10 | @Memoize() | ||
3 | getFollowAttributes () { | 11 | getFollowAttributes () { |
4 | return [ | 12 | logger.error('coucou') |
5 | '"ActorFollowModel"."id"', | 13 | |
6 | '"ActorFollowModel"."state"', | 14 | return ActorFollowModel.getSQLAttributes('ActorFollowModel').join(', ') |
7 | '"ActorFollowModel"."score"', | ||
8 | '"ActorFollowModel"."url"', | ||
9 | '"ActorFollowModel"."actorId"', | ||
10 | '"ActorFollowModel"."targetActorId"', | ||
11 | '"ActorFollowModel"."createdAt"', | ||
12 | '"ActorFollowModel"."updatedAt"' | ||
13 | ].join(', ') | ||
14 | } | 15 | } |
15 | 16 | ||
17 | @Memoize() | ||
16 | getActorAttributes (actorTableName: string) { | 18 | getActorAttributes (actorTableName: string) { |
17 | return [ | 19 | return ActorModel.getSQLAttributes(actorTableName, `${actorTableName}.`).join(', ') |
18 | `"${actorTableName}"."id" AS "${actorTableName}.id"`, | ||
19 | `"${actorTableName}"."type" AS "${actorTableName}.type"`, | ||
20 | `"${actorTableName}"."preferredUsername" AS "${actorTableName}.preferredUsername"`, | ||
21 | `"${actorTableName}"."url" AS "${actorTableName}.url"`, | ||
22 | `"${actorTableName}"."publicKey" AS "${actorTableName}.publicKey"`, | ||
23 | `"${actorTableName}"."privateKey" AS "${actorTableName}.privateKey"`, | ||
24 | `"${actorTableName}"."followersCount" AS "${actorTableName}.followersCount"`, | ||
25 | `"${actorTableName}"."followingCount" AS "${actorTableName}.followingCount"`, | ||
26 | `"${actorTableName}"."inboxUrl" AS "${actorTableName}.inboxUrl"`, | ||
27 | `"${actorTableName}"."outboxUrl" AS "${actorTableName}.outboxUrl"`, | ||
28 | `"${actorTableName}"."sharedInboxUrl" AS "${actorTableName}.sharedInboxUrl"`, | ||
29 | `"${actorTableName}"."followersUrl" AS "${actorTableName}.followersUrl"`, | ||
30 | `"${actorTableName}"."followingUrl" AS "${actorTableName}.followingUrl"`, | ||
31 | `"${actorTableName}"."remoteCreatedAt" AS "${actorTableName}.remoteCreatedAt"`, | ||
32 | `"${actorTableName}"."serverId" AS "${actorTableName}.serverId"`, | ||
33 | `"${actorTableName}"."createdAt" AS "${actorTableName}.createdAt"`, | ||
34 | `"${actorTableName}"."updatedAt" AS "${actorTableName}.updatedAt"` | ||
35 | ].join(', ') | ||
36 | } | 20 | } |
37 | 21 | ||
22 | @Memoize() | ||
38 | getServerAttributes (actorTableName: string) { | 23 | getServerAttributes (actorTableName: string) { |
39 | return [ | 24 | return ServerModel.getSQLAttributes(`${actorTableName}->Server`, `${actorTableName}.Server.`).join(', ') |
40 | `"${actorTableName}->Server"."id" AS "${actorTableName}.Server.id"`, | ||
41 | `"${actorTableName}->Server"."host" AS "${actorTableName}.Server.host"`, | ||
42 | `"${actorTableName}->Server"."redundancyAllowed" AS "${actorTableName}.Server.redundancyAllowed"`, | ||
43 | `"${actorTableName}->Server"."createdAt" AS "${actorTableName}.Server.createdAt"`, | ||
44 | `"${actorTableName}->Server"."updatedAt" AS "${actorTableName}.Server.updatedAt"` | ||
45 | ].join(', ') | ||
46 | } | 25 | } |
47 | 26 | ||
27 | @Memoize() | ||
48 | getAvatarAttributes (actorTableName: string) { | 28 | getAvatarAttributes (actorTableName: string) { |
49 | return [ | 29 | return ActorImageModel.getSQLAttributes(`${actorTableName}->Avatars`, `${actorTableName}.Avatars.`).join(', ') |
50 | `"${actorTableName}->Avatars"."id" AS "${actorTableName}.Avatars.id"`, | ||
51 | `"${actorTableName}->Avatars"."filename" AS "${actorTableName}.Avatars.filename"`, | ||
52 | `"${actorTableName}->Avatars"."height" AS "${actorTableName}.Avatars.height"`, | ||
53 | `"${actorTableName}->Avatars"."width" AS "${actorTableName}.Avatars.width"`, | ||
54 | `"${actorTableName}->Avatars"."fileUrl" AS "${actorTableName}.Avatars.fileUrl"`, | ||
55 | `"${actorTableName}->Avatars"."onDisk" AS "${actorTableName}.Avatars.onDisk"`, | ||
56 | `"${actorTableName}->Avatars"."type" AS "${actorTableName}.Avatars.type"`, | ||
57 | `"${actorTableName}->Avatars"."actorId" AS "${actorTableName}.Avatars.actorId"`, | ||
58 | `"${actorTableName}->Avatars"."createdAt" AS "${actorTableName}.Avatars.createdAt"`, | ||
59 | `"${actorTableName}->Avatars"."updatedAt" AS "${actorTableName}.Avatars.updatedAt"` | ||
60 | ].join(', ') | ||
61 | } | 30 | } |
62 | } | 31 | } |
diff --git a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts index 1d70fbe70..d9593e48b 100644 --- a/server/models/actor/sql/shared/instance-list-follows-query-builder.ts +++ b/server/models/actor/sql/shared/instance-list-follows-query-builder.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { AbstractRunQuery } from '@server/models/shared' | 2 | import { AbstractRunQuery } from '@server/models/shared' |
3 | import { getInstanceFollowsSort } from '@server/models/utils' | ||
4 | import { ActorImageType } from '@shared/models' | 3 | import { ActorImageType } from '@shared/models' |
4 | import { getInstanceFollowsSort } from '../../../shared' | ||
5 | import { ActorFollowTableAttributes } from './actor-follow-table-attributes' | 5 | import { ActorFollowTableAttributes } from './actor-follow-table-attributes' |
6 | 6 | ||
7 | type BaseOptions = { | 7 | type BaseOptions = { |
diff --git a/server/models/redundancy/video-redundancy.ts b/server/models/redundancy/video-redundancy.ts index 15909d5f3..c2a72b71f 100644 --- a/server/models/redundancy/video-redundancy.ts +++ b/server/models/redundancy/video-redundancy.ts | |||
@@ -34,7 +34,7 @@ import { CONFIG } from '../../initializers/config' | |||
34 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' | 34 | import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants' |
35 | import { ActorModel } from '../actor/actor' | 35 | import { ActorModel } from '../actor/actor' |
36 | import { ServerModel } from '../server/server' | 36 | import { ServerModel } from '../server/server' |
37 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils' | 37 | import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../shared' |
38 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' | 38 | import { ScheduleVideoUpdateModel } from '../video/schedule-video-update' |
39 | import { VideoModel } from '../video/video' | 39 | import { VideoModel } from '../video/video' |
40 | import { VideoChannelModel } from '../video/video-channel' | 40 | import { VideoChannelModel } from '../video/video-channel' |
diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 71c205ffa..9948c9f7a 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts | |||
@@ -11,7 +11,7 @@ import { | |||
11 | isPluginStableVersionValid, | 11 | isPluginStableVersionValid, |
12 | isPluginTypeValid | 12 | isPluginTypeValid |
13 | } from '../../helpers/custom-validators/plugins' | 13 | } from '../../helpers/custom-validators/plugins' |
14 | import { getSort, throwIfNotValid } from '../utils' | 14 | import { getSort, throwIfNotValid } from '../shared' |
15 | 15 | ||
16 | @DefaultScope(() => ({ | 16 | @DefaultScope(() => ({ |
17 | attributes: { | 17 | attributes: { |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 9752dfbc3..3d755fe4a 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -4,7 +4,7 @@ import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormat | |||
4 | import { ServerBlock } from '@shared/models' | 4 | import { ServerBlock } from '@shared/models' |
5 | import { AttributesOnly } from '@shared/typescript-utils' | 5 | import { AttributesOnly } from '@shared/typescript-utils' |
6 | import { AccountModel } from '../account/account' | 6 | import { AccountModel } from '../account/account' |
7 | import { createSafeIn, getSort, searchAttribute } from '../utils' | 7 | import { createSafeIn, getSort, searchAttribute } from '../shared' |
8 | import { ServerModel } from './server' | 8 | import { ServerModel } from './server' |
9 | 9 | ||
10 | enum ScopeNames { | 10 | enum ScopeNames { |
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index ef42de090..a5e05f460 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -4,7 +4,7 @@ import { MServer, MServerFormattable } from '@server/types/models/server' | |||
4 | import { AttributesOnly } from '@shared/typescript-utils' | 4 | import { AttributesOnly } from '@shared/typescript-utils' |
5 | import { isHostValid } from '../../helpers/custom-validators/servers' | 5 | import { isHostValid } from '../../helpers/custom-validators/servers' |
6 | import { ActorModel } from '../actor/actor' | 6 | import { ActorModel } from '../actor/actor' |
7 | import { throwIfNotValid } from '../utils' | 7 | import { buildSQLAttributes, throwIfNotValid } from '../shared' |
8 | import { ServerBlocklistModel } from './server-blocklist' | 8 | import { ServerBlocklistModel } from './server-blocklist' |
9 | 9 | ||
10 | @Table({ | 10 | @Table({ |
@@ -52,6 +52,18 @@ export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> { | |||
52 | }) | 52 | }) |
53 | BlockedBy: ServerBlocklistModel[] | 53 | BlockedBy: ServerBlocklistModel[] |
54 | 54 | ||
55 | // --------------------------------------------------------------------------- | ||
56 | |||
57 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
58 | return buildSQLAttributes({ | ||
59 | model: this, | ||
60 | tableName, | ||
61 | aliasPrefix | ||
62 | }) | ||
63 | } | ||
64 | |||
65 | // --------------------------------------------------------------------------- | ||
66 | |||
55 | static load (id: number, transaction?: Transaction): Promise<MServer> { | 67 | static load (id: number, transaction?: Transaction): Promise<MServer> { |
56 | const query = { | 68 | const query = { |
57 | where: { | 69 | where: { |
diff --git a/server/models/shared/index.ts b/server/models/shared/index.ts index 04528929c..5a7621e4d 100644 --- a/server/models/shared/index.ts +++ b/server/models/shared/index.ts | |||
@@ -1,4 +1,8 @@ | |||
1 | export * from './abstract-run-query' | 1 | export * from './abstract-run-query' |
2 | export * from './model-builder' | 2 | export * from './model-builder' |
3 | export * from './model-cache' | ||
3 | export * from './query' | 4 | export * from './query' |
5 | export * from './sequelize-helpers' | ||
6 | export * from './sort' | ||
7 | export * from './sql' | ||
4 | export * from './update' | 8 | export * from './update' |
diff --git a/server/models/shared/model-builder.ts b/server/models/shared/model-builder.ts index c015ca4f5..07f7c4038 100644 --- a/server/models/shared/model-builder.ts +++ b/server/models/shared/model-builder.ts | |||
@@ -1,7 +1,24 @@ | |||
1 | import { isPlainObject } from 'lodash' | 1 | import { isPlainObject } from 'lodash' |
2 | import { Model as SequelizeModel, Sequelize } from 'sequelize' | 2 | import { Model as SequelizeModel, ModelStatic, Sequelize } from 'sequelize' |
3 | import { logger } from '@server/helpers/logger' | 3 | import { logger } from '@server/helpers/logger' |
4 | 4 | ||
5 | /** | ||
6 | * | ||
7 | * Build Sequelize models from sequelize raw query (that must use { nest: true } options) | ||
8 | * | ||
9 | * In order to sequelize to correctly build the JSON this class will ingest, | ||
10 | * the columns selected in the raw query should be in the following form: | ||
11 | * * All tables must be Pascal Cased (for example "VideoChannel") | ||
12 | * * Root table must end with `Model` (for example "VideoCommentModel") | ||
13 | * * Joined tables must contain the origin table name + '->JoinedTable'. For example: | ||
14 | * * "Actor" is joined to "Account": "Actor" table must be renamed "Account->Actor" | ||
15 | * * "Account->Actor" is joined to "Server": "Server" table must be renamed to "Account->Actor->Server" | ||
16 | * * Selected columns must be renamed to contain the JSON path: | ||
17 | * * "videoComment"."id": "VideoCommentModel"."id" | ||
18 | * * "Account"."Actor"."Server"."id": "Account.Actor.Server.id" | ||
19 | * * All tables must contain the row id | ||
20 | */ | ||
21 | |||
5 | export class ModelBuilder <T extends SequelizeModel> { | 22 | export class ModelBuilder <T extends SequelizeModel> { |
6 | private readonly modelRegistry = new Map<string, T>() | 23 | private readonly modelRegistry = new Map<string, T>() |
7 | 24 | ||
@@ -72,18 +89,18 @@ export class ModelBuilder <T extends SequelizeModel> { | |||
72 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), | 89 | 'Cannot build model %s that does not exist', this.buildSequelizeModelName(modelName), |
73 | { existing: this.sequelize.modelManager.all.map(m => m.name) } | 90 | { existing: this.sequelize.modelManager.all.map(m => m.name) } |
74 | ) | 91 | ) |
75 | return undefined | 92 | return { created: false, model: null } |
76 | } | 93 | } |
77 | 94 | ||
78 | // FIXME: typings | 95 | const model = Model.build(json, { raw: true, isNewRecord: false }) |
79 | const model = new (Model as any)(json) | 96 | |
80 | this.modelRegistry.set(registryKey, model) | 97 | this.modelRegistry.set(registryKey, model) |
81 | 98 | ||
82 | return { created: true, model } | 99 | return { created: true, model } |
83 | } | 100 | } |
84 | 101 | ||
85 | private findModelBuilder (modelName: string) { | 102 | private findModelBuilder (modelName: string) { |
86 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) | 103 | return this.sequelize.modelManager.getModel(this.buildSequelizeModelName(modelName)) as ModelStatic<T> |
87 | } | 104 | } |
88 | 105 | ||
89 | private buildSequelizeModelName (modelName: string) { | 106 | private buildSequelizeModelName (modelName: string) { |
diff --git a/server/models/model-cache.ts b/server/models/shared/model-cache.ts index 3651267e7..3651267e7 100644 --- a/server/models/model-cache.ts +++ b/server/models/shared/model-cache.ts | |||
diff --git a/server/models/shared/query.ts b/server/models/shared/query.ts index 036cc13c6..934acc21f 100644 --- a/server/models/shared/query.ts +++ b/server/models/shared/query.ts | |||
@@ -1,17 +1,82 @@ | |||
1 | import { BindOrReplacements, QueryTypes } from 'sequelize' | 1 | import { BindOrReplacements, Op, QueryTypes, Sequelize } from 'sequelize' |
2 | import { sequelizeTypescript } from '@server/initializers/database' | 2 | import validator from 'validator' |
3 | import { forceNumber } from '@shared/core-utils' | ||
3 | 4 | ||
4 | function doesExist (query: string, bind?: BindOrReplacements) { | 5 | function doesExist (sequelize: Sequelize, query: string, bind?: BindOrReplacements) { |
5 | const options = { | 6 | const options = { |
6 | type: QueryTypes.SELECT as QueryTypes.SELECT, | 7 | type: QueryTypes.SELECT as QueryTypes.SELECT, |
7 | bind, | 8 | bind, |
8 | raw: true | 9 | raw: true |
9 | } | 10 | } |
10 | 11 | ||
11 | return sequelizeTypescript.query(query, options) | 12 | return sequelize.query(query, options) |
12 | .then(results => results.length === 1) | 13 | .then(results => results.length === 1) |
13 | } | 14 | } |
14 | 15 | ||
16 | function createSimilarityAttribute (col: string, value: string) { | ||
17 | return Sequelize.fn( | ||
18 | 'similarity', | ||
19 | |||
20 | searchTrigramNormalizeCol(col), | ||
21 | |||
22 | searchTrigramNormalizeValue(value) | ||
23 | ) | ||
24 | } | ||
25 | |||
26 | function buildWhereIdOrUUID (id: number | string) { | ||
27 | return validator.isInt('' + id) ? { id } : { uuid: id } | ||
28 | } | ||
29 | |||
30 | function parseAggregateResult (result: any) { | ||
31 | if (!result) return 0 | ||
32 | |||
33 | const total = forceNumber(result) | ||
34 | if (isNaN(total)) return 0 | ||
35 | |||
36 | return total | ||
37 | } | ||
38 | |||
39 | function parseRowCountResult (result: any) { | ||
40 | if (result.length !== 0) return result[0].total | ||
41 | |||
42 | return 0 | ||
43 | } | ||
44 | |||
45 | function createSafeIn (sequelize: Sequelize, toEscape: (string | number)[], additionalUnescaped: string[] = []) { | ||
46 | return toEscape.map(t => { | ||
47 | return t === null | ||
48 | ? null | ||
49 | : sequelize.escape('' + t) | ||
50 | }).concat(additionalUnescaped).join(', ') | ||
51 | } | ||
52 | |||
53 | function searchAttribute (sourceField?: string, targetField?: string) { | ||
54 | if (!sourceField) return {} | ||
55 | |||
56 | return { | ||
57 | [targetField]: { | ||
58 | // FIXME: ts error | ||
59 | [Op.iLike as any]: `%${sourceField}%` | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | |||
15 | export { | 64 | export { |
16 | doesExist | 65 | doesExist, |
66 | createSimilarityAttribute, | ||
67 | buildWhereIdOrUUID, | ||
68 | parseAggregateResult, | ||
69 | parseRowCountResult, | ||
70 | createSafeIn, | ||
71 | searchAttribute | ||
72 | } | ||
73 | |||
74 | // --------------------------------------------------------------------------- | ||
75 | |||
76 | function searchTrigramNormalizeValue (value: string) { | ||
77 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value)) | ||
78 | } | ||
79 | |||
80 | function searchTrigramNormalizeCol (col: string) { | ||
81 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) | ||
17 | } | 82 | } |
diff --git a/server/models/shared/sequelize-helpers.ts b/server/models/shared/sequelize-helpers.ts new file mode 100644 index 000000000..7af8471dc --- /dev/null +++ b/server/models/shared/sequelize-helpers.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import { Sequelize } from 'sequelize' | ||
2 | |||
3 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | ||
4 | if (!model.createdAt || !model.updatedAt) { | ||
5 | throw new Error('Miss createdAt & updatedAt attributes to model') | ||
6 | } | ||
7 | |||
8 | const now = Date.now() | ||
9 | const createdAtTime = model.createdAt.getTime() | ||
10 | const updatedAtTime = model.updatedAt.getTime() | ||
11 | |||
12 | return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval | ||
13 | } | ||
14 | |||
15 | function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) { | ||
16 | if (nullable && (value === null || value === undefined)) return | ||
17 | |||
18 | if (validator(value) === false) { | ||
19 | throw new Error(`"${value}" is not a valid ${fieldName}.`) | ||
20 | } | ||
21 | } | ||
22 | |||
23 | function buildTrigramSearchIndex (indexName: string, attribute: string) { | ||
24 | return { | ||
25 | name: indexName, | ||
26 | // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function | ||
27 | fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ], | ||
28 | using: 'gin', | ||
29 | operator: 'gin_trgm_ops' | ||
30 | } | ||
31 | } | ||
32 | |||
33 | // --------------------------------------------------------------------------- | ||
34 | |||
35 | export { | ||
36 | throwIfNotValid, | ||
37 | buildTrigramSearchIndex, | ||
38 | isOutdated | ||
39 | } | ||
diff --git a/server/models/shared/sort.ts b/server/models/shared/sort.ts new file mode 100644 index 000000000..77e84dcf4 --- /dev/null +++ b/server/models/shared/sort.ts | |||
@@ -0,0 +1,160 @@ | |||
1 | import { literal, OrderItem, Sequelize } from 'sequelize' | ||
2 | |||
3 | // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] | ||
4 | function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
5 | const { direction, field } = buildSortDirectionAndField(value) | ||
6 | |||
7 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
8 | |||
9 | if (field.toLowerCase() === 'match') { // Search | ||
10 | finalField = Sequelize.col('similarity') | ||
11 | } else { | ||
12 | finalField = field | ||
13 | } | ||
14 | |||
15 | return [ [ finalField, direction ], lastSort ] | ||
16 | } | ||
17 | |||
18 | function getAdminUsersSort (value: string): OrderItem[] { | ||
19 | const { direction, field } = buildSortDirectionAndField(value) | ||
20 | |||
21 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
22 | |||
23 | if (field === 'videoQuotaUsed') { // Users list | ||
24 | finalField = Sequelize.col('videoQuotaUsed') | ||
25 | } else { | ||
26 | finalField = field | ||
27 | } | ||
28 | |||
29 | const nullPolicy = direction === 'ASC' | ||
30 | ? 'NULLS FIRST' | ||
31 | : 'NULLS LAST' | ||
32 | |||
33 | // FIXME: typings | ||
34 | return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] | ||
35 | } | ||
36 | |||
37 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
38 | const { direction, field } = buildSortDirectionAndField(value) | ||
39 | |||
40 | if (field.toLowerCase() === 'name') { | ||
41 | return [ [ 'displayName', direction ], lastSort ] | ||
42 | } | ||
43 | |||
44 | return getSort(value, lastSort) | ||
45 | } | ||
46 | |||
47 | function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
48 | const { direction, field } = buildSortDirectionAndField(value) | ||
49 | |||
50 | if (field === 'totalReplies') { | ||
51 | return [ | ||
52 | [ Sequelize.literal('"totalReplies"'), direction ], | ||
53 | lastSort | ||
54 | ] | ||
55 | } | ||
56 | |||
57 | return getSort(value, lastSort) | ||
58 | } | ||
59 | |||
60 | function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
61 | const { direction, field } = buildSortDirectionAndField(value) | ||
62 | |||
63 | if (field.toLowerCase() === 'trending') { // Sort by aggregation | ||
64 | return [ | ||
65 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], | ||
66 | |||
67 | [ Sequelize.col('VideoModel.views'), direction ], | ||
68 | |||
69 | lastSort | ||
70 | ] | ||
71 | } else if (field === 'publishedAt') { | ||
72 | return [ | ||
73 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
74 | |||
75 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
76 | |||
77 | lastSort | ||
78 | ] | ||
79 | } | ||
80 | |||
81 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
82 | |||
83 | // Alias | ||
84 | if (field.toLowerCase() === 'match') { // Search | ||
85 | finalField = Sequelize.col('similarity') | ||
86 | } else { | ||
87 | finalField = field | ||
88 | } | ||
89 | |||
90 | const firstSort: OrderItem = typeof finalField === 'string' | ||
91 | ? finalField.split('.').concat([ direction ]) as OrderItem | ||
92 | : [ finalField, direction ] | ||
93 | |||
94 | return [ firstSort, lastSort ] | ||
95 | } | ||
96 | |||
97 | function getBlacklistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
98 | const { direction, field } = buildSortDirectionAndField(value) | ||
99 | |||
100 | const videoFields = new Set([ 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid' ]) | ||
101 | |||
102 | if (videoFields.has(field)) { | ||
103 | return [ | ||
104 | [ literal(`"Video.${field}" ${direction}`) ], | ||
105 | lastSort | ||
106 | ] as OrderItem[] | ||
107 | } | ||
108 | |||
109 | return getSort(value, lastSort) | ||
110 | } | ||
111 | |||
112 | function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
113 | const { direction, field } = buildSortDirectionAndField(value) | ||
114 | |||
115 | if (field === 'redundancyAllowed') { | ||
116 | return [ | ||
117 | [ 'ActorFollowing.Server.redundancyAllowed', direction ], | ||
118 | lastSort | ||
119 | ] | ||
120 | } | ||
121 | |||
122 | return getSort(value, lastSort) | ||
123 | } | ||
124 | |||
125 | function getChannelSyncSort (value: string): OrderItem[] { | ||
126 | const { direction, field } = buildSortDirectionAndField(value) | ||
127 | if (field.toLowerCase() === 'videochannel') { | ||
128 | return [ | ||
129 | [ literal('"VideoChannel.name"'), direction ] | ||
130 | ] | ||
131 | } | ||
132 | return [ [ field, direction ] ] | ||
133 | } | ||
134 | |||
135 | function buildSortDirectionAndField (value: string) { | ||
136 | let field: string | ||
137 | let direction: 'ASC' | 'DESC' | ||
138 | |||
139 | if (value.substring(0, 1) === '-') { | ||
140 | direction = 'DESC' | ||
141 | field = value.substring(1) | ||
142 | } else { | ||
143 | direction = 'ASC' | ||
144 | field = value | ||
145 | } | ||
146 | |||
147 | return { direction, field } | ||
148 | } | ||
149 | |||
150 | export { | ||
151 | buildSortDirectionAndField, | ||
152 | getPlaylistSort, | ||
153 | getSort, | ||
154 | getCommentSort, | ||
155 | getAdminUsersSort, | ||
156 | getVideoSort, | ||
157 | getBlacklistSort, | ||
158 | getChannelSyncSort, | ||
159 | getInstanceFollowsSort | ||
160 | } | ||
diff --git a/server/models/shared/sql.ts b/server/models/shared/sql.ts new file mode 100644 index 000000000..5aaeb49f0 --- /dev/null +++ b/server/models/shared/sql.ts | |||
@@ -0,0 +1,68 @@ | |||
1 | import { literal, Model, ModelStatic } from 'sequelize' | ||
2 | import { forceNumber } from '@shared/core-utils' | ||
3 | import { AttributesOnly } from '@shared/typescript-utils' | ||
4 | |||
5 | function buildLocalAccountIdsIn () { | ||
6 | return literal( | ||
7 | '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)' | ||
8 | ) | ||
9 | } | ||
10 | |||
11 | function buildLocalActorIdsIn () { | ||
12 | return literal( | ||
13 | '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)' | ||
14 | ) | ||
15 | } | ||
16 | |||
17 | function buildBlockedAccountSQL (blockerIds: number[]) { | ||
18 | const blockerIdsString = blockerIds.join(', ') | ||
19 | |||
20 | return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + | ||
21 | ' UNION ' + | ||
22 | 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + | ||
23 | 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + | ||
24 | 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' | ||
25 | } | ||
26 | |||
27 | function buildServerIdsFollowedBy (actorId: any) { | ||
28 | const actorIdNumber = forceNumber(actorId) | ||
29 | |||
30 | return '(' + | ||
31 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | ||
32 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | ||
33 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
34 | ')' | ||
35 | } | ||
36 | |||
37 | function buildSQLAttributes<M extends Model> (options: { | ||
38 | model: ModelStatic<M> | ||
39 | tableName: string | ||
40 | |||
41 | excludeAttributes?: Exclude<keyof AttributesOnly<M>, symbol>[] | ||
42 | aliasPrefix?: string | ||
43 | }) { | ||
44 | const { model, tableName, aliasPrefix, excludeAttributes } = options | ||
45 | |||
46 | const attributes = Object.keys(model.getAttributes()) as Exclude<keyof AttributesOnly<M>, symbol>[] | ||
47 | |||
48 | return attributes | ||
49 | .filter(a => { | ||
50 | if (!excludeAttributes) return true | ||
51 | if (excludeAttributes.includes(a)) return false | ||
52 | |||
53 | return true | ||
54 | }) | ||
55 | .map(a => { | ||
56 | return `"${tableName}"."${a}" AS "${aliasPrefix || ''}${a}"` | ||
57 | }) | ||
58 | } | ||
59 | |||
60 | // --------------------------------------------------------------------------- | ||
61 | |||
62 | export { | ||
63 | buildSQLAttributes, | ||
64 | buildBlockedAccountSQL, | ||
65 | buildServerIdsFollowedBy, | ||
66 | buildLocalAccountIdsIn, | ||
67 | buildLocalActorIdsIn | ||
68 | } | ||
diff --git a/server/models/shared/update.ts b/server/models/shared/update.ts index d338211e3..d02c4535d 100644 --- a/server/models/shared/update.ts +++ b/server/models/shared/update.ts | |||
@@ -1,9 +1,15 @@ | |||
1 | import { QueryTypes, Transaction } from 'sequelize' | 1 | import { QueryTypes, Sequelize, Transaction } from 'sequelize' |
2 | import { sequelizeTypescript } from '@server/initializers/database' | ||
3 | 2 | ||
4 | // Sequelize always skip the update if we only update updatedAt field | 3 | // Sequelize always skip the update if we only update updatedAt field |
5 | function setAsUpdated (table: string, id: number, transaction?: Transaction) { | 4 | function setAsUpdated (options: { |
6 | return sequelizeTypescript.query( | 5 | sequelize: Sequelize |
6 | table: string | ||
7 | id: number | ||
8 | transaction?: Transaction | ||
9 | }) { | ||
10 | const { sequelize, table, id, transaction } = options | ||
11 | |||
12 | return sequelize.query( | ||
7 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, | 13 | `UPDATE "${table}" SET "updatedAt" = :updatedAt WHERE id = :id`, |
8 | { | 14 | { |
9 | replacements: { table, id, updatedAt: new Date() }, | 15 | replacements: { table, id, updatedAt: new Date() }, |
diff --git a/server/models/user/sql/user-notitication-list-query-builder.ts b/server/models/user/sql/user-notitication-list-query-builder.ts index 31b4932bf..d11546df0 100644 --- a/server/models/user/sql/user-notitication-list-query-builder.ts +++ b/server/models/user/sql/user-notitication-list-query-builder.ts | |||
@@ -1,8 +1,8 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' | 2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' |
3 | import { getSort } from '@server/models/utils' | ||
4 | import { UserNotificationModelForApi } from '@server/types/models' | 3 | import { UserNotificationModelForApi } from '@server/types/models' |
5 | import { ActorImageType } from '@shared/models' | 4 | import { ActorImageType } from '@shared/models' |
5 | import { getSort } from '../../shared' | ||
6 | 6 | ||
7 | export interface ListNotificationsOptions { | 7 | export interface ListNotificationsOptions { |
8 | userId: number | 8 | userId: number |
diff --git a/server/models/user/user-notification-setting.ts b/server/models/user/user-notification-setting.ts index 66e1d85b3..394494c0c 100644 --- a/server/models/user/user-notification-setting.ts +++ b/server/models/user/user-notification-setting.ts | |||
@@ -17,7 +17,7 @@ import { MNotificationSettingFormattable } from '@server/types/models' | |||
17 | import { AttributesOnly } from '@shared/typescript-utils' | 17 | import { AttributesOnly } from '@shared/typescript-utils' |
18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' | 18 | import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model' |
19 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' | 19 | import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications' |
20 | import { throwIfNotValid } from '../utils' | 20 | import { throwIfNotValid } from '../shared' |
21 | import { UserModel } from './user' | 21 | import { UserModel } from './user' |
22 | 22 | ||
23 | @Table({ | 23 | @Table({ |
diff --git a/server/models/user/user-notification.ts b/server/models/user/user-notification.ts index d37fa5dc7..6e134158f 100644 --- a/server/models/user/user-notification.ts +++ b/server/models/user/user-notification.ts | |||
@@ -13,7 +13,7 @@ import { AccountModel } from '../account/account' | |||
13 | import { ActorFollowModel } from '../actor/actor-follow' | 13 | import { ActorFollowModel } from '../actor/actor-follow' |
14 | import { ApplicationModel } from '../application/application' | 14 | import { ApplicationModel } from '../application/application' |
15 | import { PluginModel } from '../server/plugin' | 15 | import { PluginModel } from '../server/plugin' |
16 | import { throwIfNotValid } from '../utils' | 16 | import { throwIfNotValid } from '../shared' |
17 | import { VideoModel } from '../video/video' | 17 | import { VideoModel } from '../video/video' |
18 | import { VideoBlacklistModel } from '../video/video-blacklist' | 18 | import { VideoBlacklistModel } from '../video/video-blacklist' |
19 | import { VideoCommentModel } from '../video/video-comment' | 19 | import { VideoCommentModel } from '../video/video-comment' |
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 672728a2a..0932a367a 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -30,6 +30,7 @@ import { | |||
30 | MUserNotifSettingChannelDefault, | 30 | MUserNotifSettingChannelDefault, |
31 | MUserWithNotificationSetting | 31 | MUserWithNotificationSetting |
32 | } from '@server/types/models' | 32 | } from '@server/types/models' |
33 | import { forceNumber } from '@shared/core-utils' | ||
33 | import { AttributesOnly } from '@shared/typescript-utils' | 34 | import { AttributesOnly } from '@shared/typescript-utils' |
34 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' | 35 | import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users' |
35 | import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' | 36 | import { AbuseState, MyUser, UserRight, VideoPlaylistType } from '../../../shared/models' |
@@ -63,14 +64,13 @@ import { ActorModel } from '../actor/actor' | |||
63 | import { ActorFollowModel } from '../actor/actor-follow' | 64 | import { ActorFollowModel } from '../actor/actor-follow' |
64 | import { ActorImageModel } from '../actor/actor-image' | 65 | import { ActorImageModel } from '../actor/actor-image' |
65 | import { OAuthTokenModel } from '../oauth/oauth-token' | 66 | import { OAuthTokenModel } from '../oauth/oauth-token' |
66 | import { getAdminUsersSort, throwIfNotValid } from '../utils' | 67 | import { getAdminUsersSort, throwIfNotValid } from '../shared' |
67 | import { VideoModel } from '../video/video' | 68 | import { VideoModel } from '../video/video' |
68 | import { VideoChannelModel } from '../video/video-channel' | 69 | import { VideoChannelModel } from '../video/video-channel' |
69 | import { VideoImportModel } from '../video/video-import' | 70 | import { VideoImportModel } from '../video/video-import' |
70 | import { VideoLiveModel } from '../video/video-live' | 71 | import { VideoLiveModel } from '../video/video-live' |
71 | import { VideoPlaylistModel } from '../video/video-playlist' | 72 | import { VideoPlaylistModel } from '../video/video-playlist' |
72 | import { UserNotificationSettingModel } from './user-notification-setting' | 73 | import { UserNotificationSettingModel } from './user-notification-setting' |
73 | import { forceNumber } from '@shared/core-utils' | ||
74 | 74 | ||
75 | enum ScopeNames { | 75 | enum ScopeNames { |
76 | FOR_ME_API = 'FOR_ME_API', | 76 | FOR_ME_API = 'FOR_ME_API', |
diff --git a/server/models/utils.ts b/server/models/utils.ts deleted file mode 100644 index 3476799ce..000000000 --- a/server/models/utils.ts +++ /dev/null | |||
@@ -1,317 +0,0 @@ | |||
1 | import { literal, Op, OrderItem, Sequelize } from 'sequelize' | ||
2 | import validator from 'validator' | ||
3 | import { forceNumber } from '@shared/core-utils' | ||
4 | |||
5 | type SortType = { sortModel: string, sortValue: string } | ||
6 | |||
7 | // Translate for example "-name" to [ [ 'name', 'DESC' ], [ 'id', 'ASC' ] ] | ||
8 | function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
9 | const { direction, field } = buildDirectionAndField(value) | ||
10 | |||
11 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
12 | |||
13 | if (field.toLowerCase() === 'match') { // Search | ||
14 | finalField = Sequelize.col('similarity') | ||
15 | } else { | ||
16 | finalField = field | ||
17 | } | ||
18 | |||
19 | return [ [ finalField, direction ], lastSort ] | ||
20 | } | ||
21 | |||
22 | function getAdminUsersSort (value: string): OrderItem[] { | ||
23 | const { direction, field } = buildDirectionAndField(value) | ||
24 | |||
25 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
26 | |||
27 | if (field === 'videoQuotaUsed') { // Users list | ||
28 | finalField = Sequelize.col('videoQuotaUsed') | ||
29 | } else { | ||
30 | finalField = field | ||
31 | } | ||
32 | |||
33 | const nullPolicy = direction === 'ASC' | ||
34 | ? 'NULLS FIRST' | ||
35 | : 'NULLS LAST' | ||
36 | |||
37 | // FIXME: typings | ||
38 | return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ] | ||
39 | } | ||
40 | |||
41 | function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
42 | const { direction, field } = buildDirectionAndField(value) | ||
43 | |||
44 | if (field.toLowerCase() === 'name') { | ||
45 | return [ [ 'displayName', direction ], lastSort ] | ||
46 | } | ||
47 | |||
48 | return getSort(value, lastSort) | ||
49 | } | ||
50 | |||
51 | function getCommentSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
52 | const { direction, field } = buildDirectionAndField(value) | ||
53 | |||
54 | if (field === 'totalReplies') { | ||
55 | return [ | ||
56 | [ Sequelize.literal('"totalReplies"'), direction ], | ||
57 | lastSort | ||
58 | ] | ||
59 | } | ||
60 | |||
61 | return getSort(value, lastSort) | ||
62 | } | ||
63 | |||
64 | function getVideoSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
65 | const { direction, field } = buildDirectionAndField(value) | ||
66 | |||
67 | if (field.toLowerCase() === 'trending') { // Sort by aggregation | ||
68 | return [ | ||
69 | [ Sequelize.fn('COALESCE', Sequelize.fn('SUM', Sequelize.col('VideoViews.views')), '0'), direction ], | ||
70 | |||
71 | [ Sequelize.col('VideoModel.views'), direction ], | ||
72 | |||
73 | lastSort | ||
74 | ] | ||
75 | } else if (field === 'publishedAt') { | ||
76 | return [ | ||
77 | [ 'ScheduleVideoUpdate', 'updateAt', direction + ' NULLS LAST' ], | ||
78 | |||
79 | [ Sequelize.col('VideoModel.publishedAt'), direction ], | ||
80 | |||
81 | lastSort | ||
82 | ] | ||
83 | } | ||
84 | |||
85 | let finalField: string | ReturnType<typeof Sequelize.col> | ||
86 | |||
87 | // Alias | ||
88 | if (field.toLowerCase() === 'match') { // Search | ||
89 | finalField = Sequelize.col('similarity') | ||
90 | } else { | ||
91 | finalField = field | ||
92 | } | ||
93 | |||
94 | const firstSort: OrderItem = typeof finalField === 'string' | ||
95 | ? finalField.split('.').concat([ direction ]) as OrderItem | ||
96 | : [ finalField, direction ] | ||
97 | |||
98 | return [ firstSort, lastSort ] | ||
99 | } | ||
100 | |||
101 | function getBlacklistSort (model: any, value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
102 | const [ firstSort ] = getSort(value) | ||
103 | |||
104 | if (model) return [ [ literal(`"${model}.${firstSort[0]}" ${firstSort[1]}`) ], lastSort ] as OrderItem[] | ||
105 | return [ firstSort, lastSort ] | ||
106 | } | ||
107 | |||
108 | function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { | ||
109 | const { direction, field } = buildDirectionAndField(value) | ||
110 | |||
111 | if (field === 'redundancyAllowed') { | ||
112 | return [ | ||
113 | [ 'ActorFollowing.Server.redundancyAllowed', direction ], | ||
114 | lastSort | ||
115 | ] | ||
116 | } | ||
117 | |||
118 | return getSort(value, lastSort) | ||
119 | } | ||
120 | |||
121 | function getChannelSyncSort (value: string): OrderItem[] { | ||
122 | const { direction, field } = buildDirectionAndField(value) | ||
123 | if (field.toLowerCase() === 'videochannel') { | ||
124 | return [ | ||
125 | [ literal('"VideoChannel.name"'), direction ] | ||
126 | ] | ||
127 | } | ||
128 | return [ [ field, direction ] ] | ||
129 | } | ||
130 | |||
131 | function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { | ||
132 | if (!model.createdAt || !model.updatedAt) { | ||
133 | throw new Error('Miss createdAt & updatedAt attributes to model') | ||
134 | } | ||
135 | |||
136 | const now = Date.now() | ||
137 | const createdAtTime = model.createdAt.getTime() | ||
138 | const updatedAtTime = model.updatedAt.getTime() | ||
139 | |||
140 | return (now - createdAtTime) > refreshInterval && (now - updatedAtTime) > refreshInterval | ||
141 | } | ||
142 | |||
143 | function throwIfNotValid (value: any, validator: (value: any) => boolean, fieldName = 'value', nullable = false) { | ||
144 | if (nullable && (value === null || value === undefined)) return | ||
145 | |||
146 | if (validator(value) === false) { | ||
147 | throw new Error(`"${value}" is not a valid ${fieldName}.`) | ||
148 | } | ||
149 | } | ||
150 | |||
151 | function buildTrigramSearchIndex (indexName: string, attribute: string) { | ||
152 | return { | ||
153 | name: indexName, | ||
154 | // FIXME: gin_trgm_ops is not taken into account in Sequelize 6, so adding it ourselves in the literal function | ||
155 | fields: [ Sequelize.literal('lower(immutable_unaccent(' + attribute + ')) gin_trgm_ops') as any ], | ||
156 | using: 'gin', | ||
157 | operator: 'gin_trgm_ops' | ||
158 | } | ||
159 | } | ||
160 | |||
161 | function createSimilarityAttribute (col: string, value: string) { | ||
162 | return Sequelize.fn( | ||
163 | 'similarity', | ||
164 | |||
165 | searchTrigramNormalizeCol(col), | ||
166 | |||
167 | searchTrigramNormalizeValue(value) | ||
168 | ) | ||
169 | } | ||
170 | |||
171 | function buildBlockedAccountSQL (blockerIds: number[]) { | ||
172 | const blockerIdsString = blockerIds.join(', ') | ||
173 | |||
174 | return 'SELECT "targetAccountId" AS "id" FROM "accountBlocklist" WHERE "accountId" IN (' + blockerIdsString + ')' + | ||
175 | ' UNION ' + | ||
176 | 'SELECT "account"."id" AS "id" FROM account INNER JOIN "actor" ON account."actorId" = actor.id ' + | ||
177 | 'INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ' + | ||
178 | 'WHERE "serverBlocklist"."accountId" IN (' + blockerIdsString + ')' | ||
179 | } | ||
180 | |||
181 | function buildBlockedAccountSQLOptimized (columnNameJoin: string, blockerIds: number[]) { | ||
182 | const blockerIdsString = blockerIds.join(', ') | ||
183 | |||
184 | return [ | ||
185 | literal( | ||
186 | `NOT EXISTS (` + | ||
187 | ` SELECT 1 FROM "accountBlocklist" ` + | ||
188 | ` WHERE "targetAccountId" = ${columnNameJoin} ` + | ||
189 | ` AND "accountId" IN (${blockerIdsString})` + | ||
190 | `)` | ||
191 | ), | ||
192 | |||
193 | literal( | ||
194 | `NOT EXISTS (` + | ||
195 | ` SELECT 1 FROM "account" ` + | ||
196 | ` INNER JOIN "actor" ON account."actorId" = actor.id ` + | ||
197 | ` INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` + | ||
198 | ` WHERE "account"."id" = ${columnNameJoin} ` + | ||
199 | ` AND "serverBlocklist"."accountId" IN (${blockerIdsString})` + | ||
200 | `)` | ||
201 | ) | ||
202 | ] | ||
203 | } | ||
204 | |||
205 | function buildServerIdsFollowedBy (actorId: any) { | ||
206 | const actorIdNumber = forceNumber(actorId) | ||
207 | |||
208 | return '(' + | ||
209 | 'SELECT "actor"."serverId" FROM "actorFollow" ' + | ||
210 | 'INNER JOIN "actor" ON actor.id = "actorFollow"."targetActorId" ' + | ||
211 | 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + | ||
212 | ')' | ||
213 | } | ||
214 | |||
215 | function buildWhereIdOrUUID (id: number | string) { | ||
216 | return validator.isInt('' + id) ? { id } : { uuid: id } | ||
217 | } | ||
218 | |||
219 | function parseAggregateResult (result: any) { | ||
220 | if (!result) return 0 | ||
221 | |||
222 | const total = forceNumber(result) | ||
223 | if (isNaN(total)) return 0 | ||
224 | |||
225 | return total | ||
226 | } | ||
227 | |||
228 | function parseRowCountResult (result: any) { | ||
229 | if (result.length !== 0) return result[0].total | ||
230 | |||
231 | return 0 | ||
232 | } | ||
233 | |||
234 | function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) { | ||
235 | return stringArr.map(t => { | ||
236 | return t === null | ||
237 | ? null | ||
238 | : sequelize.escape('' + t) | ||
239 | }).join(', ') | ||
240 | } | ||
241 | |||
242 | function buildLocalAccountIdsIn () { | ||
243 | return literal( | ||
244 | '(SELECT "account"."id" FROM "account" INNER JOIN "actor" ON "actor"."id" = "account"."actorId" AND "actor"."serverId" IS NULL)' | ||
245 | ) | ||
246 | } | ||
247 | |||
248 | function buildLocalActorIdsIn () { | ||
249 | return literal( | ||
250 | '(SELECT "actor"."id" FROM "actor" WHERE "actor"."serverId" IS NULL)' | ||
251 | ) | ||
252 | } | ||
253 | |||
254 | function buildDirectionAndField (value: string) { | ||
255 | let field: string | ||
256 | let direction: 'ASC' | 'DESC' | ||
257 | |||
258 | if (value.substring(0, 1) === '-') { | ||
259 | direction = 'DESC' | ||
260 | field = value.substring(1) | ||
261 | } else { | ||
262 | direction = 'ASC' | ||
263 | field = value | ||
264 | } | ||
265 | |||
266 | return { direction, field } | ||
267 | } | ||
268 | |||
269 | function searchAttribute (sourceField?: string, targetField?: string) { | ||
270 | if (!sourceField) return {} | ||
271 | |||
272 | return { | ||
273 | [targetField]: { | ||
274 | // FIXME: ts error | ||
275 | [Op.iLike as any]: `%${sourceField}%` | ||
276 | } | ||
277 | } | ||
278 | } | ||
279 | |||
280 | // --------------------------------------------------------------------------- | ||
281 | |||
282 | export { | ||
283 | buildBlockedAccountSQL, | ||
284 | buildBlockedAccountSQLOptimized, | ||
285 | buildLocalActorIdsIn, | ||
286 | getPlaylistSort, | ||
287 | SortType, | ||
288 | buildLocalAccountIdsIn, | ||
289 | getSort, | ||
290 | getCommentSort, | ||
291 | getAdminUsersSort, | ||
292 | getVideoSort, | ||
293 | getBlacklistSort, | ||
294 | getChannelSyncSort, | ||
295 | createSimilarityAttribute, | ||
296 | throwIfNotValid, | ||
297 | buildServerIdsFollowedBy, | ||
298 | buildTrigramSearchIndex, | ||
299 | buildWhereIdOrUUID, | ||
300 | isOutdated, | ||
301 | parseAggregateResult, | ||
302 | getInstanceFollowsSort, | ||
303 | buildDirectionAndField, | ||
304 | createSafeIn, | ||
305 | searchAttribute, | ||
306 | parseRowCountResult | ||
307 | } | ||
308 | |||
309 | // --------------------------------------------------------------------------- | ||
310 | |||
311 | function searchTrigramNormalizeValue (value: string) { | ||
312 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', value)) | ||
313 | } | ||
314 | |||
315 | function searchTrigramNormalizeCol (col: string) { | ||
316 | return Sequelize.fn('lower', Sequelize.fn('immutable_unaccent', Sequelize.col(col))) | ||
317 | } | ||
diff --git a/server/models/video/sql/comment/video-comment-list-query-builder.ts b/server/models/video/sql/comment/video-comment-list-query-builder.ts new file mode 100644 index 000000000..3960f6b13 --- /dev/null +++ b/server/models/video/sql/comment/video-comment-list-query-builder.ts | |||
@@ -0,0 +1,385 @@ | |||
1 | import { Model, Sequelize, Transaction } from 'sequelize' | ||
2 | import { AbstractRunQuery, ModelBuilder } from '@server/models/shared' | ||
3 | import { ActorImageType, VideoPrivacy } from '@shared/models' | ||
4 | import { createSafeIn, getCommentSort, parseRowCountResult } from '../../../shared' | ||
5 | import { VideoCommentTableAttributes } from './video-comment-table-attributes' | ||
6 | |||
7 | export interface ListVideoCommentsOptions { | ||
8 | selectType: 'api' | 'feed' | 'comment-only' | ||
9 | |||
10 | start?: number | ||
11 | count?: number | ||
12 | sort?: string | ||
13 | |||
14 | videoId?: number | ||
15 | threadId?: number | ||
16 | accountId?: number | ||
17 | videoChannelId?: number | ||
18 | |||
19 | blockerAccountIds?: number[] | ||
20 | |||
21 | isThread?: boolean | ||
22 | notDeleted?: boolean | ||
23 | isLocal?: boolean | ||
24 | onLocalVideo?: boolean | ||
25 | onPublicVideo?: boolean | ||
26 | videoAccountOwnerId?: boolean | ||
27 | |||
28 | search?: string | ||
29 | searchAccount?: string | ||
30 | searchVideo?: string | ||
31 | |||
32 | includeReplyCounters?: boolean | ||
33 | |||
34 | transaction?: Transaction | ||
35 | } | ||
36 | |||
37 | export class VideoCommentListQueryBuilder extends AbstractRunQuery { | ||
38 | private readonly tableAttributes = new VideoCommentTableAttributes() | ||
39 | |||
40 | private innerQuery: string | ||
41 | |||
42 | private select = '' | ||
43 | private joins = '' | ||
44 | |||
45 | private innerSelect = '' | ||
46 | private innerJoins = '' | ||
47 | private innerWhere = '' | ||
48 | |||
49 | private readonly built = { | ||
50 | cte: false, | ||
51 | accountJoin: false, | ||
52 | videoJoin: false, | ||
53 | videoChannelJoin: false, | ||
54 | avatarJoin: false | ||
55 | } | ||
56 | |||
57 | constructor ( | ||
58 | protected readonly sequelize: Sequelize, | ||
59 | private readonly options: ListVideoCommentsOptions | ||
60 | ) { | ||
61 | super(sequelize) | ||
62 | } | ||
63 | |||
64 | async listComments <T extends Model> () { | ||
65 | this.buildListQuery() | ||
66 | |||
67 | const results = await this.runQuery({ nest: true, transaction: this.options.transaction }) | ||
68 | const modelBuilder = new ModelBuilder<T>(this.sequelize) | ||
69 | |||
70 | return modelBuilder.createModels(results, 'VideoComment') | ||
71 | } | ||
72 | |||
73 | async countComments () { | ||
74 | this.buildCountQuery() | ||
75 | |||
76 | const result = await this.runQuery({ transaction: this.options.transaction }) | ||
77 | |||
78 | return parseRowCountResult(result) | ||
79 | } | ||
80 | |||
81 | // --------------------------------------------------------------------------- | ||
82 | |||
83 | private buildListQuery () { | ||
84 | this.buildInnerListQuery() | ||
85 | this.buildListSelect() | ||
86 | |||
87 | this.query = `${this.select} ` + | ||
88 | `FROM (${this.innerQuery}) AS "VideoCommentModel" ` + | ||
89 | `${this.joins} ` + | ||
90 | `${this.getOrder()}` | ||
91 | } | ||
92 | |||
93 | private buildInnerListQuery () { | ||
94 | this.buildWhere() | ||
95 | this.buildInnerListSelect() | ||
96 | |||
97 | this.innerQuery = `${this.innerSelect} ` + | ||
98 | `FROM "videoComment" AS "VideoCommentModel" ` + | ||
99 | `${this.innerJoins} ` + | ||
100 | `${this.innerWhere} ` + | ||
101 | `${this.getOrder()} ` + | ||
102 | `${this.getInnerLimit()}` | ||
103 | } | ||
104 | |||
105 | // --------------------------------------------------------------------------- | ||
106 | |||
107 | private buildCountQuery () { | ||
108 | this.buildWhere() | ||
109 | |||
110 | this.query = `SELECT COUNT(*) AS "total" ` + | ||
111 | `FROM "videoComment" AS "VideoCommentModel" ` + | ||
112 | `${this.innerJoins} ` + | ||
113 | `${this.innerWhere}` | ||
114 | } | ||
115 | |||
116 | // --------------------------------------------------------------------------- | ||
117 | |||
118 | private buildWhere () { | ||
119 | let where: string[] = [] | ||
120 | |||
121 | if (this.options.videoId) { | ||
122 | this.replacements.videoId = this.options.videoId | ||
123 | |||
124 | where.push('"VideoCommentModel"."videoId" = :videoId') | ||
125 | } | ||
126 | |||
127 | if (this.options.threadId) { | ||
128 | this.replacements.threadId = this.options.threadId | ||
129 | |||
130 | where.push('("VideoCommentModel"."id" = :threadId OR "VideoCommentModel"."originCommentId" = :threadId)') | ||
131 | } | ||
132 | |||
133 | if (this.options.accountId) { | ||
134 | this.replacements.accountId = this.options.accountId | ||
135 | |||
136 | where.push('"VideoCommentModel"."accountId" = :accountId') | ||
137 | } | ||
138 | |||
139 | if (this.options.videoChannelId) { | ||
140 | this.buildVideoChannelJoin() | ||
141 | |||
142 | this.replacements.videoChannelId = this.options.videoChannelId | ||
143 | |||
144 | where.push('"Account->VideoChannel"."id" = :videoChannelId') | ||
145 | } | ||
146 | |||
147 | if (this.options.blockerAccountIds) { | ||
148 | this.buildVideoChannelJoin() | ||
149 | |||
150 | where = where.concat(this.getBlockWhere('VideoCommentModel', 'Video->VideoChannel')) | ||
151 | } | ||
152 | |||
153 | if (this.options.isThread === true) { | ||
154 | where.push('"VideoCommentModel"."inReplyToCommentId" IS NULL') | ||
155 | } | ||
156 | |||
157 | if (this.options.notDeleted === true) { | ||
158 | where.push('"VideoCommentModel"."deletedAt" IS NULL') | ||
159 | } | ||
160 | |||
161 | if (this.options.isLocal === true) { | ||
162 | this.buildAccountJoin() | ||
163 | |||
164 | where.push('"Account->Actor"."serverId" IS NULL') | ||
165 | } else if (this.options.isLocal === false) { | ||
166 | this.buildAccountJoin() | ||
167 | |||
168 | where.push('"Account->Actor"."serverId" IS NOT NULL') | ||
169 | } | ||
170 | |||
171 | if (this.options.onLocalVideo === true) { | ||
172 | this.buildVideoJoin() | ||
173 | |||
174 | where.push('"Video"."remote" IS FALSE') | ||
175 | } else if (this.options.onLocalVideo === false) { | ||
176 | this.buildVideoJoin() | ||
177 | |||
178 | where.push('"Video"."remote" IS TRUE') | ||
179 | } | ||
180 | |||
181 | if (this.options.onPublicVideo === true) { | ||
182 | this.buildVideoJoin() | ||
183 | |||
184 | where.push(`"Video"."privacy" = ${VideoPrivacy.PUBLIC}`) | ||
185 | } | ||
186 | |||
187 | if (this.options.videoAccountOwnerId) { | ||
188 | this.buildVideoChannelJoin() | ||
189 | |||
190 | this.replacements.videoAccountOwnerId = this.options.videoAccountOwnerId | ||
191 | |||
192 | where.push(`"Video->VideoChannel"."accountId" = :videoAccountOwnerId`) | ||
193 | } | ||
194 | |||
195 | if (this.options.search) { | ||
196 | this.buildVideoJoin() | ||
197 | this.buildAccountJoin() | ||
198 | |||
199 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.search + '%') | ||
200 | |||
201 | where.push( | ||
202 | `(` + | ||
203 | `"VideoCommentModel"."text" ILIKE ${escapedLikeSearch} OR ` + | ||
204 | `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` + | ||
205 | `"Account"."name" ILIKE ${escapedLikeSearch} OR ` + | ||
206 | `"Video"."name" ILIKE ${escapedLikeSearch} ` + | ||
207 | `)` | ||
208 | ) | ||
209 | } | ||
210 | |||
211 | if (this.options.searchAccount) { | ||
212 | this.buildAccountJoin() | ||
213 | |||
214 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchAccount + '%') | ||
215 | |||
216 | where.push( | ||
217 | `(` + | ||
218 | `"Account->Actor"."preferredUsername" ILIKE ${escapedLikeSearch} OR ` + | ||
219 | `"Account"."name" ILIKE ${escapedLikeSearch} ` + | ||
220 | `)` | ||
221 | ) | ||
222 | } | ||
223 | |||
224 | if (this.options.searchVideo) { | ||
225 | this.buildVideoJoin() | ||
226 | |||
227 | const escapedLikeSearch = this.sequelize.escape('%' + this.options.searchVideo + '%') | ||
228 | |||
229 | where.push(`"Video"."name" ILIKE ${escapedLikeSearch}`) | ||
230 | } | ||
231 | |||
232 | if (where.length !== 0) { | ||
233 | this.innerWhere = `WHERE ${where.join(' AND ')}` | ||
234 | } | ||
235 | } | ||
236 | |||
237 | private buildAccountJoin () { | ||
238 | if (this.built.accountJoin) return | ||
239 | |||
240 | this.innerJoins += ' LEFT JOIN "account" "Account" ON "Account"."id" = "VideoCommentModel"."accountId" ' + | ||
241 | 'LEFT JOIN "actor" "Account->Actor" ON "Account->Actor"."id" = "Account"."actorId" ' + | ||
242 | 'LEFT JOIN "server" "Account->Actor->Server" ON "Account->Actor"."serverId" = "Account->Actor->Server"."id" ' | ||
243 | |||
244 | this.built.accountJoin = true | ||
245 | } | ||
246 | |||
247 | private buildVideoJoin () { | ||
248 | if (this.built.videoJoin) return | ||
249 | |||
250 | this.innerJoins += ' LEFT JOIN "video" "Video" ON "Video"."id" = "VideoCommentModel"."videoId" ' | ||
251 | |||
252 | this.built.videoJoin = true | ||
253 | } | ||
254 | |||
255 | private buildVideoChannelJoin () { | ||
256 | if (this.built.videoChannelJoin) return | ||
257 | |||
258 | this.buildVideoJoin() | ||
259 | |||
260 | this.innerJoins += ' LEFT JOIN "videoChannel" "Video->VideoChannel" ON "Video"."channelId" = "Video->VideoChannel"."id" ' | ||
261 | |||
262 | this.built.videoChannelJoin = true | ||
263 | } | ||
264 | |||
265 | private buildAvatarsJoin () { | ||
266 | if (this.options.selectType !== 'api' && this.options.selectType !== 'feed') return '' | ||
267 | if (this.built.avatarJoin) return | ||
268 | |||
269 | this.joins += `LEFT JOIN "actorImage" "Account->Actor->Avatars" ` + | ||
270 | `ON "VideoCommentModel"."Account.Actor.id" = "Account->Actor->Avatars"."actorId" ` + | ||
271 | `AND "Account->Actor->Avatars"."type" = ${ActorImageType.AVATAR}` | ||
272 | |||
273 | this.built.avatarJoin = true | ||
274 | } | ||
275 | |||
276 | // --------------------------------------------------------------------------- | ||
277 | |||
278 | private buildListSelect () { | ||
279 | const toSelect = [ '"VideoCommentModel".*' ] | ||
280 | |||
281 | if (this.options.selectType === 'api' || this.options.selectType === 'feed') { | ||
282 | this.buildAvatarsJoin() | ||
283 | |||
284 | toSelect.push(this.tableAttributes.getAvatarAttributes()) | ||
285 | } | ||
286 | |||
287 | if (this.options.includeReplyCounters === true) { | ||
288 | toSelect.push(this.getTotalRepliesSelect()) | ||
289 | toSelect.push(this.getAuthorTotalRepliesSelect()) | ||
290 | } | ||
291 | |||
292 | this.select = this.buildSelect(toSelect) | ||
293 | } | ||
294 | |||
295 | private buildInnerListSelect () { | ||
296 | let toSelect = [ this.tableAttributes.getVideoCommentAttributes() ] | ||
297 | |||
298 | if (this.options.selectType === 'api' || this.options.selectType === 'feed') { | ||
299 | this.buildAccountJoin() | ||
300 | this.buildVideoJoin() | ||
301 | |||
302 | toSelect = toSelect.concat([ | ||
303 | this.tableAttributes.getVideoAttributes(), | ||
304 | this.tableAttributes.getAccountAttributes(), | ||
305 | this.tableAttributes.getActorAttributes(), | ||
306 | this.tableAttributes.getServerAttributes() | ||
307 | ]) | ||
308 | } | ||
309 | |||
310 | this.innerSelect = this.buildSelect(toSelect) | ||
311 | } | ||
312 | |||
313 | // --------------------------------------------------------------------------- | ||
314 | |||
315 | private getBlockWhere (commentTableName: string, channelTableName: string) { | ||
316 | const where: string[] = [] | ||
317 | |||
318 | const blockerIdsString = createSafeIn( | ||
319 | this.sequelize, | ||
320 | this.options.blockerAccountIds, | ||
321 | [ `"${channelTableName}"."accountId"` ] | ||
322 | ) | ||
323 | |||
324 | where.push( | ||
325 | `NOT EXISTS (` + | ||
326 | `SELECT 1 FROM "accountBlocklist" ` + | ||
327 | `WHERE "targetAccountId" = "${commentTableName}"."accountId" ` + | ||
328 | `AND "accountId" IN (${blockerIdsString})` + | ||
329 | `)` | ||
330 | ) | ||
331 | |||
332 | where.push( | ||
333 | `NOT EXISTS (` + | ||
334 | `SELECT 1 FROM "account" ` + | ||
335 | `INNER JOIN "actor" ON account."actorId" = actor.id ` + | ||
336 | `INNER JOIN "serverBlocklist" ON "actor"."serverId" = "serverBlocklist"."targetServerId" ` + | ||
337 | `WHERE "account"."id" = "${commentTableName}"."accountId" ` + | ||
338 | `AND "serverBlocklist"."accountId" IN (${blockerIdsString})` + | ||
339 | `)` | ||
340 | ) | ||
341 | |||
342 | return where | ||
343 | } | ||
344 | |||
345 | // --------------------------------------------------------------------------- | ||
346 | |||
347 | private getTotalRepliesSelect () { | ||
348 | const blockWhereString = this.getBlockWhere('replies', 'videoChannel').join(' AND ') | ||
349 | |||
350 | return `(` + | ||
351 | `SELECT COUNT("replies"."id") FROM "videoComment" AS "replies" ` + | ||
352 | `LEFT JOIN "video" ON "video"."id" = "replies"."videoId" ` + | ||
353 | `LEFT JOIN "videoChannel" ON "video"."channelId" = "videoChannel"."id" ` + | ||
354 | `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ` + | ||
355 | `AND "deletedAt" IS NULL ` + | ||
356 | `AND ${blockWhereString} ` + | ||
357 | `) AS "totalReplies"` | ||
358 | } | ||
359 | |||
360 | private getAuthorTotalRepliesSelect () { | ||
361 | return `(` + | ||
362 | `SELECT COUNT("replies"."id") FROM "videoComment" AS "replies" ` + | ||
363 | `INNER JOIN "video" ON "video"."id" = "replies"."videoId" ` + | ||
364 | `INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ` + | ||
365 | `WHERE "replies"."originCommentId" = "VideoCommentModel"."id" AND "replies"."accountId" = "videoChannel"."accountId"` + | ||
366 | `) AS "totalRepliesFromVideoAuthor"` | ||
367 | } | ||
368 | |||
369 | private getOrder () { | ||
370 | if (!this.options.sort) return '' | ||
371 | |||
372 | const orders = getCommentSort(this.options.sort) | ||
373 | |||
374 | return 'ORDER BY ' + orders.map(o => `"${o[0]}" ${o[1]}`).join(', ') | ||
375 | } | ||
376 | |||
377 | private getInnerLimit () { | ||
378 | if (!this.options.count) return '' | ||
379 | |||
380 | this.replacements.limit = this.options.count | ||
381 | this.replacements.offset = this.options.start || 0 | ||
382 | |||
383 | return `LIMIT :limit OFFSET :offset ` | ||
384 | } | ||
385 | } | ||
diff --git a/server/models/video/sql/comment/video-comment-table-attributes.ts b/server/models/video/sql/comment/video-comment-table-attributes.ts new file mode 100644 index 000000000..87f8750c1 --- /dev/null +++ b/server/models/video/sql/comment/video-comment-table-attributes.ts | |||
@@ -0,0 +1,43 @@ | |||
1 | import { Memoize } from '@server/helpers/memoize' | ||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { ActorModel } from '@server/models/actor/actor' | ||
4 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
5 | import { ServerModel } from '@server/models/server/server' | ||
6 | import { VideoCommentModel } from '../../video-comment' | ||
7 | |||
8 | export class VideoCommentTableAttributes { | ||
9 | |||
10 | @Memoize() | ||
11 | getVideoCommentAttributes () { | ||
12 | return VideoCommentModel.getSQLAttributes('VideoCommentModel').join(', ') | ||
13 | } | ||
14 | |||
15 | @Memoize() | ||
16 | getAccountAttributes () { | ||
17 | return AccountModel.getSQLAttributes('Account', 'Account.').join(', ') | ||
18 | } | ||
19 | |||
20 | @Memoize() | ||
21 | getVideoAttributes () { | ||
22 | return [ | ||
23 | `"Video"."id" AS "Video.id"`, | ||
24 | `"Video"."uuid" AS "Video.uuid"`, | ||
25 | `"Video"."name" AS "Video.name"` | ||
26 | ].join(', ') | ||
27 | } | ||
28 | |||
29 | @Memoize() | ||
30 | getActorAttributes () { | ||
31 | return ActorModel.getSQLAPIAttributes('Account->Actor', `Account.Actor.`).join(', ') | ||
32 | } | ||
33 | |||
34 | @Memoize() | ||
35 | getServerAttributes () { | ||
36 | return ServerModel.getSQLAttributes('Account->Actor->Server', `Account.Actor.Server.`).join(', ') | ||
37 | } | ||
38 | |||
39 | @Memoize() | ||
40 | getAvatarAttributes () { | ||
41 | return ActorImageModel.getSQLAttributes('Account->Actor->Avatars', 'Account.Actor.Avatars.').join(', ') | ||
42 | } | ||
43 | } | ||
diff --git a/server/models/video/sql/video/shared/abstract-video-query-builder.ts b/server/models/video/sql/video/shared/abstract-video-query-builder.ts index f0ce69501..cbd57ad8c 100644 --- a/server/models/video/sql/video/shared/abstract-video-query-builder.ts +++ b/server/models/video/sql/video/shared/abstract-video-query-builder.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { createSafeIn } from '@server/models/utils' | ||
4 | import { MUserAccountId } from '@server/types/models' | 3 | import { MUserAccountId } from '@server/types/models' |
5 | import { ActorImageType } from '@shared/models' | 4 | import { ActorImageType } from '@shared/models' |
6 | import { AbstractRunQuery } from '../../../../shared/abstract-run-query' | 5 | import { AbstractRunQuery } from '../../../../shared/abstract-run-query' |
6 | import { createSafeIn } from '../../../../shared' | ||
7 | import { VideoTableAttributes } from './video-table-attributes' | 7 | import { VideoTableAttributes } from './video-table-attributes' |
8 | 8 | ||
9 | /** | 9 | /** |
diff --git a/server/models/video/sql/video/videos-id-list-query-builder.ts b/server/models/video/sql/video/videos-id-list-query-builder.ts index 7c864bf27..62f1855c7 100644 --- a/server/models/video/sql/video/videos-id-list-query-builder.ts +++ b/server/models/video/sql/video/videos-id-list-query-builder.ts | |||
@@ -2,11 +2,12 @@ import { Sequelize, Transaction } from 'sequelize' | |||
2 | import validator from 'validator' | 2 | import validator from 'validator' |
3 | import { exists } from '@server/helpers/custom-validators/misc' | 3 | import { exists } from '@server/helpers/custom-validators/misc' |
4 | import { WEBSERVER } from '@server/initializers/constants' | 4 | import { WEBSERVER } from '@server/initializers/constants' |
5 | import { buildDirectionAndField, createSafeIn, parseRowCountResult } from '@server/models/utils' | 5 | import { buildSortDirectionAndField } from '@server/models/shared' |
6 | import { MUserAccountId, MUserId } from '@server/types/models' | 6 | import { MUserAccountId, MUserId } from '@server/types/models' |
7 | import { forceNumber } from '@shared/core-utils' | ||
7 | import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' | 8 | import { VideoInclude, VideoPrivacy, VideoState } from '@shared/models' |
9 | import { createSafeIn, parseRowCountResult } from '../../../shared' | ||
8 | import { AbstractRunQuery } from '../../../shared/abstract-run-query' | 10 | import { AbstractRunQuery } from '../../../shared/abstract-run-query' |
9 | import { forceNumber } from '@shared/core-utils' | ||
10 | 11 | ||
11 | /** | 12 | /** |
12 | * | 13 | * |
@@ -665,7 +666,7 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery { | |||
665 | } | 666 | } |
666 | 667 | ||
667 | private buildOrder (value: string) { | 668 | private buildOrder (value: string) { |
668 | const { direction, field } = buildDirectionAndField(value) | 669 | const { direction, field } = buildSortDirectionAndField(value) |
669 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) | 670 | if (field.match(/^[a-zA-Z."]+$/) === null) throw new Error('Invalid sort column ' + field) |
670 | 671 | ||
671 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' | 672 | if (field.toLowerCase() === 'random') return 'ORDER BY RANDOM()' |
diff --git a/server/models/video/tag.ts b/server/models/video/tag.ts index 653b9694b..cebde3755 100644 --- a/server/models/video/tag.ts +++ b/server/models/video/tag.ts | |||
@@ -4,7 +4,7 @@ import { MTag } from '@server/types/models' | |||
4 | import { AttributesOnly } from '@shared/typescript-utils' | 4 | import { AttributesOnly } from '@shared/typescript-utils' |
5 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' | 5 | import { VideoPrivacy, VideoState } from '../../../shared/models/videos' |
6 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' | 6 | import { isVideoTagValid } from '../../helpers/custom-validators/videos' |
7 | import { throwIfNotValid } from '../utils' | 7 | import { throwIfNotValid } from '../shared' |
8 | import { VideoModel } from './video' | 8 | import { VideoModel } from './video' |
9 | import { VideoTagModel } from './video-tag' | 9 | import { VideoTagModel } from './video-tag' |
10 | 10 | ||
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index 1cd8224c0..9247d0e2b 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -5,7 +5,7 @@ import { AttributesOnly } from '@shared/typescript-utils' | |||
5 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' | 5 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' |
6 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' | 6 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' |
7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
8 | import { getBlacklistSort, searchAttribute, SortType, throwIfNotValid } from '../utils' | 8 | import { getBlacklistSort, searchAttribute, throwIfNotValid } from '../shared' |
9 | import { ThumbnailModel } from './thumbnail' | 9 | import { ThumbnailModel } from './thumbnail' |
10 | import { VideoModel } from './video' | 10 | import { VideoModel } from './video' |
11 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' | 11 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
@@ -57,7 +57,7 @@ export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlack | |||
57 | static listForApi (parameters: { | 57 | static listForApi (parameters: { |
58 | start: number | 58 | start: number |
59 | count: number | 59 | count: number |
60 | sort: SortType | 60 | sort: string |
61 | search?: string | 61 | search?: string |
62 | type?: VideoBlacklistType | 62 | type?: VideoBlacklistType |
63 | }) { | 63 | }) { |
@@ -67,7 +67,7 @@ export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlack | |||
67 | return { | 67 | return { |
68 | offset: start, | 68 | offset: start, |
69 | limit: count, | 69 | limit: count, |
70 | order: getBlacklistSort(sort.sortModel, sort.sortValue) | 70 | order: getBlacklistSort(sort) |
71 | } | 71 | } |
72 | } | 72 | } |
73 | 73 | ||
diff --git a/server/models/video/video-caption.ts b/server/models/video/video-caption.ts index 5fbcd6e3b..2eaa77407 100644 --- a/server/models/video/video-caption.ts +++ b/server/models/video/video-caption.ts | |||
@@ -23,7 +23,7 @@ import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/vid | |||
23 | import { logger } from '../../helpers/logger' | 23 | import { logger } from '../../helpers/logger' |
24 | import { CONFIG } from '../../initializers/config' | 24 | import { CONFIG } from '../../initializers/config' |
25 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' | 25 | import { CONSTRAINTS_FIELDS, LAZY_STATIC_PATHS, VIDEO_LANGUAGES, WEBSERVER } from '../../initializers/constants' |
26 | import { buildWhereIdOrUUID, throwIfNotValid } from '../utils' | 26 | import { buildWhereIdOrUUID, throwIfNotValid } from '../shared' |
27 | import { VideoModel } from './video' | 27 | import { VideoModel } from './video' |
28 | 28 | ||
29 | export enum ScopeNames { | 29 | export enum ScopeNames { |
diff --git a/server/models/video/video-change-ownership.ts b/server/models/video/video-change-ownership.ts index 1a1b8c88d..2db4b523a 100644 --- a/server/models/video/video-change-ownership.ts +++ b/server/models/video/video-change-ownership.ts | |||
@@ -3,7 +3,7 @@ import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@se | |||
3 | import { AttributesOnly } from '@shared/typescript-utils' | 3 | import { AttributesOnly } from '@shared/typescript-utils' |
4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' | 4 | import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos' |
5 | import { AccountModel } from '../account/account' | 5 | import { AccountModel } from '../account/account' |
6 | import { getSort } from '../utils' | 6 | import { getSort } from '../shared' |
7 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' | 7 | import { ScopeNames as VideoScopeNames, VideoModel } from './video' |
8 | 8 | ||
9 | enum ScopeNames { | 9 | enum ScopeNames { |
diff --git a/server/models/video/video-channel-sync.ts b/server/models/video/video-channel-sync.ts index 6e49cde10..a4cbf51f5 100644 --- a/server/models/video/video-channel-sync.ts +++ b/server/models/video/video-channel-sync.ts | |||
@@ -21,7 +21,7 @@ import { VideoChannelSync, VideoChannelSyncState } from '@shared/models' | |||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 21 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { AccountModel } from '../account/account' | 22 | import { AccountModel } from '../account/account' |
23 | import { UserModel } from '../user/user' | 23 | import { UserModel } from '../user/user' |
24 | import { getChannelSyncSort, throwIfNotValid } from '../utils' | 24 | import { getChannelSyncSort, throwIfNotValid } from '../shared' |
25 | import { VideoChannelModel } from './video-channel' | 25 | import { VideoChannelModel } from './video-channel' |
26 | 26 | ||
27 | @DefaultScope(() => ({ | 27 | @DefaultScope(() => ({ |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 132c8f021..b71f5a197 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -43,8 +43,14 @@ import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | |||
43 | import { ActorFollowModel } from '../actor/actor-follow' | 43 | import { ActorFollowModel } from '../actor/actor-follow' |
44 | import { ActorImageModel } from '../actor/actor-image' | 44 | import { ActorImageModel } from '../actor/actor-image' |
45 | import { ServerModel } from '../server/server' | 45 | import { ServerModel } from '../server/server' |
46 | import { setAsUpdated } from '../shared' | 46 | import { |
47 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 47 | buildServerIdsFollowedBy, |
48 | buildTrigramSearchIndex, | ||
49 | createSimilarityAttribute, | ||
50 | getSort, | ||
51 | setAsUpdated, | ||
52 | throwIfNotValid | ||
53 | } from '../shared' | ||
48 | import { VideoModel } from './video' | 54 | import { VideoModel } from './video' |
49 | import { VideoPlaylistModel } from './video-playlist' | 55 | import { VideoPlaylistModel } from './video-playlist' |
50 | 56 | ||
@@ -831,6 +837,6 @@ export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannel | |||
831 | } | 837 | } |
832 | 838 | ||
833 | setAsUpdated (transaction?: Transaction) { | 839 | setAsUpdated (transaction?: Transaction) { |
834 | return setAsUpdated('videoChannel', this.id, transaction) | 840 | return setAsUpdated({ sequelize: this.sequelize, table: 'videoChannel', id: this.id, transaction }) |
835 | } | 841 | } |
836 | } | 842 | } |
diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index af9614d30..ff5142809 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { FindOptions, Op, Order, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 1 | import { FindOptions, Op, Order, QueryTypes, Sequelize, Transaction } from 'sequelize' |
2 | import { | 2 | import { |
3 | AllowNull, | 3 | AllowNull, |
4 | BelongsTo, | 4 | BelongsTo, |
@@ -13,11 +13,9 @@ import { | |||
13 | Table, | 13 | Table, |
14 | UpdatedAt | 14 | UpdatedAt |
15 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
16 | import { exists } from '@server/helpers/custom-validators/misc' | ||
17 | import { getServerActor } from '@server/models/application/application' | 16 | import { getServerActor } from '@server/models/application/application' |
18 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' | 17 | import { MAccount, MAccountId, MUserAccountId } from '@server/types/models' |
19 | import { uniqify } from '@shared/core-utils' | 18 | import { pick, uniqify } from '@shared/core-utils' |
20 | import { VideoPrivacy } from '@shared/models' | ||
21 | import { AttributesOnly } from '@shared/typescript-utils' | 19 | import { AttributesOnly } from '@shared/typescript-utils' |
22 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' | 20 | import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects' |
23 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' | 21 | import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object' |
@@ -41,61 +39,19 @@ import { | |||
41 | } from '../../types/models/video' | 39 | } from '../../types/models/video' |
42 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' | 40 | import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse' |
43 | import { AccountModel } from '../account/account' | 41 | import { AccountModel } from '../account/account' |
44 | import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor' | 42 | import { ActorModel } from '../actor/actor' |
45 | import { | 43 | import { buildLocalAccountIdsIn, buildSQLAttributes, throwIfNotValid } from '../shared' |
46 | buildBlockedAccountSQL, | 44 | import { ListVideoCommentsOptions, VideoCommentListQueryBuilder } from './sql/comment/video-comment-list-query-builder' |
47 | buildBlockedAccountSQLOptimized, | ||
48 | buildLocalAccountIdsIn, | ||
49 | getCommentSort, | ||
50 | searchAttribute, | ||
51 | throwIfNotValid | ||
52 | } from '../utils' | ||
53 | import { VideoModel } from './video' | 45 | import { VideoModel } from './video' |
54 | import { VideoChannelModel } from './video-channel' | 46 | import { VideoChannelModel } from './video-channel' |
55 | 47 | ||
56 | export enum ScopeNames { | 48 | export enum ScopeNames { |
57 | WITH_ACCOUNT = 'WITH_ACCOUNT', | 49 | WITH_ACCOUNT = 'WITH_ACCOUNT', |
58 | WITH_ACCOUNT_FOR_API = 'WITH_ACCOUNT_FOR_API', | ||
59 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', | 50 | WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', |
60 | WITH_VIDEO = 'WITH_VIDEO', | 51 | WITH_VIDEO = 'WITH_VIDEO' |
61 | ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' | ||
62 | } | 52 | } |
63 | 53 | ||
64 | @Scopes(() => ({ | 54 | @Scopes(() => ({ |
65 | [ScopeNames.ATTRIBUTES_FOR_API]: (blockerAccountIds: number[]) => { | ||
66 | return { | ||
67 | attributes: { | ||
68 | include: [ | ||
69 | [ | ||
70 | Sequelize.literal( | ||
71 | '(' + | ||
72 | 'WITH "blocklist" AS (' + buildBlockedAccountSQL(blockerAccountIds) + ')' + | ||
73 | 'SELECT COUNT("replies"."id") ' + | ||
74 | 'FROM "videoComment" AS "replies" ' + | ||
75 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
76 | 'AND "deletedAt" IS NULL ' + | ||
77 | 'AND "accountId" NOT IN (SELECT "id" FROM "blocklist")' + | ||
78 | ')' | ||
79 | ), | ||
80 | 'totalReplies' | ||
81 | ], | ||
82 | [ | ||
83 | Sequelize.literal( | ||
84 | '(' + | ||
85 | 'SELECT COUNT("replies"."id") ' + | ||
86 | 'FROM "videoComment" AS "replies" ' + | ||
87 | 'INNER JOIN "video" ON "video"."id" = "replies"."videoId" ' + | ||
88 | 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + | ||
89 | 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id" ' + | ||
90 | 'AND "replies"."accountId" = "videoChannel"."accountId"' + | ||
91 | ')' | ||
92 | ), | ||
93 | 'totalRepliesFromVideoAuthor' | ||
94 | ] | ||
95 | ] | ||
96 | } | ||
97 | } as FindOptions | ||
98 | }, | ||
99 | [ScopeNames.WITH_ACCOUNT]: { | 55 | [ScopeNames.WITH_ACCOUNT]: { |
100 | include: [ | 56 | include: [ |
101 | { | 57 | { |
@@ -103,22 +59,6 @@ export enum ScopeNames { | |||
103 | } | 59 | } |
104 | ] | 60 | ] |
105 | }, | 61 | }, |
106 | [ScopeNames.WITH_ACCOUNT_FOR_API]: { | ||
107 | include: [ | ||
108 | { | ||
109 | model: AccountModel.unscoped(), | ||
110 | include: [ | ||
111 | { | ||
112 | attributes: { | ||
113 | exclude: unusedActorAttributesForAPI | ||
114 | }, | ||
115 | model: ActorModel, // Default scope includes avatar and server | ||
116 | required: true | ||
117 | } | ||
118 | ] | ||
119 | } | ||
120 | ] | ||
121 | }, | ||
122 | [ScopeNames.WITH_IN_REPLY_TO]: { | 62 | [ScopeNames.WITH_IN_REPLY_TO]: { |
123 | include: [ | 63 | include: [ |
124 | { | 64 | { |
@@ -252,6 +192,18 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
252 | }) | 192 | }) |
253 | CommentAbuses: VideoCommentAbuseModel[] | 193 | CommentAbuses: VideoCommentAbuseModel[] |
254 | 194 | ||
195 | // --------------------------------------------------------------------------- | ||
196 | |||
197 | static getSQLAttributes (tableName: string, aliasPrefix = '') { | ||
198 | return buildSQLAttributes({ | ||
199 | model: this, | ||
200 | tableName, | ||
201 | aliasPrefix | ||
202 | }) | ||
203 | } | ||
204 | |||
205 | // --------------------------------------------------------------------------- | ||
206 | |||
255 | static loadById (id: number, t?: Transaction): Promise<MComment> { | 207 | static loadById (id: number, t?: Transaction): Promise<MComment> { |
256 | const query: FindOptions = { | 208 | const query: FindOptions = { |
257 | where: { | 209 | where: { |
@@ -319,93 +271,19 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
319 | searchAccount?: string | 271 | searchAccount?: string |
320 | searchVideo?: string | 272 | searchVideo?: string |
321 | }) { | 273 | }) { |
322 | const { start, count, sort, isLocal, search, searchAccount, searchVideo, onLocalVideo } = parameters | 274 | const queryOptions: ListVideoCommentsOptions = { |
275 | ...pick(parameters, [ 'start', 'count', 'sort', 'isLocal', 'search', 'searchVideo', 'searchAccount', 'onLocalVideo' ]), | ||
323 | 276 | ||
324 | const where: WhereOptions = { | 277 | selectType: 'api', |
325 | deletedAt: null | 278 | notDeleted: true |
326 | } | ||
327 | |||
328 | const whereAccount: WhereOptions = {} | ||
329 | const whereActor: WhereOptions = {} | ||
330 | const whereVideo: WhereOptions = {} | ||
331 | |||
332 | if (isLocal === true) { | ||
333 | Object.assign(whereActor, { | ||
334 | serverId: null | ||
335 | }) | ||
336 | } else if (isLocal === false) { | ||
337 | Object.assign(whereActor, { | ||
338 | serverId: { | ||
339 | [Op.ne]: null | ||
340 | } | ||
341 | }) | ||
342 | } | ||
343 | |||
344 | if (search) { | ||
345 | Object.assign(where, { | ||
346 | [Op.or]: [ | ||
347 | searchAttribute(search, 'text'), | ||
348 | searchAttribute(search, '$Account.Actor.preferredUsername$'), | ||
349 | searchAttribute(search, '$Account.name$'), | ||
350 | searchAttribute(search, '$Video.name$') | ||
351 | ] | ||
352 | }) | ||
353 | } | ||
354 | |||
355 | if (searchAccount) { | ||
356 | Object.assign(whereActor, { | ||
357 | [Op.or]: [ | ||
358 | searchAttribute(searchAccount, '$Account.Actor.preferredUsername$'), | ||
359 | searchAttribute(searchAccount, '$Account.name$') | ||
360 | ] | ||
361 | }) | ||
362 | } | ||
363 | |||
364 | if (searchVideo) { | ||
365 | Object.assign(whereVideo, searchAttribute(searchVideo, 'name')) | ||
366 | } | ||
367 | |||
368 | if (exists(onLocalVideo)) { | ||
369 | Object.assign(whereVideo, { remote: !onLocalVideo }) | ||
370 | } | ||
371 | |||
372 | const getQuery = (forCount: boolean) => { | ||
373 | return { | ||
374 | offset: start, | ||
375 | limit: count, | ||
376 | order: getCommentSort(sort), | ||
377 | where, | ||
378 | include: [ | ||
379 | { | ||
380 | model: AccountModel.unscoped(), | ||
381 | required: true, | ||
382 | where: whereAccount, | ||
383 | include: [ | ||
384 | { | ||
385 | attributes: { | ||
386 | exclude: unusedActorAttributesForAPI | ||
387 | }, | ||
388 | model: forCount === true | ||
389 | ? ActorModel.unscoped() // Default scope includes avatar and server | ||
390 | : ActorModel, | ||
391 | required: true, | ||
392 | where: whereActor | ||
393 | } | ||
394 | ] | ||
395 | }, | ||
396 | { | ||
397 | model: VideoModel.unscoped(), | ||
398 | required: true, | ||
399 | where: whereVideo | ||
400 | } | ||
401 | ] | ||
402 | } | ||
403 | } | 279 | } |
404 | 280 | ||
405 | return Promise.all([ | 281 | return Promise.all([ |
406 | VideoCommentModel.count(getQuery(true)), | 282 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(), |
407 | VideoCommentModel.findAll(getQuery(false)) | 283 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
408 | ]).then(([ total, data ]) => ({ total, data })) | 284 | ]).then(([ rows, count ]) => { |
285 | return { total: count, data: rows } | ||
286 | }) | ||
409 | } | 287 | } |
410 | 288 | ||
411 | static async listThreadsForApi (parameters: { | 289 | static async listThreadsForApi (parameters: { |
@@ -416,67 +294,40 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
416 | sort: string | 294 | sort: string |
417 | user?: MUserAccountId | 295 | user?: MUserAccountId |
418 | }) { | 296 | }) { |
419 | const { videoId, isVideoOwned, start, count, sort, user } = parameters | 297 | const { videoId, user } = parameters |
420 | 298 | ||
421 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) | 299 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user }) |
422 | 300 | ||
423 | const accountBlockedWhere = { | 301 | const commonOptions: ListVideoCommentsOptions = { |
424 | accountId: { | 302 | selectType: 'api', |
425 | [Op.notIn]: Sequelize.literal( | 303 | videoId, |
426 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | 304 | blockerAccountIds |
427 | ) | ||
428 | } | ||
429 | } | 305 | } |
430 | 306 | ||
431 | const queryList = { | 307 | const listOptions: ListVideoCommentsOptions = { |
432 | offset: start, | 308 | ...commonOptions, |
433 | limit: count, | 309 | ...pick(parameters, [ 'sort', 'start', 'count' ]), |
434 | order: getCommentSort(sort), | 310 | |
435 | where: { | 311 | isThread: true, |
436 | [Op.and]: [ | 312 | includeReplyCounters: true |
437 | { | ||
438 | videoId | ||
439 | }, | ||
440 | { | ||
441 | inReplyToCommentId: null | ||
442 | }, | ||
443 | { | ||
444 | [Op.or]: [ | ||
445 | accountBlockedWhere, | ||
446 | { | ||
447 | accountId: null | ||
448 | } | ||
449 | ] | ||
450 | } | ||
451 | ] | ||
452 | } | ||
453 | } | 313 | } |
454 | 314 | ||
455 | const findScopesList: (string | ScopeOptions)[] = [ | 315 | const countOptions: ListVideoCommentsOptions = { |
456 | ScopeNames.WITH_ACCOUNT_FOR_API, | 316 | ...commonOptions, |
457 | { | ||
458 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
459 | } | ||
460 | ] | ||
461 | 317 | ||
462 | const countScopesList: ScopeOptions[] = [ | 318 | isThread: true |
463 | { | 319 | } |
464 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | ||
465 | } | ||
466 | ] | ||
467 | 320 | ||
468 | const notDeletedQueryCount = { | 321 | const notDeletedCountOptions: ListVideoCommentsOptions = { |
469 | where: { | 322 | ...commonOptions, |
470 | videoId, | 323 | |
471 | deletedAt: null, | 324 | notDeleted: true |
472 | ...accountBlockedWhere | ||
473 | } | ||
474 | } | 325 | } |
475 | 326 | ||
476 | return Promise.all([ | 327 | return Promise.all([ |
477 | VideoCommentModel.scope(findScopesList).findAll(queryList), | 328 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, listOptions).listComments<MCommentAdminFormattable>(), |
478 | VideoCommentModel.scope(countScopesList).count(queryList), | 329 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, countOptions).countComments(), |
479 | VideoCommentModel.count(notDeletedQueryCount) | 330 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, notDeletedCountOptions).countComments() |
480 | ]).then(([ rows, count, totalNotDeletedComments ]) => { | 331 | ]).then(([ rows, count, totalNotDeletedComments ]) => { |
481 | return { total: count, data: rows, totalNotDeletedComments } | 332 | return { total: count, data: rows, totalNotDeletedComments } |
482 | }) | 333 | }) |
@@ -484,54 +335,29 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
484 | 335 | ||
485 | static async listThreadCommentsForApi (parameters: { | 336 | static async listThreadCommentsForApi (parameters: { |
486 | videoId: number | 337 | videoId: number |
487 | isVideoOwned: boolean | ||
488 | threadId: number | 338 | threadId: number |
489 | user?: MUserAccountId | 339 | user?: MUserAccountId |
490 | }) { | 340 | }) { |
491 | const { videoId, threadId, user, isVideoOwned } = parameters | 341 | const { user } = parameters |
492 | 342 | ||
493 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ videoId, user, isVideoOwned }) | 343 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user }) |
494 | 344 | ||
495 | const query = { | 345 | const queryOptions: ListVideoCommentsOptions = { |
496 | order: [ [ 'createdAt', 'ASC' ], [ 'updatedAt', 'ASC' ] ] as Order, | 346 | ...pick(parameters, [ 'videoId', 'threadId' ]), |
497 | where: { | ||
498 | videoId, | ||
499 | [Op.and]: [ | ||
500 | { | ||
501 | [Op.or]: [ | ||
502 | { id: threadId }, | ||
503 | { originCommentId: threadId } | ||
504 | ] | ||
505 | }, | ||
506 | { | ||
507 | [Op.or]: [ | ||
508 | { | ||
509 | accountId: { | ||
510 | [Op.notIn]: Sequelize.literal( | ||
511 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | ||
512 | ) | ||
513 | } | ||
514 | }, | ||
515 | { | ||
516 | accountId: null | ||
517 | } | ||
518 | ] | ||
519 | } | ||
520 | ] | ||
521 | } | ||
522 | } | ||
523 | 347 | ||
524 | const scopes: any[] = [ | 348 | selectType: 'api', |
525 | ScopeNames.WITH_ACCOUNT_FOR_API, | 349 | sort: 'createdAt', |
526 | { | 350 | |
527 | method: [ ScopeNames.ATTRIBUTES_FOR_API, blockerAccountIds ] | 351 | blockerAccountIds, |
528 | } | 352 | includeReplyCounters: true |
529 | ] | 353 | } |
530 | 354 | ||
531 | return Promise.all([ | 355 | return Promise.all([ |
532 | VideoCommentModel.count(query), | 356 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentAdminFormattable>(), |
533 | VideoCommentModel.scope(scopes).findAll(query) | 357 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
534 | ]).then(([ total, data ]) => ({ total, data })) | 358 | ]).then(([ rows, count ]) => { |
359 | return { total: count, data: rows } | ||
360 | }) | ||
535 | } | 361 | } |
536 | 362 | ||
537 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { | 363 | static listThreadParentComments (comment: MCommentId, t: Transaction, order: 'ASC' | 'DESC' = 'ASC'): Promise<MCommentOwner[]> { |
@@ -559,31 +385,31 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
559 | .findAll(query) | 385 | .findAll(query) |
560 | } | 386 | } |
561 | 387 | ||
562 | static async listAndCountByVideoForAP (video: MVideoImmutable, start: number, count: number, t?: Transaction) { | 388 | static async listAndCountByVideoForAP (parameters: { |
563 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ | 389 | video: MVideoImmutable |
390 | start: number | ||
391 | count: number | ||
392 | }) { | ||
393 | const { video } = parameters | ||
394 | |||
395 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null }) | ||
396 | |||
397 | const queryOptions: ListVideoCommentsOptions = { | ||
398 | ...pick(parameters, [ 'start', 'count' ]), | ||
399 | |||
400 | selectType: 'comment-only', | ||
564 | videoId: video.id, | 401 | videoId: video.id, |
565 | isVideoOwned: video.isOwned() | 402 | sort: 'createdAt', |
566 | }) | ||
567 | 403 | ||
568 | const query = { | 404 | blockerAccountIds |
569 | order: [ [ 'createdAt', 'ASC' ] ] as Order, | ||
570 | offset: start, | ||
571 | limit: count, | ||
572 | where: { | ||
573 | videoId: video.id, | ||
574 | accountId: { | ||
575 | [Op.notIn]: Sequelize.literal( | ||
576 | '(' + buildBlockedAccountSQL(blockerAccountIds) + ')' | ||
577 | ) | ||
578 | } | ||
579 | }, | ||
580 | transaction: t | ||
581 | } | 405 | } |
582 | 406 | ||
583 | return Promise.all([ | 407 | return Promise.all([ |
584 | VideoCommentModel.count(query), | 408 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>(), |
585 | VideoCommentModel.findAll<MComment>(query) | 409 | new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).countComments() |
586 | ]).then(([ total, data ]) => ({ total, data })) | 410 | ]).then(([ rows, count ]) => { |
411 | return { total: count, data: rows } | ||
412 | }) | ||
587 | } | 413 | } |
588 | 414 | ||
589 | static async listForFeed (parameters: { | 415 | static async listForFeed (parameters: { |
@@ -592,97 +418,36 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
592 | videoId?: number | 418 | videoId?: number |
593 | accountId?: number | 419 | accountId?: number |
594 | videoChannelId?: number | 420 | videoChannelId?: number |
595 | }): Promise<MCommentOwnerVideoFeed[]> { | 421 | }) { |
596 | const serverActor = await getServerActor() | 422 | const blockerAccountIds = await VideoCommentModel.buildBlockerAccountIds({ user: null }) |
597 | const { start, count, videoId, accountId, videoChannelId } = parameters | ||
598 | |||
599 | const whereAnd: WhereOptions[] = buildBlockedAccountSQLOptimized( | ||
600 | '"VideoCommentModel"."accountId"', | ||
601 | [ serverActor.Account.id, '"Video->VideoChannel"."accountId"' ] | ||
602 | ) | ||
603 | 423 | ||
604 | if (accountId) { | 424 | const queryOptions: ListVideoCommentsOptions = { |
605 | whereAnd.push({ | 425 | ...pick(parameters, [ 'start', 'count', 'accountId', 'videoId', 'videoChannelId' ]), |
606 | accountId | ||
607 | }) | ||
608 | } | ||
609 | 426 | ||
610 | const accountWhere = { | 427 | selectType: 'feed', |
611 | [Op.and]: whereAnd | ||
612 | } | ||
613 | 428 | ||
614 | const videoChannelWhere = videoChannelId ? { id: videoChannelId } : undefined | 429 | sort: '-createdAt', |
430 | onPublicVideo: true, | ||
431 | notDeleted: true, | ||
615 | 432 | ||
616 | const query = { | 433 | blockerAccountIds |
617 | order: [ [ 'createdAt', 'DESC' ] ] as Order, | ||
618 | offset: start, | ||
619 | limit: count, | ||
620 | where: { | ||
621 | deletedAt: null, | ||
622 | accountId: accountWhere | ||
623 | }, | ||
624 | include: [ | ||
625 | { | ||
626 | attributes: [ 'name', 'uuid' ], | ||
627 | model: VideoModel.unscoped(), | ||
628 | required: true, | ||
629 | where: { | ||
630 | privacy: VideoPrivacy.PUBLIC | ||
631 | }, | ||
632 | include: [ | ||
633 | { | ||
634 | attributes: [ 'accountId' ], | ||
635 | model: VideoChannelModel.unscoped(), | ||
636 | required: true, | ||
637 | where: videoChannelWhere | ||
638 | } | ||
639 | ] | ||
640 | } | ||
641 | ] | ||
642 | } | 434 | } |
643 | 435 | ||
644 | if (videoId) query.where['videoId'] = videoId | 436 | return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MCommentOwnerVideoFeed>() |
645 | |||
646 | return VideoCommentModel | ||
647 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
648 | .findAll(query) | ||
649 | } | 437 | } |
650 | 438 | ||
651 | static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { | 439 | static listForBulkDelete (ofAccount: MAccount, filter: { onVideosOfAccount?: MAccountId } = {}) { |
652 | const accountWhere = filter.onVideosOfAccount | 440 | const queryOptions: ListVideoCommentsOptions = { |
653 | ? { id: filter.onVideosOfAccount.id } | 441 | selectType: 'comment-only', |
654 | : {} | ||
655 | 442 | ||
656 | const query = { | 443 | accountId: ofAccount.id, |
657 | limit: 1000, | 444 | videoAccountOwnerId: filter.onVideosOfAccount?.id, |
658 | where: { | 445 | |
659 | deletedAt: null, | 446 | notDeleted: true, |
660 | accountId: ofAccount.id | 447 | count: 5000 |
661 | }, | ||
662 | include: [ | ||
663 | { | ||
664 | model: VideoModel, | ||
665 | required: true, | ||
666 | include: [ | ||
667 | { | ||
668 | model: VideoChannelModel, | ||
669 | required: true, | ||
670 | include: [ | ||
671 | { | ||
672 | model: AccountModel, | ||
673 | required: true, | ||
674 | where: accountWhere | ||
675 | } | ||
676 | ] | ||
677 | } | ||
678 | ] | ||
679 | } | ||
680 | ] | ||
681 | } | 448 | } |
682 | 449 | ||
683 | return VideoCommentModel | 450 | return new VideoCommentListQueryBuilder(VideoCommentModel.sequelize, queryOptions).listComments<MComment>() |
684 | .scope([ ScopeNames.WITH_ACCOUNT ]) | ||
685 | .findAll(query) | ||
686 | } | 451 | } |
687 | 452 | ||
688 | static async getStats () { | 453 | static async getStats () { |
@@ -750,9 +515,7 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
750 | } | 515 | } |
751 | 516 | ||
752 | isOwned () { | 517 | isOwned () { |
753 | if (!this.Account) { | 518 | if (!this.Account) return false |
754 | return false | ||
755 | } | ||
756 | 519 | ||
757 | return this.Account.isOwned() | 520 | return this.Account.isOwned() |
758 | } | 521 | } |
@@ -906,22 +669,15 @@ export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoComment | |||
906 | } | 669 | } |
907 | 670 | ||
908 | private static async buildBlockerAccountIds (options: { | 671 | private static async buildBlockerAccountIds (options: { |
909 | videoId: number | 672 | user: MUserAccountId |
910 | isVideoOwned: boolean | 673 | }): Promise<number[]> { |
911 | user?: MUserAccountId | 674 | const { user } = options |
912 | }) { | ||
913 | const { videoId, user, isVideoOwned } = options | ||
914 | 675 | ||
915 | const serverActor = await getServerActor() | 676 | const serverActor = await getServerActor() |
916 | const blockerAccountIds = [ serverActor.Account.id ] | 677 | const blockerAccountIds = [ serverActor.Account.id ] |
917 | 678 | ||
918 | if (user) blockerAccountIds.push(user.Account.id) | 679 | if (user) blockerAccountIds.push(user.Account.id) |
919 | 680 | ||
920 | if (isVideoOwned) { | ||
921 | const videoOwnerAccount = await AccountModel.loadAccountIdFromVideo(videoId) | ||
922 | if (videoOwnerAccount) blockerAccountIds.push(videoOwnerAccount.id) | ||
923 | } | ||
924 | |||
925 | return blockerAccountIds | 681 | return blockerAccountIds |
926 | } | 682 | } |
927 | } | 683 | } |
diff --git a/server/models/video/video-file.ts b/server/models/video/video-file.ts index 9c4e6d078..07bc13de1 100644 --- a/server/models/video/video-file.ts +++ b/server/models/video/video-file.ts | |||
@@ -21,6 +21,7 @@ import { | |||
21 | import validator from 'validator' | 21 | import validator from 'validator' |
22 | import { logger } from '@server/helpers/logger' | 22 | import { logger } from '@server/helpers/logger' |
23 | import { extractVideo } from '@server/helpers/video' | 23 | import { extractVideo } from '@server/helpers/video' |
24 | import { CONFIG } from '@server/initializers/config' | ||
24 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' | 25 | import { buildRemoteVideoBaseUrl } from '@server/lib/activitypub/url' |
25 | import { | 26 | import { |
26 | getHLSPrivateFileUrl, | 27 | getHLSPrivateFileUrl, |
@@ -50,11 +51,9 @@ import { | |||
50 | } from '../../initializers/constants' | 51 | } from '../../initializers/constants' |
51 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' | 52 | import { MVideoFile, MVideoFileStreamingPlaylistVideo, MVideoFileVideo } from '../../types/models/video/video-file' |
52 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 53 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
53 | import { doesExist } from '../shared' | 54 | import { doesExist, parseAggregateResult, throwIfNotValid } from '../shared' |
54 | import { parseAggregateResult, throwIfNotValid } from '../utils' | ||
55 | import { VideoModel } from './video' | 55 | import { VideoModel } from './video' |
56 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 56 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
57 | import { CONFIG } from '@server/initializers/config' | ||
58 | 57 | ||
59 | export enum ScopeNames { | 58 | export enum ScopeNames { |
60 | WITH_VIDEO = 'WITH_VIDEO', | 59 | WITH_VIDEO = 'WITH_VIDEO', |
@@ -266,7 +265,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
266 | static doesInfohashExist (infoHash: string) { | 265 | static doesInfohashExist (infoHash: string) { |
267 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' | 266 | const query = 'SELECT 1 FROM "videoFile" WHERE "infoHash" = $infoHash LIMIT 1' |
268 | 267 | ||
269 | return doesExist(query, { infoHash }) | 268 | return doesExist(this.sequelize, query, { infoHash }) |
270 | } | 269 | } |
271 | 270 | ||
272 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { | 271 | static async doesVideoExistForVideoFile (id: number, videoIdOrUUID: number | string) { |
@@ -282,14 +281,14 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
282 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + | 281 | 'LEFT JOIN "video" "hlsVideo" ON "hlsVideo"."id" = "videoStreamingPlaylist"."videoId" AND "hlsVideo"."remote" IS FALSE ' + |
283 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' | 282 | 'WHERE "torrentFilename" = $filename AND ("hlsVideo"."id" IS NOT NULL OR "webtorrent"."id" IS NOT NULL) LIMIT 1' |
284 | 283 | ||
285 | return doesExist(query, { filename }) | 284 | return doesExist(this.sequelize, query, { filename }) |
286 | } | 285 | } |
287 | 286 | ||
288 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { | 287 | static async doesOwnedWebTorrentVideoFileExist (filename: string) { |
289 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + | 288 | const query = 'SELECT 1 FROM "videoFile" INNER JOIN "video" ON "video"."id" = "videoFile"."videoId" AND "video"."remote" IS FALSE ' + |
290 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 289 | `WHERE "filename" = $filename AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
291 | 290 | ||
292 | return doesExist(query, { filename }) | 291 | return doesExist(this.sequelize, query, { filename }) |
293 | } | 292 | } |
294 | 293 | ||
295 | static loadByFilename (filename: string) { | 294 | static loadByFilename (filename: string) { |
@@ -439,7 +438,7 @@ export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel> | |||
439 | if (!element) return videoFile.save({ transaction }) | 438 | if (!element) return videoFile.save({ transaction }) |
440 | 439 | ||
441 | for (const k of Object.keys(videoFile.toJSON())) { | 440 | for (const k of Object.keys(videoFile.toJSON())) { |
442 | element[k] = videoFile[k] | 441 | element.set(k, videoFile[k]) |
443 | } | 442 | } |
444 | 443 | ||
445 | return element.save({ transaction }) | 444 | return element.save({ transaction }) |
diff --git a/server/models/video/video-import.ts b/server/models/video/video-import.ts index da6b92c7a..c040e0fda 100644 --- a/server/models/video/video-import.ts +++ b/server/models/video/video-import.ts | |||
@@ -22,7 +22,7 @@ import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../help | |||
22 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' | 22 | import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos' |
23 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' | 23 | import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants' |
24 | import { UserModel } from '../user/user' | 24 | import { UserModel } from '../user/user' |
25 | import { getSort, searchAttribute, throwIfNotValid } from '../utils' | 25 | import { getSort, searchAttribute, throwIfNotValid } from '../shared' |
26 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' | 26 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' |
27 | import { VideoChannelSyncModel } from './video-channel-sync' | 27 | import { VideoChannelSyncModel } from './video-channel-sync' |
28 | 28 | ||
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index 7181b5599..48f4ed5a9 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -31,7 +31,7 @@ import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/ | |||
31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 31 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
32 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 32 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
33 | import { AccountModel } from '../account/account' | 33 | import { AccountModel } from '../account/account' |
34 | import { getSort, throwIfNotValid } from '../utils' | 34 | import { getSort, throwIfNotValid } from '../shared' |
35 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' | 35 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
36 | import { VideoPlaylistModel } from './video-playlist' | 36 | import { VideoPlaylistModel } from './video-playlist' |
37 | 37 | ||
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 8bbe54c49..faf4bea78 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -21,12 +21,8 @@ import { activityPubCollectionPagination } from '@server/lib/activitypub/collect | |||
21 | import { MAccountId, MChannelId } from '@server/types/models' | 21 | import { MAccountId, MChannelId } from '@server/types/models' |
22 | import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' | 22 | import { buildPlaylistEmbedPath, buildPlaylistWatchPath, pick } from '@shared/core-utils' |
23 | import { buildUUID, uuidToShort } from '@shared/extra-utils' | 23 | import { buildUUID, uuidToShort } from '@shared/extra-utils' |
24 | import { ActivityIconObject, PlaylistObject, VideoPlaylist, VideoPlaylistPrivacy, VideoPlaylistType } from '@shared/models' | ||
24 | import { AttributesOnly } from '@shared/typescript-utils' | 25 | import { AttributesOnly } from '@shared/typescript-utils' |
25 | import { ActivityIconObject } from '../../../shared/models/activitypub/objects' | ||
26 | import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object' | ||
27 | import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model' | ||
28 | import { VideoPlaylistType } from '../../../shared/models/videos/playlist/video-playlist-type.model' | ||
29 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | ||
30 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 26 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
31 | import { | 27 | import { |
32 | isVideoPlaylistDescriptionValid, | 28 | isVideoPlaylistDescriptionValid, |
@@ -53,7 +49,6 @@ import { | |||
53 | } from '../../types/models/video/video-playlist' | 49 | } from '../../types/models/video/video-playlist' |
54 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' | 50 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
55 | import { ActorModel } from '../actor/actor' | 51 | import { ActorModel } from '../actor/actor' |
56 | import { setAsUpdated } from '../shared' | ||
57 | import { | 52 | import { |
58 | buildServerIdsFollowedBy, | 53 | buildServerIdsFollowedBy, |
59 | buildTrigramSearchIndex, | 54 | buildTrigramSearchIndex, |
@@ -61,8 +56,9 @@ import { | |||
61 | createSimilarityAttribute, | 56 | createSimilarityAttribute, |
62 | getPlaylistSort, | 57 | getPlaylistSort, |
63 | isOutdated, | 58 | isOutdated, |
59 | setAsUpdated, | ||
64 | throwIfNotValid | 60 | throwIfNotValid |
65 | } from '../utils' | 61 | } from '../shared' |
66 | import { ThumbnailModel } from './thumbnail' | 62 | import { ThumbnailModel } from './thumbnail' |
67 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 63 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
68 | import { VideoPlaylistElementModel } from './video-playlist-element' | 64 | import { VideoPlaylistElementModel } from './video-playlist-element' |
@@ -641,7 +637,7 @@ export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlayli | |||
641 | } | 637 | } |
642 | 638 | ||
643 | setAsRefreshed () { | 639 | setAsRefreshed () { |
644 | return setAsUpdated('videoPlaylist', this.id) | 640 | return setAsUpdated({ sequelize: this.sequelize, table: 'videoPlaylist', id: this.id }) |
645 | } | 641 | } |
646 | 642 | ||
647 | setVideosLength (videosLength: number) { | 643 | setVideosLength (videosLength: number) { |
diff --git a/server/models/video/video-share.ts b/server/models/video/video-share.ts index f2190037e..b4de2b20f 100644 --- a/server/models/video/video-share.ts +++ b/server/models/video/video-share.ts | |||
@@ -7,7 +7,7 @@ import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | |||
7 | import { MActorDefault, MActorFollowersUrl, MActorId } from '../../types/models' | 7 | import { MActorDefault, MActorFollowersUrl, MActorId } from '../../types/models' |
8 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' | 8 | import { MVideoShareActor, MVideoShareFull } from '../../types/models/video' |
9 | import { ActorModel } from '../actor/actor' | 9 | import { ActorModel } from '../actor/actor' |
10 | import { buildLocalActorIdsIn, throwIfNotValid } from '../utils' | 10 | import { buildLocalActorIdsIn, throwIfNotValid } from '../shared' |
11 | import { VideoModel } from './video' | 11 | import { VideoModel } from './video' |
12 | 12 | ||
13 | enum ScopeNames { | 13 | enum ScopeNames { |
diff --git a/server/models/video/video-streaming-playlist.ts b/server/models/video/video-streaming-playlist.ts index 0386edf28..a85c79c9f 100644 --- a/server/models/video/video-streaming-playlist.ts +++ b/server/models/video/video-streaming-playlist.ts | |||
@@ -37,8 +37,7 @@ import { | |||
37 | WEBSERVER | 37 | WEBSERVER |
38 | } from '../../initializers/constants' | 38 | } from '../../initializers/constants' |
39 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' | 39 | import { VideoRedundancyModel } from '../redundancy/video-redundancy' |
40 | import { doesExist } from '../shared' | 40 | import { doesExist, throwIfNotValid } from '../shared' |
41 | import { throwIfNotValid } from '../utils' | ||
42 | import { VideoModel } from './video' | 41 | import { VideoModel } from './video' |
43 | 42 | ||
44 | @Table({ | 43 | @Table({ |
@@ -138,7 +137,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
138 | static doesInfohashExist (infoHash: string) { | 137 | static doesInfohashExist (infoHash: string) { |
139 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' | 138 | const query = 'SELECT 1 FROM "videoStreamingPlaylist" WHERE $infoHash = ANY("p2pMediaLoaderInfohashes") LIMIT 1' |
140 | 139 | ||
141 | return doesExist(query, { infoHash }) | 140 | return doesExist(this.sequelize, query, { infoHash }) |
142 | } | 141 | } |
143 | 142 | ||
144 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { | 143 | static buildP2PMediaLoaderInfoHashes (playlistUrl: string, files: unknown[]) { |
@@ -237,7 +236,7 @@ export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<Vi | |||
237 | `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` + | 236 | `AND "video"."remote" IS FALSE AND "video"."uuid" = $videoUUID ` + |
238 | `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` | 237 | `AND "storage" = ${VideoStorage.FILE_SYSTEM} LIMIT 1` |
239 | 238 | ||
240 | return doesExist(query, { videoUUID }) | 239 | return doesExist(this.sequelize, query, { videoUUID }) |
241 | } | 240 | } |
242 | 241 | ||
243 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { | 242 | assignP2PMediaLoaderInfoHashes (video: MVideo, files: unknown[]) { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 56cc45cfe..1a10d2da2 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -32,7 +32,7 @@ import { getHLSDirectory, getHLSRedundancyDirectory, getHlsResolutionPlaylistFil | |||
32 | import { VideoPathManager } from '@server/lib/video-path-manager' | 32 | import { VideoPathManager } from '@server/lib/video-path-manager' |
33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' | 33 | import { isVideoInPrivateDirectory } from '@server/lib/video-privacy' |
34 | import { getServerActor } from '@server/models/application/application' | 34 | import { getServerActor } from '@server/models/application/application' |
35 | import { ModelCache } from '@server/models/model-cache' | 35 | import { ModelCache } from '@server/models/shared/model-cache' |
36 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' | 36 | import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@shared/core-utils' |
37 | import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' | 37 | import { ffprobePromise, getAudioStream, hasAudioStream, uuidToShort } from '@shared/extra-utils' |
38 | import { | 38 | import { |
@@ -103,10 +103,9 @@ import { VideoRedundancyModel } from '../redundancy/video-redundancy' | |||
103 | import { ServerModel } from '../server/server' | 103 | import { ServerModel } from '../server/server' |
104 | import { TrackerModel } from '../server/tracker' | 104 | import { TrackerModel } from '../server/tracker' |
105 | import { VideoTrackerModel } from '../server/video-tracker' | 105 | import { VideoTrackerModel } from '../server/video-tracker' |
106 | import { setAsUpdated } from '../shared' | 106 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, setAsUpdated, throwIfNotValid } from '../shared' |
107 | import { UserModel } from '../user/user' | 107 | import { UserModel } from '../user/user' |
108 | import { UserVideoHistoryModel } from '../user/user-video-history' | 108 | import { UserVideoHistoryModel } from '../user/user-video-history' |
109 | import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils' | ||
110 | import { VideoViewModel } from '../view/video-view' | 109 | import { VideoViewModel } from '../view/video-view' |
111 | import { | 110 | import { |
112 | videoFilesModelToFormattedJSON, | 111 | videoFilesModelToFormattedJSON, |
@@ -1871,7 +1870,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> { | |||
1871 | } | 1870 | } |
1872 | 1871 | ||
1873 | setAsRefreshed (transaction?: Transaction) { | 1872 | setAsRefreshed (transaction?: Transaction) { |
1874 | return setAsUpdated('video', this.id, transaction) | 1873 | return setAsUpdated({ sequelize: this.sequelize, table: 'video', id: this.id, transaction }) |
1875 | } | 1874 | } |
1876 | 1875 | ||
1877 | // --------------------------------------------------------------------------- | 1876 | // --------------------------------------------------------------------------- |
diff --git a/server/tests/api/activitypub/cleaner.ts b/server/tests/api/activitypub/cleaner.ts index eb6779123..1c1495022 100644 --- a/server/tests/api/activitypub/cleaner.ts +++ b/server/tests/api/activitypub/cleaner.ts | |||
@@ -148,7 +148,7 @@ describe('Test AP cleaner', function () { | |||
148 | it('Should destroy server 3 internal shares and correctly clean them', async function () { | 148 | it('Should destroy server 3 internal shares and correctly clean them', async function () { |
149 | this.timeout(20000) | 149 | this.timeout(20000) |
150 | 150 | ||
151 | const preCount = await servers[0].sql.getCount('videoShare') | 151 | const preCount = await servers[0].sql.getVideoShareCount() |
152 | expect(preCount).to.equal(6) | 152 | expect(preCount).to.equal(6) |
153 | 153 | ||
154 | await servers[2].sql.deleteAll('videoShare') | 154 | await servers[2].sql.deleteAll('videoShare') |
@@ -156,7 +156,7 @@ describe('Test AP cleaner', function () { | |||
156 | await waitJobs(servers) | 156 | await waitJobs(servers) |
157 | 157 | ||
158 | // Still 6 because we don't have remote shares on local videos | 158 | // Still 6 because we don't have remote shares on local videos |
159 | const postCount = await servers[0].sql.getCount('videoShare') | 159 | const postCount = await servers[0].sql.getVideoShareCount() |
160 | expect(postCount).to.equal(6) | 160 | expect(postCount).to.equal(6) |
161 | }) | 161 | }) |
162 | 162 | ||
@@ -185,7 +185,7 @@ describe('Test AP cleaner', function () { | |||
185 | async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') { | 185 | async function check (like: string, ofServerUrl: string, urlSuffix: string, remote: 'true' | 'false') { |
186 | const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` + | 186 | const query = `SELECT "videoId", "accountVideoRate".url FROM "accountVideoRate" ` + |
187 | `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'` | 187 | `INNER JOIN video ON "accountVideoRate"."videoId" = video.id AND remote IS ${remote} WHERE "accountVideoRate"."url" LIKE '${like}'` |
188 | const res = await servers[0].sql.selectQuery(query) | 188 | const res = await servers[0].sql.selectQuery<{ url: string }>(query) |
189 | 189 | ||
190 | for (const rate of res) { | 190 | for (const rate of res) { |
191 | const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`) | 191 | const matcher = new RegExp(`^${ofServerUrl}/accounts/root/dislikes/\\d+${urlSuffix}$`) |
@@ -231,7 +231,7 @@ describe('Test AP cleaner', function () { | |||
231 | const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` + | 231 | const query = `SELECT "videoId", "videoComment".url, uuid as "videoUUID" FROM "videoComment" ` + |
232 | `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'` | 232 | `INNER JOIN video ON "videoComment"."videoId" = video.id AND remote IS ${remote} WHERE "videoComment"."url" LIKE '${like}'` |
233 | 233 | ||
234 | const res = await servers[0].sql.selectQuery(query) | 234 | const res = await servers[0].sql.selectQuery<{ url: string, videoUUID: string }>(query) |
235 | 235 | ||
236 | for (const comment of res) { | 236 | for (const comment of res) { |
237 | const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`) | 237 | const matcher = new RegExp(`${ofServerUrl}/videos/watch/${comment.videoUUID}/comments/\\d+${urlSuffix}`) |
diff --git a/server/tests/api/check-params/redundancy.ts b/server/tests/api/check-params/redundancy.ts index 908407b9a..73dfd489d 100644 --- a/server/tests/api/check-params/redundancy.ts +++ b/server/tests/api/check-params/redundancy.ts | |||
@@ -24,7 +24,7 @@ describe('Test server redundancy API validators', function () { | |||
24 | // --------------------------------------------------------------- | 24 | // --------------------------------------------------------------- |
25 | 25 | ||
26 | before(async function () { | 26 | before(async function () { |
27 | this.timeout(80000) | 27 | this.timeout(160000) |
28 | 28 | ||
29 | servers = await createMultipleServers(2) | 29 | servers = await createMultipleServers(2) |
30 | 30 | ||
diff --git a/server/tests/api/live/live-fast-restream.ts b/server/tests/api/live/live-fast-restream.ts index c0bb8d529..f6959b83c 100644 --- a/server/tests/api/live/live-fast-restream.ts +++ b/server/tests/api/live/live-fast-restream.ts | |||
@@ -78,9 +78,15 @@ describe('Fast restream in live', function () { | |||
78 | const video = await server.videos.get({ id: liveId }) | 78 | const video = await server.videos.get({ id: liveId }) |
79 | expect(video.streamingPlaylists).to.have.lengthOf(1) | 79 | expect(video.streamingPlaylists).to.have.lengthOf(1) |
80 | 80 | ||
81 | await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 }) | 81 | try { |
82 | await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) | 82 | await server.live.getSegmentFile({ videoUUID: liveId, segment: 0, playlistNumber: 0 }) |
83 | await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | 83 | await makeRawRequest({ url: video.streamingPlaylists[0].playlistUrl, expectedStatus: HttpStatusCode.OK_200 }) |
84 | await makeRawRequest({ url: video.streamingPlaylists[0].segmentsSha256Url, expectedStatus: HttpStatusCode.OK_200 }) | ||
85 | } catch (err) { | ||
86 | // FIXME: try to debug error in CI "Unexpected end of JSON input" | ||
87 | console.error(err) | ||
88 | throw err | ||
89 | } | ||
84 | 90 | ||
85 | await wait(100) | 91 | await wait(100) |
86 | } | 92 | } |
@@ -129,7 +135,7 @@ describe('Fast restream in live', function () { | |||
129 | await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) | 135 | await server.config.enableLive({ allowReplay: true, transcoding: true, resolutions: 'min' }) |
130 | }) | 136 | }) |
131 | 137 | ||
132 | it('Should correctly fast reastream in a permanent live with and without save replay', async function () { | 138 | it('Should correctly fast restream in a permanent live with and without save replay', async function () { |
133 | this.timeout(480000) | 139 | this.timeout(480000) |
134 | 140 | ||
135 | // A test can take a long time, so prefer to run them in parallel | 141 | // A test can take a long time, so prefer to run them in parallel |
diff --git a/server/tests/api/notifications/moderation-notifications.ts b/server/tests/api/notifications/moderation-notifications.ts index b127a7a31..c7b9b5fb0 100644 --- a/server/tests/api/notifications/moderation-notifications.ts +++ b/server/tests/api/notifications/moderation-notifications.ts | |||
@@ -34,7 +34,7 @@ describe('Test moderation notifications', function () { | |||
34 | let emails: object[] = [] | 34 | let emails: object[] = [] |
35 | 35 | ||
36 | before(async function () { | 36 | before(async function () { |
37 | this.timeout(120000) | 37 | this.timeout(50000) |
38 | 38 | ||
39 | const res = await prepareNotificationsTest(3) | 39 | const res = await prepareNotificationsTest(3) |
40 | emails = res.emails | 40 | emails = res.emails |
@@ -60,7 +60,7 @@ describe('Test moderation notifications', function () { | |||
60 | }) | 60 | }) |
61 | 61 | ||
62 | it('Should not send a notification to moderators on local abuse reported by an admin', async function () { | 62 | it('Should not send a notification to moderators on local abuse reported by an admin', async function () { |
63 | this.timeout(20000) | 63 | this.timeout(50000) |
64 | 64 | ||
65 | const name = 'video for abuse ' + buildUUID() | 65 | const name = 'video for abuse ' + buildUUID() |
66 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 66 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -72,7 +72,7 @@ describe('Test moderation notifications', function () { | |||
72 | }) | 72 | }) |
73 | 73 | ||
74 | it('Should send a notification to moderators on local video abuse', async function () { | 74 | it('Should send a notification to moderators on local video abuse', async function () { |
75 | this.timeout(20000) | 75 | this.timeout(50000) |
76 | 76 | ||
77 | const name = 'video for abuse ' + buildUUID() | 77 | const name = 'video for abuse ' + buildUUID() |
78 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 78 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -84,7 +84,7 @@ describe('Test moderation notifications', function () { | |||
84 | }) | 84 | }) |
85 | 85 | ||
86 | it('Should send a notification to moderators on remote video abuse', async function () { | 86 | it('Should send a notification to moderators on remote video abuse', async function () { |
87 | this.timeout(20000) | 87 | this.timeout(50000) |
88 | 88 | ||
89 | const name = 'video for abuse ' + buildUUID() | 89 | const name = 'video for abuse ' + buildUUID() |
90 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 90 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -99,7 +99,7 @@ describe('Test moderation notifications', function () { | |||
99 | }) | 99 | }) |
100 | 100 | ||
101 | it('Should send a notification to moderators on local comment abuse', async function () { | 101 | it('Should send a notification to moderators on local comment abuse', async function () { |
102 | this.timeout(20000) | 102 | this.timeout(50000) |
103 | 103 | ||
104 | const name = 'video for abuse ' + buildUUID() | 104 | const name = 'video for abuse ' + buildUUID() |
105 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 105 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -118,7 +118,7 @@ describe('Test moderation notifications', function () { | |||
118 | }) | 118 | }) |
119 | 119 | ||
120 | it('Should send a notification to moderators on remote comment abuse', async function () { | 120 | it('Should send a notification to moderators on remote comment abuse', async function () { |
121 | this.timeout(20000) | 121 | this.timeout(50000) |
122 | 122 | ||
123 | const name = 'video for abuse ' + buildUUID() | 123 | const name = 'video for abuse ' + buildUUID() |
124 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) | 124 | const video = await servers[0].videos.upload({ token: userToken1, attributes: { name } }) |
@@ -140,7 +140,7 @@ describe('Test moderation notifications', function () { | |||
140 | }) | 140 | }) |
141 | 141 | ||
142 | it('Should send a notification to moderators on local account abuse', async function () { | 142 | it('Should send a notification to moderators on local account abuse', async function () { |
143 | this.timeout(20000) | 143 | this.timeout(50000) |
144 | 144 | ||
145 | const username = 'user' + new Date().getTime() | 145 | const username = 'user' + new Date().getTime() |
146 | const { account } = await servers[0].users.create({ username, password: 'donald' }) | 146 | const { account } = await servers[0].users.create({ username, password: 'donald' }) |
@@ -153,7 +153,7 @@ describe('Test moderation notifications', function () { | |||
153 | }) | 153 | }) |
154 | 154 | ||
155 | it('Should send a notification to moderators on remote account abuse', async function () { | 155 | it('Should send a notification to moderators on remote account abuse', async function () { |
156 | this.timeout(20000) | 156 | this.timeout(50000) |
157 | 157 | ||
158 | const username = 'user' + new Date().getTime() | 158 | const username = 'user' + new Date().getTime() |
159 | const tmpToken = await servers[0].users.generateUserAndToken(username) | 159 | const tmpToken = await servers[0].users.generateUserAndToken(username) |
@@ -512,10 +512,14 @@ describe('Test moderation notifications', function () { | |||
512 | }) | 512 | }) |
513 | 513 | ||
514 | it('Should not send video publish notification if auto-blacklisted', async function () { | 514 | it('Should not send video publish notification if auto-blacklisted', async function () { |
515 | this.timeout(120000) | ||
516 | |||
515 | await checkVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' }) | 517 | await checkVideoIsPublished({ ...userBaseParams, videoName, shortUUID, checkType: 'absence' }) |
516 | }) | 518 | }) |
517 | 519 | ||
518 | it('Should not send a local user subscription notification if auto-blacklisted', async function () { | 520 | it('Should not send a local user subscription notification if auto-blacklisted', async function () { |
521 | this.timeout(120000) | ||
522 | |||
519 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'absence' }) | 523 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'absence' }) |
520 | }) | 524 | }) |
521 | 525 | ||
@@ -524,7 +528,7 @@ describe('Test moderation notifications', function () { | |||
524 | }) | 528 | }) |
525 | 529 | ||
526 | it('Should send video published and unblacklist after video unblacklisted', async function () { | 530 | it('Should send video published and unblacklist after video unblacklisted', async function () { |
527 | this.timeout(40000) | 531 | this.timeout(120000) |
528 | 532 | ||
529 | await servers[0].blacklist.remove({ videoId: uuid }) | 533 | await servers[0].blacklist.remove({ videoId: uuid }) |
530 | 534 | ||
@@ -537,10 +541,14 @@ describe('Test moderation notifications', function () { | |||
537 | }) | 541 | }) |
538 | 542 | ||
539 | it('Should send a local user subscription notification after removed from blacklist', async function () { | 543 | it('Should send a local user subscription notification after removed from blacklist', async function () { |
544 | this.timeout(120000) | ||
545 | |||
540 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'presence' }) | 546 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer1, videoName, shortUUID, checkType: 'presence' }) |
541 | }) | 547 | }) |
542 | 548 | ||
543 | it('Should send a remote user subscription notification after removed from blacklist', async function () { | 549 | it('Should send a remote user subscription notification after removed from blacklist', async function () { |
550 | this.timeout(120000) | ||
551 | |||
544 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'presence' }) | 552 | await checkNewVideoFromSubscription({ ...adminBaseParamsServer2, videoName, shortUUID, checkType: 'presence' }) |
545 | }) | 553 | }) |
546 | 554 | ||
@@ -576,7 +584,7 @@ describe('Test moderation notifications', function () { | |||
576 | }) | 584 | }) |
577 | 585 | ||
578 | it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () { | 586 | it('Should not send publish/subscription notifications after scheduled update if video still auto-blacklisted', async function () { |
579 | this.timeout(40000) | 587 | this.timeout(120000) |
580 | 588 | ||
581 | // In 2 seconds | 589 | // In 2 seconds |
582 | const updateAt = new Date(new Date().getTime() + 2000) | 590 | const updateAt = new Date(new Date().getTime() + 2000) |
diff --git a/server/tests/api/object-storage/video-static-file-privacy.ts b/server/tests/api/object-storage/video-static-file-privacy.ts index 71ad35a43..869d437d5 100644 --- a/server/tests/api/object-storage/video-static-file-privacy.ts +++ b/server/tests/api/object-storage/video-static-file-privacy.ts | |||
@@ -120,7 +120,7 @@ describe('Object storage for video static file privacy', function () { | |||
120 | // --------------------------------------------------------------------------- | 120 | // --------------------------------------------------------------------------- |
121 | 121 | ||
122 | it('Should upload a private video and have appropriate object storage ACL', async function () { | 122 | it('Should upload a private video and have appropriate object storage ACL', async function () { |
123 | this.timeout(60000) | 123 | this.timeout(120000) |
124 | 124 | ||
125 | { | 125 | { |
126 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) | 126 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.PRIVATE }) |
@@ -138,7 +138,7 @@ describe('Object storage for video static file privacy', function () { | |||
138 | }) | 138 | }) |
139 | 139 | ||
140 | it('Should upload a public video and have appropriate object storage ACL', async function () { | 140 | it('Should upload a public video and have appropriate object storage ACL', async function () { |
141 | this.timeout(60000) | 141 | this.timeout(120000) |
142 | 142 | ||
143 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.UNLISTED }) | 143 | const { uuid } = await server.videos.quickUpload({ name: 'video', privacy: VideoPrivacy.UNLISTED }) |
144 | await waitJobs([ server ]) | 144 | await waitJobs([ server ]) |
diff --git a/server/tests/api/users/index.ts b/server/tests/api/users/index.ts index 643f1a531..0313845ef 100644 --- a/server/tests/api/users/index.ts +++ b/server/tests/api/users/index.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import './oauth' | ||
1 | import './two-factor' | 2 | import './two-factor' |
2 | import './user-subscriptions' | 3 | import './user-subscriptions' |
3 | import './user-videos' | 4 | import './user-videos' |
diff --git a/server/tests/api/users/oauth.ts b/server/tests/api/users/oauth.ts new file mode 100644 index 000000000..6a3da5ea2 --- /dev/null +++ b/server/tests/api/users/oauth.ts | |||
@@ -0,0 +1,192 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
3 | import { expect } from 'chai' | ||
4 | import { wait } from '@shared/core-utils' | ||
5 | import { HttpStatusCode, OAuth2ErrorCode, PeerTubeProblemDocument } from '@shared/models' | ||
6 | import { cleanupTests, createSingleServer, killallServers, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' | ||
7 | |||
8 | describe('Test oauth', function () { | ||
9 | let server: PeerTubeServer | ||
10 | |||
11 | before(async function () { | ||
12 | this.timeout(30000) | ||
13 | |||
14 | server = await createSingleServer(1, { | ||
15 | rates_limit: { | ||
16 | login: { | ||
17 | max: 30 | ||
18 | } | ||
19 | } | ||
20 | }) | ||
21 | |||
22 | await setAccessTokensToServers([ server ]) | ||
23 | }) | ||
24 | |||
25 | describe('OAuth client', function () { | ||
26 | |||
27 | function expectInvalidClient (body: PeerTubeProblemDocument) { | ||
28 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
29 | expect(body.error).to.contain('client is invalid') | ||
30 | expect(body.type.startsWith('https://')).to.be.true | ||
31 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
32 | } | ||
33 | |||
34 | it('Should create a new client') | ||
35 | |||
36 | it('Should return the first client') | ||
37 | |||
38 | it('Should remove the last client') | ||
39 | |||
40 | it('Should not login with an invalid client id', async function () { | ||
41 | const client = { id: 'client', secret: server.store.client.secret } | ||
42 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
43 | |||
44 | expectInvalidClient(body) | ||
45 | }) | ||
46 | |||
47 | it('Should not login with an invalid client secret', async function () { | ||
48 | const client = { id: server.store.client.id, secret: 'coucou' } | ||
49 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
50 | |||
51 | expectInvalidClient(body) | ||
52 | }) | ||
53 | }) | ||
54 | |||
55 | describe('Login', function () { | ||
56 | |||
57 | function expectInvalidCredentials (body: PeerTubeProblemDocument) { | ||
58 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
59 | expect(body.error).to.contain('credentials are invalid') | ||
60 | expect(body.type.startsWith('https://')).to.be.true | ||
61 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
62 | } | ||
63 | |||
64 | it('Should not login with an invalid username', async function () { | ||
65 | const user = { username: 'captain crochet', password: server.store.user.password } | ||
66 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
67 | |||
68 | expectInvalidCredentials(body) | ||
69 | }) | ||
70 | |||
71 | it('Should not login with an invalid password', async function () { | ||
72 | const user = { username: server.store.user.username, password: 'mew_three' } | ||
73 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
74 | |||
75 | expectInvalidCredentials(body) | ||
76 | }) | ||
77 | |||
78 | it('Should be able to login', async function () { | ||
79 | await server.login.login({ expectedStatus: HttpStatusCode.OK_200 }) | ||
80 | }) | ||
81 | |||
82 | it('Should be able to login with an insensitive username', async function () { | ||
83 | const user = { username: 'RoOt', password: server.store.user.password } | ||
84 | await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) | ||
85 | |||
86 | const user2 = { username: 'rOoT', password: server.store.user.password } | ||
87 | await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 }) | ||
88 | |||
89 | const user3 = { username: 'ROOt', password: server.store.user.password } | ||
90 | await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 }) | ||
91 | }) | ||
92 | }) | ||
93 | |||
94 | describe('Logout', function () { | ||
95 | |||
96 | it('Should logout (revoke token)', async function () { | ||
97 | await server.login.logout({ token: server.accessToken }) | ||
98 | }) | ||
99 | |||
100 | it('Should not be able to get the user information', async function () { | ||
101 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
102 | }) | ||
103 | |||
104 | it('Should not be able to upload a video', async function () { | ||
105 | await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
106 | }) | ||
107 | |||
108 | it('Should be able to login again', async function () { | ||
109 | const body = await server.login.login() | ||
110 | server.accessToken = body.access_token | ||
111 | server.refreshToken = body.refresh_token | ||
112 | }) | ||
113 | |||
114 | it('Should be able to get my user information again', async function () { | ||
115 | await server.users.getMyInfo() | ||
116 | }) | ||
117 | |||
118 | it('Should have an expired access token', async function () { | ||
119 | this.timeout(60000) | ||
120 | |||
121 | await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
122 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
123 | |||
124 | await killallServers([ server ]) | ||
125 | await server.run() | ||
126 | |||
127 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
128 | }) | ||
129 | |||
130 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
131 | await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
132 | }) | ||
133 | |||
134 | it('Should refresh the token', async function () { | ||
135 | this.timeout(50000) | ||
136 | |||
137 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
138 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
139 | |||
140 | await killallServers([ server ]) | ||
141 | await server.run() | ||
142 | |||
143 | const res = await server.login.refreshToken({ refreshToken: server.refreshToken }) | ||
144 | server.accessToken = res.body.access_token | ||
145 | server.refreshToken = res.body.refresh_token | ||
146 | }) | ||
147 | |||
148 | it('Should be able to get my user information again', async function () { | ||
149 | await server.users.getMyInfo() | ||
150 | }) | ||
151 | }) | ||
152 | |||
153 | describe('Custom token lifetime', function () { | ||
154 | before(async function () { | ||
155 | this.timeout(120_000) | ||
156 | |||
157 | await server.kill() | ||
158 | await server.run({ | ||
159 | oauth2: { | ||
160 | token_lifetime: { | ||
161 | access_token: '2 seconds', | ||
162 | refresh_token: '2 seconds' | ||
163 | } | ||
164 | } | ||
165 | }) | ||
166 | }) | ||
167 | |||
168 | it('Should have a very short access token lifetime', async function () { | ||
169 | this.timeout(50000) | ||
170 | |||
171 | const { access_token: accessToken } = await server.login.login() | ||
172 | await server.users.getMyInfo({ token: accessToken }) | ||
173 | |||
174 | await wait(3000) | ||
175 | await server.users.getMyInfo({ token: accessToken, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
176 | }) | ||
177 | |||
178 | it('Should have a very short refresh token lifetime', async function () { | ||
179 | this.timeout(50000) | ||
180 | |||
181 | const { refresh_token: refreshToken } = await server.login.login() | ||
182 | await server.login.refreshToken({ refreshToken }) | ||
183 | |||
184 | await wait(3000) | ||
185 | await server.login.refreshToken({ refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
186 | }) | ||
187 | }) | ||
188 | |||
189 | after(async function () { | ||
190 | await cleanupTests([ server ]) | ||
191 | }) | ||
192 | }) | ||
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index 421b3ce16..93e2e489a 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -2,15 +2,8 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { testImage } from '@server/tests/shared' | 4 | import { testImage } from '@server/tests/shared' |
5 | import { AbuseState, HttpStatusCode, OAuth2ErrorCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@shared/models' | 5 | import { AbuseState, HttpStatusCode, UserAdminFlag, UserRole, VideoPlaylistType } from '@shared/models' |
6 | import { | 6 | import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServers } from '@shared/server-commands' |
7 | cleanupTests, | ||
8 | createSingleServer, | ||
9 | killallServers, | ||
10 | makePutBodyRequest, | ||
11 | PeerTubeServer, | ||
12 | setAccessTokensToServers | ||
13 | } from '@shared/server-commands' | ||
14 | 7 | ||
15 | describe('Test users', function () { | 8 | describe('Test users', function () { |
16 | let server: PeerTubeServer | 9 | let server: PeerTubeServer |
@@ -39,166 +32,6 @@ describe('Test users', function () { | |||
39 | await server.plugins.install({ npmName: 'peertube-theme-background-red' }) | 32 | await server.plugins.install({ npmName: 'peertube-theme-background-red' }) |
40 | }) | 33 | }) |
41 | 34 | ||
42 | describe('OAuth client', function () { | ||
43 | it('Should create a new client') | ||
44 | |||
45 | it('Should return the first client') | ||
46 | |||
47 | it('Should remove the last client') | ||
48 | |||
49 | it('Should not login with an invalid client id', async function () { | ||
50 | const client = { id: 'client', secret: server.store.client.secret } | ||
51 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
52 | |||
53 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
54 | expect(body.error).to.contain('client is invalid') | ||
55 | expect(body.type.startsWith('https://')).to.be.true | ||
56 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
57 | }) | ||
58 | |||
59 | it('Should not login with an invalid client secret', async function () { | ||
60 | const client = { id: server.store.client.id, secret: 'coucou' } | ||
61 | const body = await server.login.login({ client, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
62 | |||
63 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_CLIENT) | ||
64 | expect(body.error).to.contain('client is invalid') | ||
65 | expect(body.type.startsWith('https://')).to.be.true | ||
66 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_CLIENT) | ||
67 | }) | ||
68 | }) | ||
69 | |||
70 | describe('Login', function () { | ||
71 | |||
72 | it('Should not login with an invalid username', async function () { | ||
73 | const user = { username: 'captain crochet', password: server.store.user.password } | ||
74 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
75 | |||
76 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
77 | expect(body.error).to.contain('credentials are invalid') | ||
78 | expect(body.type.startsWith('https://')).to.be.true | ||
79 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
80 | }) | ||
81 | |||
82 | it('Should not login with an invalid password', async function () { | ||
83 | const user = { username: server.store.user.username, password: 'mew_three' } | ||
84 | const body = await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
85 | |||
86 | expect(body.code).to.equal(OAuth2ErrorCode.INVALID_GRANT) | ||
87 | expect(body.error).to.contain('credentials are invalid') | ||
88 | expect(body.type.startsWith('https://')).to.be.true | ||
89 | expect(body.type).to.contain(OAuth2ErrorCode.INVALID_GRANT) | ||
90 | }) | ||
91 | |||
92 | it('Should not be able to upload a video', async function () { | ||
93 | token = 'my_super_token' | ||
94 | |||
95 | await server.videos.upload({ token, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
96 | }) | ||
97 | |||
98 | it('Should not be able to follow', async function () { | ||
99 | token = 'my_super_token' | ||
100 | |||
101 | await server.follows.follow({ | ||
102 | hosts: [ 'http://example.com' ], | ||
103 | token, | ||
104 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
105 | }) | ||
106 | }) | ||
107 | |||
108 | it('Should not be able to unfollow') | ||
109 | |||
110 | it('Should be able to login', async function () { | ||
111 | const body = await server.login.login({ expectedStatus: HttpStatusCode.OK_200 }) | ||
112 | |||
113 | token = body.access_token | ||
114 | }) | ||
115 | |||
116 | it('Should be able to login with an insensitive username', async function () { | ||
117 | const user = { username: 'RoOt', password: server.store.user.password } | ||
118 | await server.login.login({ user, expectedStatus: HttpStatusCode.OK_200 }) | ||
119 | |||
120 | const user2 = { username: 'rOoT', password: server.store.user.password } | ||
121 | await server.login.login({ user: user2, expectedStatus: HttpStatusCode.OK_200 }) | ||
122 | |||
123 | const user3 = { username: 'ROOt', password: server.store.user.password } | ||
124 | await server.login.login({ user: user3, expectedStatus: HttpStatusCode.OK_200 }) | ||
125 | }) | ||
126 | }) | ||
127 | |||
128 | describe('Logout', function () { | ||
129 | it('Should logout (revoke token)', async function () { | ||
130 | await server.login.logout({ token: server.accessToken }) | ||
131 | }) | ||
132 | |||
133 | it('Should not be able to get the user information', async function () { | ||
134 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
135 | }) | ||
136 | |||
137 | it('Should not be able to upload a video', async function () { | ||
138 | await server.videos.upload({ attributes: { name: 'video' }, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
139 | }) | ||
140 | |||
141 | it('Should not be able to rate a video', async function () { | ||
142 | const path = '/api/v1/videos/' | ||
143 | const data = { | ||
144 | rating: 'likes' | ||
145 | } | ||
146 | |||
147 | const options = { | ||
148 | url: server.url, | ||
149 | path: path + videoId, | ||
150 | token: 'wrong token', | ||
151 | fields: data, | ||
152 | expectedStatus: HttpStatusCode.UNAUTHORIZED_401 | ||
153 | } | ||
154 | await makePutBodyRequest(options) | ||
155 | }) | ||
156 | |||
157 | it('Should be able to login again', async function () { | ||
158 | const body = await server.login.login() | ||
159 | server.accessToken = body.access_token | ||
160 | server.refreshToken = body.refresh_token | ||
161 | }) | ||
162 | |||
163 | it('Should be able to get my user information again', async function () { | ||
164 | await server.users.getMyInfo() | ||
165 | }) | ||
166 | |||
167 | it('Should have an expired access token', async function () { | ||
168 | this.timeout(60000) | ||
169 | |||
170 | await server.sql.setTokenField(server.accessToken, 'accessTokenExpiresAt', new Date().toISOString()) | ||
171 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', new Date().toISOString()) | ||
172 | |||
173 | await killallServers([ server ]) | ||
174 | await server.run() | ||
175 | |||
176 | await server.users.getMyInfo({ expectedStatus: HttpStatusCode.UNAUTHORIZED_401 }) | ||
177 | }) | ||
178 | |||
179 | it('Should not be able to refresh an access token with an expired refresh token', async function () { | ||
180 | await server.login.refreshToken({ refreshToken: server.refreshToken, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | ||
181 | }) | ||
182 | |||
183 | it('Should refresh the token', async function () { | ||
184 | this.timeout(50000) | ||
185 | |||
186 | const futureDate = new Date(new Date().getTime() + 1000 * 60).toISOString() | ||
187 | await server.sql.setTokenField(server.accessToken, 'refreshTokenExpiresAt', futureDate) | ||
188 | |||
189 | await killallServers([ server ]) | ||
190 | await server.run() | ||
191 | |||
192 | const res = await server.login.refreshToken({ refreshToken: server.refreshToken }) | ||
193 | server.accessToken = res.body.access_token | ||
194 | server.refreshToken = res.body.refresh_token | ||
195 | }) | ||
196 | |||
197 | it('Should be able to get my user information again', async function () { | ||
198 | await server.users.getMyInfo() | ||
199 | }) | ||
200 | }) | ||
201 | |||
202 | describe('Creating a user', function () { | 35 | describe('Creating a user', function () { |
203 | 36 | ||
204 | it('Should be able to create a new user', async function () { | 37 | it('Should be able to create a new user', async function () { |
@@ -512,6 +345,7 @@ describe('Test users', function () { | |||
512 | }) | 345 | }) |
513 | 346 | ||
514 | describe('Updating another user', function () { | 347 | describe('Updating another user', function () { |
348 | |||
515 | it('Should be able to update another user', async function () { | 349 | it('Should be able to update another user', async function () { |
516 | await server.users.update({ | 350 | await server.users.update({ |
517 | userId, | 351 | userId, |
@@ -562,13 +396,6 @@ describe('Test users', function () { | |||
562 | }) | 396 | }) |
563 | }) | 397 | }) |
564 | 398 | ||
565 | describe('Video blacklists', function () { | ||
566 | |||
567 | it('Should be able to list my video blacklist', async function () { | ||
568 | await server.blacklist.list({ token: userToken }) | ||
569 | }) | ||
570 | }) | ||
571 | |||
572 | describe('Remove a user', function () { | 399 | describe('Remove a user', function () { |
573 | 400 | ||
574 | before(async function () { | 401 | before(async function () { |
@@ -653,8 +480,9 @@ describe('Test users', function () { | |||
653 | }) | 480 | }) |
654 | 481 | ||
655 | describe('User blocking', function () { | 482 | describe('User blocking', function () { |
656 | let user16Id | 483 | let user16Id: number |
657 | let user16AccessToken | 484 | let user16AccessToken: string |
485 | |||
658 | const user16 = { | 486 | const user16 = { |
659 | username: 'user_16', | 487 | username: 'user_16', |
660 | password: 'my super password' | 488 | password: 'my super password' |
diff --git a/server/tests/api/videos/video-channel-syncs.ts b/server/tests/api/videos/video-channel-syncs.ts index 91291524d..dd483f95e 100644 --- a/server/tests/api/videos/video-channel-syncs.ts +++ b/server/tests/api/videos/video-channel-syncs.ts | |||
@@ -307,6 +307,7 @@ describe('Test channel synchronizations', function () { | |||
307 | }) | 307 | }) |
308 | } | 308 | } |
309 | 309 | ||
310 | runSuite('youtube-dl') | 310 | // FIXME: suite is broken with youtube-dl |
311 | // runSuite('youtube-dl') | ||
311 | runSuite('yt-dlp') | 312 | runSuite('yt-dlp') |
312 | }) | 313 | }) |
diff --git a/server/tests/api/videos/video-comments.ts b/server/tests/api/videos/video-comments.ts index dc47f8a4a..e077cbf73 100644 --- a/server/tests/api/videos/video-comments.ts +++ b/server/tests/api/videos/video-comments.ts | |||
@@ -38,6 +38,8 @@ describe('Test video comments', function () { | |||
38 | await setDefaultAccountAvatar(server) | 38 | await setDefaultAccountAvatar(server) |
39 | 39 | ||
40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') | 40 | userAccessTokenServer1 = await server.users.generateUserAndToken('user1') |
41 | await setDefaultChannelAvatar(server, 'user1_channel') | ||
42 | await setDefaultAccountAvatar(server, userAccessTokenServer1) | ||
41 | 43 | ||
42 | command = server.comments | 44 | command = server.comments |
43 | }) | 45 | }) |
@@ -232,16 +234,34 @@ describe('Test video comments', function () { | |||
232 | await command.addReply({ videoId, toCommentId: threadId2, text: text3 }) | 234 | await command.addReply({ videoId, toCommentId: threadId2, text: text3 }) |
233 | 235 | ||
234 | const tree = await command.getThread({ videoId: videoUUID, threadId: threadId2 }) | 236 | const tree = await command.getThread({ videoId: videoUUID, threadId: threadId2 }) |
235 | expect(tree.comment.totalReplies).to.equal(tree.comment.totalRepliesFromVideoAuthor + 1) | 237 | expect(tree.comment.totalRepliesFromVideoAuthor).to.equal(1) |
238 | expect(tree.comment.totalReplies).to.equal(2) | ||
236 | }) | 239 | }) |
237 | }) | 240 | }) |
238 | 241 | ||
239 | describe('All instance comments', function () { | 242 | describe('All instance comments', function () { |
240 | 243 | ||
241 | it('Should list instance comments as admin', async function () { | 244 | it('Should list instance comments as admin', async function () { |
242 | const { data } = await command.listForAdmin({ start: 0, count: 1 }) | 245 | { |
246 | const { data, total } = await command.listForAdmin({ start: 0, count: 1 }) | ||
243 | 247 | ||
244 | expect(data[0].text).to.equal('my second answer to thread 4') | 248 | expect(total).to.equal(7) |
249 | expect(data).to.have.lengthOf(1) | ||
250 | expect(data[0].text).to.equal('my second answer to thread 4') | ||
251 | expect(data[0].account.name).to.equal('root') | ||
252 | expect(data[0].account.displayName).to.equal('root') | ||
253 | expect(data[0].account.avatars).to.have.lengthOf(2) | ||
254 | } | ||
255 | |||
256 | { | ||
257 | const { data, total } = await command.listForAdmin({ start: 1, count: 2 }) | ||
258 | |||
259 | expect(total).to.equal(7) | ||
260 | expect(data).to.have.lengthOf(2) | ||
261 | |||
262 | expect(data[0].account.avatars).to.have.lengthOf(2) | ||
263 | expect(data[1].account.avatars).to.have.lengthOf(2) | ||
264 | } | ||
245 | }) | 265 | }) |
246 | 266 | ||
247 | it('Should filter instance comments by isLocal', async function () { | 267 | it('Should filter instance comments by isLocal', async function () { |
diff --git a/server/tests/external-plugins/auto-block-videos.ts b/server/tests/external-plugins/auto-block-videos.ts index d14587c38..cadd02e8d 100644 --- a/server/tests/external-plugins/auto-block-videos.ts +++ b/server/tests/external-plugins/auto-block-videos.ts | |||
@@ -30,7 +30,7 @@ describe('Official plugin auto-block videos', function () { | |||
30 | let port: number | 30 | let port: number |
31 | 31 | ||
32 | before(async function () { | 32 | before(async function () { |
33 | this.timeout(60000) | 33 | this.timeout(120000) |
34 | 34 | ||
35 | servers = await createMultipleServers(2) | 35 | servers = await createMultipleServers(2) |
36 | await setAccessTokensToServers(servers) | 36 | await setAccessTokensToServers(servers) |
diff --git a/server/tests/external-plugins/auto-mute.ts b/server/tests/external-plugins/auto-mute.ts index 440b58bfd..cfed76e88 100644 --- a/server/tests/external-plugins/auto-mute.ts +++ b/server/tests/external-plugins/auto-mute.ts | |||
@@ -21,7 +21,7 @@ describe('Official plugin auto-mute', function () { | |||
21 | let port: number | 21 | let port: number |
22 | 22 | ||
23 | before(async function () { | 23 | before(async function () { |
24 | this.timeout(30000) | 24 | this.timeout(120000) |
25 | 25 | ||
26 | servers = await createMultipleServers(2) | 26 | servers = await createMultipleServers(2) |
27 | await setAccessTokensToServers(servers) | 27 | await setAccessTokensToServers(servers) |
diff --git a/server/tests/feeds/feeds.ts b/server/tests/feeds/feeds.ts index 906dab1a3..7345f728a 100644 --- a/server/tests/feeds/feeds.ts +++ b/server/tests/feeds/feeds.ts | |||
@@ -189,7 +189,7 @@ describe('Test syndication feeds', () => { | |||
189 | const jsonObj = JSON.parse(json) | 189 | const jsonObj = JSON.parse(json) |
190 | expect(jsonObj.items.length).to.be.equal(1) | 190 | expect(jsonObj.items.length).to.be.equal(1) |
191 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | 191 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') |
192 | expect(jsonObj.items[0].author.name).to.equal('root') | 192 | expect(jsonObj.items[0].author.name).to.equal('Main root channel') |
193 | } | 193 | } |
194 | 194 | ||
195 | { | 195 | { |
@@ -197,7 +197,7 @@ describe('Test syndication feeds', () => { | |||
197 | const jsonObj = JSON.parse(json) | 197 | const jsonObj = JSON.parse(json) |
198 | expect(jsonObj.items.length).to.be.equal(1) | 198 | expect(jsonObj.items.length).to.be.equal(1) |
199 | expect(jsonObj.items[0].title).to.equal('user video') | 199 | expect(jsonObj.items[0].title).to.equal('user video') |
200 | expect(jsonObj.items[0].author.name).to.equal('john') | 200 | expect(jsonObj.items[0].author.name).to.equal('Main john channel') |
201 | } | 201 | } |
202 | 202 | ||
203 | for (const server of servers) { | 203 | for (const server of servers) { |
@@ -223,7 +223,7 @@ describe('Test syndication feeds', () => { | |||
223 | const jsonObj = JSON.parse(json) | 223 | const jsonObj = JSON.parse(json) |
224 | expect(jsonObj.items.length).to.be.equal(1) | 224 | expect(jsonObj.items.length).to.be.equal(1) |
225 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') | 225 | expect(jsonObj.items[0].title).to.equal('my super name for server 1') |
226 | expect(jsonObj.items[0].author.name).to.equal('root') | 226 | expect(jsonObj.items[0].author.name).to.equal('Main root channel') |
227 | } | 227 | } |
228 | 228 | ||
229 | { | 229 | { |
@@ -231,7 +231,7 @@ describe('Test syndication feeds', () => { | |||
231 | const jsonObj = JSON.parse(json) | 231 | const jsonObj = JSON.parse(json) |
232 | expect(jsonObj.items.length).to.be.equal(1) | 232 | expect(jsonObj.items.length).to.be.equal(1) |
233 | expect(jsonObj.items[0].title).to.equal('user video') | 233 | expect(jsonObj.items[0].title).to.equal('user video') |
234 | expect(jsonObj.items[0].author.name).to.equal('john') | 234 | expect(jsonObj.items[0].author.name).to.equal('Main john channel') |
235 | } | 235 | } |
236 | 236 | ||
237 | for (const server of servers) { | 237 | for (const server of servers) { |
diff --git a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js index c65b8d3a8..58bc27661 100644 --- a/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js +++ b/server/tests/fixtures/peertube-plugin-test-external-auth-one/main.js | |||
@@ -33,7 +33,17 @@ async function register ({ | |||
33 | username: 'kefka', | 33 | username: 'kefka', |
34 | email: 'kefka@example.com', | 34 | email: 'kefka@example.com', |
35 | role: 0, | 35 | role: 0, |
36 | displayName: 'Kefka Palazzo' | 36 | displayName: 'Kefka Palazzo', |
37 | adminFlags: 1, | ||
38 | videoQuota: 42000, | ||
39 | videoQuotaDaily: 42100, | ||
40 | |||
41 | // Always use new value except for videoQuotaDaily field | ||
42 | userUpdater: ({ fieldName, currentValue, newValue }) => { | ||
43 | if (fieldName === 'videoQuotaDaily') return currentValue | ||
44 | |||
45 | return newValue | ||
46 | } | ||
37 | }) | 47 | }) |
38 | }, | 48 | }, |
39 | hookTokenValidity: (options) => { | 49 | hookTokenValidity: (options) => { |
diff --git a/server/tests/fixtures/peertube-plugin-test-four/main.js b/server/tests/fixtures/peertube-plugin-test-four/main.js index 3e848c49e..b10177b45 100644 --- a/server/tests/fixtures/peertube-plugin-test-four/main.js +++ b/server/tests/fixtures/peertube-plugin-test-four/main.js | |||
@@ -76,6 +76,12 @@ async function register ({ | |||
76 | return res.json({ serverConfig }) | 76 | return res.json({ serverConfig }) |
77 | }) | 77 | }) |
78 | 78 | ||
79 | router.get('/server-listening-config', async (req, res) => { | ||
80 | const config = await peertubeHelpers.config.getServerListeningConfig() | ||
81 | |||
82 | return res.json({ config }) | ||
83 | }) | ||
84 | |||
79 | router.get('/static-route', async (req, res) => { | 85 | router.get('/static-route', async (req, res) => { |
80 | const staticRoute = peertubeHelpers.plugin.getBaseStaticRoute() | 86 | const staticRoute = peertubeHelpers.plugin.getBaseStaticRoute() |
81 | 87 | ||
diff --git a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js index ceab7b60d..fad5abf60 100644 --- a/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js +++ b/server/tests/fixtures/peertube-plugin-test-id-pass-auth-two/main.js | |||
@@ -33,7 +33,18 @@ async function register ({ | |||
33 | if (body.id === 'laguna' && body.password === 'laguna password') { | 33 | if (body.id === 'laguna' && body.password === 'laguna password') { |
34 | return Promise.resolve({ | 34 | return Promise.resolve({ |
35 | username: 'laguna', | 35 | username: 'laguna', |
36 | email: 'laguna@example.com' | 36 | email: 'laguna@example.com', |
37 | displayName: 'Laguna Loire', | ||
38 | adminFlags: 1, | ||
39 | videoQuota: 42000, | ||
40 | videoQuotaDaily: 42100, | ||
41 | |||
42 | // Always use new value except for videoQuotaDaily field | ||
43 | userUpdater: ({ fieldName, currentValue, newValue }) => { | ||
44 | if (fieldName === 'videoQuotaDaily') return currentValue | ||
45 | |||
46 | return newValue | ||
47 | } | ||
37 | }) | 48 | }) |
38 | } | 49 | } |
39 | 50 | ||
diff --git a/server/tests/fixtures/peertube-plugin-test/main.js b/server/tests/fixtures/peertube-plugin-test/main.js index 19dccf26e..19ba9f278 100644 --- a/server/tests/fixtures/peertube-plugin-test/main.js +++ b/server/tests/fixtures/peertube-plugin-test/main.js | |||
@@ -250,7 +250,10 @@ async function register ({ registerHook, registerSetting, settingsManager, stora | |||
250 | 250 | ||
251 | registerHook({ | 251 | registerHook({ |
252 | target: 'filter:api.download.video.allowed.result', | 252 | target: 'filter:api.download.video.allowed.result', |
253 | handler: (result, params) => { | 253 | handler: async (result, params) => { |
254 | const loggedInUser = await peertubeHelpers.user.getAuthUser(params.res) | ||
255 | if (loggedInUser) return { allowed: true } | ||
256 | |||
254 | if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) { | 257 | if (params && !params.streamingPlaylist && params.video.name.includes('bad file')) { |
255 | return { allowed: false, errorMessage: 'Cao Cao' } | 258 | return { allowed: false, errorMessage: 'Cao Cao' } |
256 | } | 259 | } |
diff --git a/server/tests/plugins/external-auth.ts b/server/tests/plugins/external-auth.ts index 437777e90..e600f958f 100644 --- a/server/tests/plugins/external-auth.ts +++ b/server/tests/plugins/external-auth.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import { expect } from 'chai' | 3 | import { expect } from 'chai' |
4 | import { wait } from '@shared/core-utils' | 4 | import { wait } from '@shared/core-utils' |
5 | import { HttpStatusCode, UserRole } from '@shared/models' | 5 | import { HttpStatusCode, UserAdminFlag, UserRole } from '@shared/models' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
8 | createSingleServer, | 8 | createSingleServer, |
@@ -51,6 +51,7 @@ describe('Test external auth plugins', function () { | |||
51 | 51 | ||
52 | let kefkaAccessToken: string | 52 | let kefkaAccessToken: string |
53 | let kefkaRefreshToken: string | 53 | let kefkaRefreshToken: string |
54 | let kefkaId: number | ||
54 | 55 | ||
55 | let externalAuthToken: string | 56 | let externalAuthToken: string |
56 | 57 | ||
@@ -156,6 +157,9 @@ describe('Test external auth plugins', function () { | |||
156 | expect(body.account.displayName).to.equal('cyan') | 157 | expect(body.account.displayName).to.equal('cyan') |
157 | expect(body.email).to.equal('cyan@example.com') | 158 | expect(body.email).to.equal('cyan@example.com') |
158 | expect(body.role.id).to.equal(UserRole.USER) | 159 | expect(body.role.id).to.equal(UserRole.USER) |
160 | expect(body.adminFlags).to.equal(UserAdminFlag.NONE) | ||
161 | expect(body.videoQuota).to.equal(5242880) | ||
162 | expect(body.videoQuotaDaily).to.equal(-1) | ||
159 | } | 163 | } |
160 | }) | 164 | }) |
161 | 165 | ||
@@ -178,6 +182,11 @@ describe('Test external auth plugins', function () { | |||
178 | expect(body.account.displayName).to.equal('Kefka Palazzo') | 182 | expect(body.account.displayName).to.equal('Kefka Palazzo') |
179 | expect(body.email).to.equal('kefka@example.com') | 183 | expect(body.email).to.equal('kefka@example.com') |
180 | expect(body.role.id).to.equal(UserRole.ADMINISTRATOR) | 184 | expect(body.role.id).to.equal(UserRole.ADMINISTRATOR) |
185 | expect(body.adminFlags).to.equal(UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST) | ||
186 | expect(body.videoQuota).to.equal(42000) | ||
187 | expect(body.videoQuotaDaily).to.equal(42100) | ||
188 | |||
189 | kefkaId = body.id | ||
181 | } | 190 | } |
182 | }) | 191 | }) |
183 | 192 | ||
@@ -240,6 +249,37 @@ describe('Test external auth plugins', function () { | |||
240 | expect(body.role.id).to.equal(UserRole.USER) | 249 | expect(body.role.id).to.equal(UserRole.USER) |
241 | }) | 250 | }) |
242 | 251 | ||
252 | it('Should login Kefka and update the profile', async function () { | ||
253 | { | ||
254 | await server.users.update({ userId: kefkaId, videoQuota: 43000, videoQuotaDaily: 43100 }) | ||
255 | await server.users.updateMe({ token: kefkaAccessToken, displayName: 'kefka updated' }) | ||
256 | |||
257 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | ||
258 | expect(body.username).to.equal('kefka') | ||
259 | expect(body.account.displayName).to.equal('kefka updated') | ||
260 | expect(body.videoQuota).to.equal(43000) | ||
261 | expect(body.videoQuotaDaily).to.equal(43100) | ||
262 | } | ||
263 | |||
264 | { | ||
265 | const res = await loginExternal({ | ||
266 | server, | ||
267 | npmName: 'test-external-auth-one', | ||
268 | authName: 'external-auth-2', | ||
269 | username: 'kefka' | ||
270 | }) | ||
271 | |||
272 | kefkaAccessToken = res.access_token | ||
273 | kefkaRefreshToken = res.refresh_token | ||
274 | |||
275 | const body = await server.users.getMyInfo({ token: kefkaAccessToken }) | ||
276 | expect(body.username).to.equal('kefka') | ||
277 | expect(body.account.displayName).to.equal('Kefka Palazzo') | ||
278 | expect(body.videoQuota).to.equal(42000) | ||
279 | expect(body.videoQuotaDaily).to.equal(43100) | ||
280 | } | ||
281 | }) | ||
282 | |||
243 | it('Should not update an external auth email', async function () { | 283 | it('Should not update an external auth email', async function () { |
244 | await server.users.updateMe({ | 284 | await server.users.updateMe({ |
245 | token: cyanAccessToken, | 285 | token: cyanAccessToken, |
diff --git a/server/tests/plugins/filter-hooks.ts b/server/tests/plugins/filter-hooks.ts index 083fd43ca..6724b3bf8 100644 --- a/server/tests/plugins/filter-hooks.ts +++ b/server/tests/plugins/filter-hooks.ts | |||
@@ -430,6 +430,7 @@ describe('Test plugin filter hooks', function () { | |||
430 | 430 | ||
431 | describe('Download hooks', function () { | 431 | describe('Download hooks', function () { |
432 | const downloadVideos: VideoDetails[] = [] | 432 | const downloadVideos: VideoDetails[] = [] |
433 | let downloadVideo2Token: string | ||
433 | 434 | ||
434 | before(async function () { | 435 | before(async function () { |
435 | this.timeout(120000) | 436 | this.timeout(120000) |
@@ -459,6 +460,8 @@ describe('Test plugin filter hooks', function () { | |||
459 | for (const uuid of uuids) { | 460 | for (const uuid of uuids) { |
460 | downloadVideos.push(await servers[0].videos.get({ id: uuid })) | 461 | downloadVideos.push(await servers[0].videos.get({ id: uuid })) |
461 | } | 462 | } |
463 | |||
464 | downloadVideo2Token = await servers[0].videoToken.getVideoFileToken({ videoId: downloadVideos[2].uuid }) | ||
462 | }) | 465 | }) |
463 | 466 | ||
464 | it('Should run filter:api.download.torrent.allowed.result', async function () { | 467 | it('Should run filter:api.download.torrent.allowed.result', async function () { |
@@ -471,32 +474,42 @@ describe('Test plugin filter hooks', function () { | |||
471 | 474 | ||
472 | it('Should run filter:api.download.video.allowed.result', async function () { | 475 | it('Should run filter:api.download.video.allowed.result', async function () { |
473 | { | 476 | { |
474 | const res = await makeRawRequest({ url: downloadVideos[1].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | 477 | const refused = downloadVideos[1].files[0].fileDownloadUrl |
478 | const allowed = [ | ||
479 | downloadVideos[0].files[0].fileDownloadUrl, | ||
480 | downloadVideos[2].files[0].fileDownloadUrl | ||
481 | ] | ||
482 | |||
483 | const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
475 | expect(res.body.error).to.equal('Cao Cao') | 484 | expect(res.body.error).to.equal('Cao Cao') |
476 | 485 | ||
477 | await makeRawRequest({ url: downloadVideos[0].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) | 486 | for (const url of allowed) { |
478 | await makeRawRequest({ url: downloadVideos[2].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) | 487 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) |
488 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) | ||
489 | } | ||
479 | } | 490 | } |
480 | 491 | ||
481 | { | 492 | { |
482 | const res = await makeRawRequest({ | 493 | const refused = downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl |
483 | url: downloadVideos[2].streamingPlaylists[0].files[0].fileDownloadUrl, | ||
484 | expectedStatus: HttpStatusCode.FORBIDDEN_403 | ||
485 | }) | ||
486 | 494 | ||
487 | expect(res.body.error).to.equal('Sun Jian') | 495 | const allowed = [ |
496 | downloadVideos[2].files[0].fileDownloadUrl, | ||
497 | downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, | ||
498 | downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl | ||
499 | ] | ||
488 | 500 | ||
489 | await makeRawRequest({ url: downloadVideos[2].files[0].fileDownloadUrl, expectedStatus: HttpStatusCode.OK_200 }) | 501 | // Only streaming playlist is refuse |
502 | const res = await makeRawRequest({ url: refused, expectedStatus: HttpStatusCode.FORBIDDEN_403 }) | ||
503 | expect(res.body.error).to.equal('Sun Jian') | ||
490 | 504 | ||
491 | await makeRawRequest({ | 505 | // But not we there is a user in res |
492 | url: downloadVideos[0].streamingPlaylists[0].files[0].fileDownloadUrl, | 506 | await makeRawRequest({ url: refused, token: servers[0].accessToken, expectedStatus: HttpStatusCode.OK_200 }) |
493 | expectedStatus: HttpStatusCode.OK_200 | 507 | await makeRawRequest({ url: refused, query: { videoFileToken: downloadVideo2Token }, expectedStatus: HttpStatusCode.OK_200 }) |
494 | }) | ||
495 | 508 | ||
496 | await makeRawRequest({ | 509 | // Other files work |
497 | url: downloadVideos[1].streamingPlaylists[0].files[0].fileDownloadUrl, | 510 | for (const url of allowed) { |
498 | expectedStatus: HttpStatusCode.OK_200 | 511 | await makeRawRequest({ url, expectedStatus: HttpStatusCode.OK_200 }) |
499 | }) | 512 | } |
500 | } | 513 | } |
501 | }) | 514 | }) |
502 | }) | 515 | }) |
diff --git a/server/tests/plugins/id-and-pass-auth.ts b/server/tests/plugins/id-and-pass-auth.ts index fc24a5656..10155c28b 100644 --- a/server/tests/plugins/id-and-pass-auth.ts +++ b/server/tests/plugins/id-and-pass-auth.ts | |||
@@ -13,6 +13,7 @@ describe('Test id and pass auth plugins', function () { | |||
13 | 13 | ||
14 | let lagunaAccessToken: string | 14 | let lagunaAccessToken: string |
15 | let lagunaRefreshToken: string | 15 | let lagunaRefreshToken: string |
16 | let lagunaId: number | ||
16 | 17 | ||
17 | before(async function () { | 18 | before(async function () { |
18 | this.timeout(30000) | 19 | this.timeout(30000) |
@@ -78,8 +79,10 @@ describe('Test id and pass auth plugins', function () { | |||
78 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | 79 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) |
79 | 80 | ||
80 | expect(body.username).to.equal('laguna') | 81 | expect(body.username).to.equal('laguna') |
81 | expect(body.account.displayName).to.equal('laguna') | 82 | expect(body.account.displayName).to.equal('Laguna Loire') |
82 | expect(body.role.id).to.equal(UserRole.USER) | 83 | expect(body.role.id).to.equal(UserRole.USER) |
84 | |||
85 | lagunaId = body.id | ||
83 | } | 86 | } |
84 | }) | 87 | }) |
85 | 88 | ||
@@ -132,6 +135,33 @@ describe('Test id and pass auth plugins', function () { | |||
132 | expect(body.role.id).to.equal(UserRole.MODERATOR) | 135 | expect(body.role.id).to.equal(UserRole.MODERATOR) |
133 | }) | 136 | }) |
134 | 137 | ||
138 | it('Should login Laguna and update the profile', async function () { | ||
139 | { | ||
140 | await server.users.update({ userId: lagunaId, videoQuota: 43000, videoQuotaDaily: 43100 }) | ||
141 | await server.users.updateMe({ token: lagunaAccessToken, displayName: 'laguna updated' }) | ||
142 | |||
143 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | ||
144 | expect(body.username).to.equal('laguna') | ||
145 | expect(body.account.displayName).to.equal('laguna updated') | ||
146 | expect(body.videoQuota).to.equal(43000) | ||
147 | expect(body.videoQuotaDaily).to.equal(43100) | ||
148 | } | ||
149 | |||
150 | { | ||
151 | const body = await server.login.login({ user: { username: 'laguna', password: 'laguna password' } }) | ||
152 | lagunaAccessToken = body.access_token | ||
153 | lagunaRefreshToken = body.refresh_token | ||
154 | } | ||
155 | |||
156 | { | ||
157 | const body = await server.users.getMyInfo({ token: lagunaAccessToken }) | ||
158 | expect(body.username).to.equal('laguna') | ||
159 | expect(body.account.displayName).to.equal('Laguna Loire') | ||
160 | expect(body.videoQuota).to.equal(42000) | ||
161 | expect(body.videoQuotaDaily).to.equal(43100) | ||
162 | } | ||
163 | }) | ||
164 | |||
135 | it('Should reject token of laguna by the plugin hook', async function () { | 165 | it('Should reject token of laguna by the plugin hook', async function () { |
136 | this.timeout(10000) | 166 | this.timeout(10000) |
137 | 167 | ||
@@ -147,7 +177,7 @@ describe('Test id and pass auth plugins', function () { | |||
147 | await server.servers.waitUntilLog('valid username') | 177 | await server.servers.waitUntilLog('valid username') |
148 | 178 | ||
149 | await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 179 | await command.login({ user: { username: 'kiros', password: 'kiros password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
150 | await server.servers.waitUntilLog('valid display name') | 180 | await server.servers.waitUntilLog('valid displayName') |
151 | 181 | ||
152 | await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) | 182 | await command.login({ user: { username: 'raine', password: 'raine password' }, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) |
153 | await server.servers.waitUntilLog('valid role') | 183 | await server.servers.waitUntilLog('valid role') |
diff --git a/server/tests/plugins/plugin-helpers.ts b/server/tests/plugins/plugin-helpers.ts index 038e3f0d6..e25992723 100644 --- a/server/tests/plugins/plugin-helpers.ts +++ b/server/tests/plugins/plugin-helpers.ts | |||
@@ -64,6 +64,18 @@ describe('Test plugin helpers', function () { | |||
64 | await servers[0].servers.waitUntilLog(`server url is ${servers[0].url}`) | 64 | await servers[0].servers.waitUntilLog(`server url is ${servers[0].url}`) |
65 | }) | 65 | }) |
66 | 66 | ||
67 | it('Should have the correct listening config', async function () { | ||
68 | const res = await makeGetRequest({ | ||
69 | url: servers[0].url, | ||
70 | path: '/plugins/test-four/router/server-listening-config', | ||
71 | expectedStatus: HttpStatusCode.OK_200 | ||
72 | }) | ||
73 | |||
74 | expect(res.body.config).to.exist | ||
75 | expect(res.body.config.hostname).to.equal('::') | ||
76 | expect(res.body.config.port).to.equal(servers[0].port) | ||
77 | }) | ||
78 | |||
67 | it('Should have the correct config', async function () { | 79 | it('Should have the correct config', async function () { |
68 | const res = await makeGetRequest({ | 80 | const res = await makeGetRequest({ |
69 | url: servers[0].url, | 81 | url: servers[0].url, |
diff --git a/server/types/express.d.ts b/server/types/express.d.ts index 3738ffc47..6fea4dac2 100644 --- a/server/types/express.d.ts +++ b/server/types/express.d.ts | |||
@@ -1,4 +1,3 @@ | |||
1 | |||
2 | import { OutgoingHttpHeaders } from 'http' | 1 | import { OutgoingHttpHeaders } from 'http' |
3 | import { RegisterServerAuthExternalOptions } from '@server/types' | 2 | import { RegisterServerAuthExternalOptions } from '@server/types' |
4 | import { | 3 | import { |
@@ -10,6 +9,7 @@ import { | |||
10 | MChannelBannerAccountDefault, | 9 | MChannelBannerAccountDefault, |
11 | MChannelSyncChannel, | 10 | MChannelSyncChannel, |
12 | MStreamingPlaylist, | 11 | MStreamingPlaylist, |
12 | MUserAccountUrl, | ||
13 | MVideoChangeOwnershipFull, | 13 | MVideoChangeOwnershipFull, |
14 | MVideoFile, | 14 | MVideoFile, |
15 | MVideoFormattableDetails, | 15 | MVideoFormattableDetails, |
@@ -187,6 +187,10 @@ declare module 'express' { | |||
187 | actor: MActorAccountChannelId | 187 | actor: MActorAccountChannelId |
188 | } | 188 | } |
189 | 189 | ||
190 | videoFileToken?: { | ||
191 | user: MUserAccountUrl | ||
192 | } | ||
193 | |||
190 | authenticated?: boolean | 194 | authenticated?: boolean |
191 | 195 | ||
192 | registeredPlugin?: RegisteredPlugin | 196 | registeredPlugin?: RegisteredPlugin |
diff --git a/server/types/lib.d.ts b/server/types/lib.d.ts new file mode 100644 index 000000000..c901e2032 --- /dev/null +++ b/server/types/lib.d.ts | |||
@@ -0,0 +1,12 @@ | |||
1 | type ObjectKeys<T> = | ||
2 | T extends object | ||
3 | ? `${Exclude<keyof T, symbol>}`[] | ||
4 | : T extends number | ||
5 | ? [] | ||
6 | : T extends any | string | ||
7 | ? string[] | ||
8 | : never | ||
9 | |||
10 | interface ObjectConstructor { | ||
11 | keys<T> (o: T): ObjectKeys<T> | ||
12 | } | ||
diff --git a/server/types/plugins/register-server-auth.model.ts b/server/types/plugins/register-server-auth.model.ts index 79c18c406..e10968c20 100644 --- a/server/types/plugins/register-server-auth.model.ts +++ b/server/types/plugins/register-server-auth.model.ts | |||
@@ -1,14 +1,33 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { UserRole } from '@shared/models' | 2 | import { UserAdminFlag, UserRole } from '@shared/models' |
3 | import { MOAuthToken, MUser } from '../models' | 3 | import { MOAuthToken, MUser } from '../models' |
4 | 4 | ||
5 | export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions | 5 | export type RegisterServerAuthOptions = RegisterServerAuthPassOptions | RegisterServerAuthExternalOptions |
6 | 6 | ||
7 | export type AuthenticatedResultUpdaterFieldName = 'displayName' | 'role' | 'adminFlags' | 'videoQuota' | 'videoQuotaDaily' | ||
8 | |||
7 | export interface RegisterServerAuthenticatedResult { | 9 | export interface RegisterServerAuthenticatedResult { |
10 | // Update the user profile if it already exists | ||
11 | // Default behaviour is no update | ||
12 | // Introduced in PeerTube >= 5.1 | ||
13 | userUpdater?: <T> (options: { | ||
14 | fieldName: AuthenticatedResultUpdaterFieldName | ||
15 | currentValue: T | ||
16 | newValue: T | ||
17 | }) => T | ||
18 | |||
8 | username: string | 19 | username: string |
9 | email: string | 20 | email: string |
10 | role?: UserRole | 21 | role?: UserRole |
11 | displayName?: string | 22 | displayName?: string |
23 | |||
24 | // PeerTube >= 5.1 | ||
25 | adminFlags?: UserAdminFlag | ||
26 | |||
27 | // PeerTube >= 5.1 | ||
28 | videoQuota?: number | ||
29 | // PeerTube >= 5.1 | ||
30 | videoQuotaDaily?: number | ||
12 | } | 31 | } |
13 | 32 | ||
14 | export interface RegisterServerExternalAuthenticatedResult extends RegisterServerAuthenticatedResult { | 33 | export interface RegisterServerExternalAuthenticatedResult extends RegisterServerAuthenticatedResult { |
diff --git a/server/types/plugins/register-server-option.model.ts b/server/types/plugins/register-server-option.model.ts index 1e2bd830e..df419fff4 100644 --- a/server/types/plugins/register-server-option.model.ts +++ b/server/types/plugins/register-server-option.model.ts | |||
@@ -71,6 +71,9 @@ export type PeerTubeHelpers = { | |||
71 | config: { | 71 | config: { |
72 | getWebserverUrl: () => string | 72 | getWebserverUrl: () => string |
73 | 73 | ||
74 | // PeerTube >= 5.1 | ||
75 | getServerListeningConfig: () => { hostname: string, port: number } | ||
76 | |||
74 | getServerConfig: () => Promise<ServerConfig> | 77 | getServerConfig: () => Promise<ServerConfig> |
75 | } | 78 | } |
76 | 79 | ||
diff --git a/shared/core-utils/plugins/hooks.ts b/shared/core-utils/plugins/hooks.ts index 3784969b5..96bcc945e 100644 --- a/shared/core-utils/plugins/hooks.ts +++ b/shared/core-utils/plugins/hooks.ts | |||
@@ -1,3 +1,4 @@ | |||
1 | import { RegisteredExternalAuthConfig } from '@shared/models' | ||
1 | import { HookType } from '../../models/plugins/hook-type.enum' | 2 | import { HookType } from '../../models/plugins/hook-type.enum' |
2 | import { isCatchable, isPromise } from '../common/promises' | 3 | import { isCatchable, isPromise } from '../common/promises' |
3 | 4 | ||
@@ -49,7 +50,12 @@ async function internalRunHook <T> (options: { | |||
49 | return result | 50 | return result |
50 | } | 51 | } |
51 | 52 | ||
53 | function getExternalAuthHref (apiUrl: string, auth: RegisteredExternalAuthConfig) { | ||
54 | return apiUrl + `/plugins/${auth.name}/${auth.version}/auth/${auth.authName}` | ||
55 | } | ||
56 | |||
52 | export { | 57 | export { |
53 | getHookType, | 58 | getHookType, |
54 | internalRunHook | 59 | internalRunHook, |
60 | getExternalAuthHref | ||
55 | } | 61 | } |
diff --git a/shared/server-commands/miscs/sql-command.ts b/shared/server-commands/miscs/sql-command.ts index 823fc9e38..35cc2253f 100644 --- a/shared/server-commands/miscs/sql-command.ts +++ b/shared/server-commands/miscs/sql-command.ts | |||
@@ -13,101 +13,87 @@ export class SQLCommand extends AbstractCommand { | |||
13 | return seq.query(`DELETE FROM "${table}"`, options) | 13 | return seq.query(`DELETE FROM "${table}"`, options) |
14 | } | 14 | } |
15 | 15 | ||
16 | async getCount (table: string) { | 16 | async getVideoShareCount () { |
17 | const seq = this.getSequelize() | 17 | const [ { total } ] = await this.selectQuery<{ total: string }>(`SELECT COUNT(*) as total FROM "videoShare"`) |
18 | |||
19 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } | ||
20 | |||
21 | const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options) | ||
22 | if (total === null) return 0 | 18 | if (total === null) return 0 |
23 | 19 | ||
24 | return parseInt(total, 10) | 20 | return parseInt(total, 10) |
25 | } | 21 | } |
26 | 22 | ||
27 | async getInternalFileUrl (fileId: number) { | 23 | async getInternalFileUrl (fileId: number) { |
28 | return this.selectQuery(`SELECT "fileUrl" FROM "videoFile" WHERE id = ${fileId}`) | 24 | return this.selectQuery<{ fileUrl: string }>(`SELECT "fileUrl" FROM "videoFile" WHERE id = :fileId`, { fileId }) |
29 | .then(rows => rows[0].fileUrl as string) | 25 | .then(rows => rows[0].fileUrl) |
30 | } | 26 | } |
31 | 27 | ||
32 | setActorField (to: string, field: string, value: string) { | 28 | setActorField (to: string, field: string, value: string) { |
33 | const seq = this.getSequelize() | 29 | return this.updateQuery(`UPDATE actor SET ${this.escapeColumnName(field)} = :value WHERE url = :to`, { value, to }) |
34 | |||
35 | const options = { type: QueryTypes.UPDATE } | ||
36 | |||
37 | return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options) | ||
38 | } | 30 | } |
39 | 31 | ||
40 | setVideoField (uuid: string, field: string, value: string) { | 32 | setVideoField (uuid: string, field: string, value: string) { |
41 | const seq = this.getSequelize() | 33 | return this.updateQuery(`UPDATE video SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid }) |
42 | |||
43 | const options = { type: QueryTypes.UPDATE } | ||
44 | |||
45 | return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) | ||
46 | } | 34 | } |
47 | 35 | ||
48 | setPlaylistField (uuid: string, field: string, value: string) { | 36 | setPlaylistField (uuid: string, field: string, value: string) { |
49 | const seq = this.getSequelize() | 37 | return this.updateQuery(`UPDATE "videoPlaylist" SET ${this.escapeColumnName(field)} = :value WHERE uuid = :uuid`, { value, uuid }) |
50 | |||
51 | const options = { type: QueryTypes.UPDATE } | ||
52 | |||
53 | return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) | ||
54 | } | 38 | } |
55 | 39 | ||
56 | async countVideoViewsOf (uuid: string) { | 40 | async countVideoViewsOf (uuid: string) { |
57 | const seq = this.getSequelize() | ||
58 | |||
59 | const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' + | 41 | const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' + |
60 | `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'` | 42 | `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = :uuid` |
61 | |||
62 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } | ||
63 | const [ { total } ] = await seq.query<{ total: number }>(query, options) | ||
64 | 43 | ||
44 | const [ { total } ] = await this.selectQuery<{ total: number }>(query, { uuid }) | ||
65 | if (!total) return 0 | 45 | if (!total) return 0 |
66 | 46 | ||
67 | return forceNumber(total) | 47 | return forceNumber(total) |
68 | } | 48 | } |
69 | 49 | ||
70 | getActorImage (filename: string) { | 50 | getActorImage (filename: string) { |
71 | return this.selectQuery(`SELECT * FROM "actorImage" WHERE filename = '${filename}'`) | 51 | return this.selectQuery<{ width: number, height: number }>(`SELECT * FROM "actorImage" WHERE filename = :filename`, { filename }) |
72 | .then(rows => rows[0]) | 52 | .then(rows => rows[0]) |
73 | } | 53 | } |
74 | 54 | ||
75 | selectQuery (query: string) { | 55 | // --------------------------------------------------------------------------- |
76 | const seq = this.getSequelize() | ||
77 | const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } | ||
78 | 56 | ||
79 | return seq.query<any>(query, options) | 57 | setPluginVersion (pluginName: string, newVersion: string) { |
58 | return this.setPluginField(pluginName, 'version', newVersion) | ||
80 | } | 59 | } |
81 | 60 | ||
82 | updateQuery (query: string) { | 61 | setPluginLatestVersion (pluginName: string, newVersion: string) { |
83 | const seq = this.getSequelize() | 62 | return this.setPluginField(pluginName, 'latestVersion', newVersion) |
84 | const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE } | 63 | } |
85 | 64 | ||
86 | return seq.query(query, options) | 65 | setPluginField (pluginName: string, field: string, value: string) { |
66 | return this.updateQuery( | ||
67 | `UPDATE "plugin" SET ${this.escapeColumnName(field)} = :value WHERE "name" = :pluginName`, | ||
68 | { pluginName, value } | ||
69 | ) | ||
87 | } | 70 | } |
88 | 71 | ||
89 | // --------------------------------------------------------------------------- | 72 | // --------------------------------------------------------------------------- |
90 | 73 | ||
91 | setPluginField (pluginName: string, field: string, value: string) { | 74 | selectQuery <T extends object> (query: string, replacements: { [id: string]: string | number } = {}) { |
92 | const seq = this.getSequelize() | 75 | const seq = this.getSequelize() |
76 | const options = { | ||
77 | type: QueryTypes.SELECT as QueryTypes.SELECT, | ||
78 | replacements | ||
79 | } | ||
93 | 80 | ||
94 | const options = { type: QueryTypes.UPDATE } | 81 | return seq.query<T>(query, options) |
95 | |||
96 | return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options) | ||
97 | } | 82 | } |
98 | 83 | ||
99 | setPluginVersion (pluginName: string, newVersion: string) { | 84 | updateQuery (query: string, replacements: { [id: string]: string | number } = {}) { |
100 | return this.setPluginField(pluginName, 'version', newVersion) | 85 | const seq = this.getSequelize() |
101 | } | 86 | const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE, replacements } |
102 | 87 | ||
103 | setPluginLatestVersion (pluginName: string, newVersion: string) { | 88 | return seq.query(query, options) |
104 | return this.setPluginField(pluginName, 'latestVersion', newVersion) | ||
105 | } | 89 | } |
106 | 90 | ||
107 | // --------------------------------------------------------------------------- | 91 | // --------------------------------------------------------------------------- |
108 | 92 | ||
109 | async getPlaylistInfohash (playlistId: number) { | 93 | async getPlaylistInfohash (playlistId: number) { |
110 | const result = await this.selectQuery('SELECT "p2pMediaLoaderInfohashes" FROM "videoStreamingPlaylist" WHERE id = ' + playlistId) | 94 | const query = 'SELECT "p2pMediaLoaderInfohashes" FROM "videoStreamingPlaylist" WHERE id = :playlistId' |
95 | |||
96 | const result = await this.selectQuery<{ p2pMediaLoaderInfohashes: string }>(query, { playlistId }) | ||
111 | if (!result || result.length === 0) return [] | 97 | if (!result || result.length === 0) return [] |
112 | 98 | ||
113 | return result[0].p2pMediaLoaderInfohashes | 99 | return result[0].p2pMediaLoaderInfohashes |
@@ -116,19 +102,14 @@ export class SQLCommand extends AbstractCommand { | |||
116 | // --------------------------------------------------------------------------- | 102 | // --------------------------------------------------------------------------- |
117 | 103 | ||
118 | setActorFollowScores (newScore: number) { | 104 | setActorFollowScores (newScore: number) { |
119 | const seq = this.getSequelize() | 105 | return this.updateQuery(`UPDATE "actorFollow" SET "score" = :newScore`, { newScore }) |
120 | |||
121 | const options = { type: QueryTypes.UPDATE } | ||
122 | |||
123 | return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options) | ||
124 | } | 106 | } |
125 | 107 | ||
126 | setTokenField (accessToken: string, field: string, value: string) { | 108 | setTokenField (accessToken: string, field: string, value: string) { |
127 | const seq = this.getSequelize() | 109 | return this.updateQuery( |
128 | 110 | `UPDATE "oAuthToken" SET ${this.escapeColumnName(field)} = :value WHERE "accessToken" = :accessToken`, | |
129 | const options = { type: QueryTypes.UPDATE } | 111 | { value, accessToken } |
130 | 112 | ) | |
131 | return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options) | ||
132 | } | 113 | } |
133 | 114 | ||
134 | async cleanup () { | 115 | async cleanup () { |
@@ -157,4 +138,9 @@ export class SQLCommand extends AbstractCommand { | |||
157 | return this.sequelize | 138 | return this.sequelize |
158 | } | 139 | } |
159 | 140 | ||
141 | private escapeColumnName (columnName: string) { | ||
142 | return this.getSequelize().escape(columnName) | ||
143 | .replace(/^'/, '"') | ||
144 | .replace(/'$/, '"') | ||
145 | } | ||
160 | } | 146 | } |
diff --git a/shared/server-commands/requests/requests.ts b/shared/server-commands/requests/requests.ts index dc9cf4e01..cb0e1a5fb 100644 --- a/shared/server-commands/requests/requests.ts +++ b/shared/server-commands/requests/requests.ts | |||
@@ -199,7 +199,7 @@ function buildRequest (req: request.Test, options: CommonRequestParams) { | |||
199 | return req.expect((res) => { | 199 | return req.expect((res) => { |
200 | if (options.expectedStatus && res.status !== options.expectedStatus) { | 200 | if (options.expectedStatus && res.status !== options.expectedStatus) { |
201 | throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` + | 201 | throw new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` + |
202 | `\nThe server responded this error: "${res.body?.error ?? res.text}".\n` + | 202 | `\nThe server responded: "${res.body?.error ?? res.text}".\n` + |
203 | 'You may take a closer look at the logs. To see how to do so, check out this page: ' + | 203 | 'You may take a closer look at the logs. To see how to do so, check out this page: ' + |
204 | 'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs') | 204 | 'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs') |
205 | } | 205 | } |
diff --git a/support/doc/dependencies.md b/support/doc/dependencies.md index bf53b8080..5cf1d5879 100644 --- a/support/doc/dependencies.md +++ b/support/doc/dependencies.md | |||
@@ -2,8 +2,6 @@ | |||
2 | 2 | ||
3 | :warning: **Warning**: dependencies guide is maintained by the community. Some parts may be outdated! :warning: | 3 | :warning: **Warning**: dependencies guide is maintained by the community. Some parts may be outdated! :warning: |
4 | 4 | ||
5 | Follow the below guides, and check their versions match [required external dependencies versions](https://github.com/Chocobozzz/PeerTube/blob/master/engines.yaml). | ||
6 | |||
7 | Main dependencies version supported by PeerTube: | 5 | Main dependencies version supported by PeerTube: |
8 | 6 | ||
9 | * `node` >=14.x | 7 | * `node` >=14.x |
diff --git a/support/doc/docker.md b/support/doc/docker.md index 267863a4d..b6990f3e3 100644 --- a/support/doc/docker.md +++ b/support/doc/docker.md | |||
@@ -120,7 +120,7 @@ See the production guide ["What now" section](https://docs.joinpeertube.org/inst | |||
120 | 120 | ||
121 | ## Upgrade | 121 | ## Upgrade |
122 | 122 | ||
123 | **Important:** Before upgrading, check you have all the `storage` fields in your [production.yaml file](https://github.com/Chocobozzz/PeerTube/blob/master/support/docker/production/config/production.yaml). | 123 | **Check the changelog (in particular the *IMPORTANT NOTES* section):** https://github.com/Chocobozzz/PeerTube/blob/develop/CHANGELOG.md |
124 | 124 | ||
125 | Pull the latest images: | 125 | Pull the latest images: |
126 | 126 | ||
diff --git a/support/doc/plugins/guide.md b/support/doc/plugins/guide.md index a1131ced5..9ddab3ece 100644 --- a/support/doc/plugins/guide.md +++ b/support/doc/plugins/guide.md | |||
@@ -433,7 +433,27 @@ function register (...) { | |||
433 | username: 'user' | 433 | username: 'user' |
434 | email: 'user@example.com' | 434 | email: 'user@example.com' |
435 | role: 2 | 435 | role: 2 |
436 | displayName: 'User display name' | 436 | displayName: 'User display name', |
437 | |||
438 | // Custom admin flags (bypass video auto moderation etc.) | ||
439 | // https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/users/user-flag.model.ts | ||
440 | // PeerTube >= 5.1 | ||
441 | adminFlags: 0, | ||
442 | // Quota in bytes | ||
443 | // PeerTube >= 5.1 | ||
444 | videoQuota: 1024 * 1024 * 1024, // 1GB | ||
445 | // PeerTube >= 5.1 | ||
446 | videoQuotaDaily: -1, // Unlimited | ||
447 | |||
448 | // Update the user profile if it already exists | ||
449 | // Default behaviour is no update | ||
450 | // Introduced in PeerTube >= 5.1 | ||
451 | userUpdater: ({ fieldName, currentValue, newValue }) => { | ||
452 | // Always use new value except for videoQuotaDaily field | ||
453 | if (fieldName === 'videoQuotaDaily') return currentValue | ||
454 | |||
455 | return newValue | ||
456 | } | ||
437 | }) | 457 | }) |
438 | }) | 458 | }) |
439 | 459 | ||
diff --git a/support/doc/production.md b/support/doc/production.md index dd57e9120..9a84f19a3 100644 --- a/support/doc/production.md +++ b/support/doc/production.md | |||
@@ -177,16 +177,17 @@ $ sudo vim /etc/letsencrypt/renewal/your-domain.com.conf | |||
177 | 177 | ||
178 | If you plan to have many concurrent viewers on your PeerTube instance, consider increasing `worker_connections` value: https://nginx.org/en/docs/ngx_core_module.html#worker_connections. | 178 | If you plan to have many concurrent viewers on your PeerTube instance, consider increasing `worker_connections` value: https://nginx.org/en/docs/ngx_core_module.html#worker_connections. |
179 | 179 | ||
180 | **FreeBSD** | 180 | <details> |
181 | <summary><strong>If using FreeBSD</strong></summary> | ||
182 | |||
181 | On FreeBSD you can use [Dehydrated](https://dehydrated.io/) `security/dehydrated` for [Let's Encrypt](https://letsencrypt.org/) | 183 | On FreeBSD you can use [Dehydrated](https://dehydrated.io/) `security/dehydrated` for [Let's Encrypt](https://letsencrypt.org/) |
182 | 184 | ||
183 | ```bash | 185 | ```bash |
184 | $ sudo pkg install dehydrated | 186 | $ sudo pkg install dehydrated |
185 | ``` | 187 | ``` |
188 | </details> | ||
186 | 189 | ||
187 | ### :alembic: TCP/IP Tuning | 190 | ### :alembic: Linux TCP/IP Tuning |
188 | |||
189 | **On Linux** | ||
190 | 191 | ||
191 | ```bash | 192 | ```bash |
192 | $ sudo cp /var/www/peertube/peertube-latest/support/sysctl.d/30-peertube-tcp.conf /etc/sysctl.d/ | 193 | $ sudo cp /var/www/peertube/peertube-latest/support/sysctl.d/30-peertube-tcp.conf /etc/sysctl.d/ |
@@ -231,7 +232,9 @@ $ sudo systemctl start peertube | |||
231 | $ sudo journalctl -feu peertube | 232 | $ sudo journalctl -feu peertube |
232 | ``` | 233 | ``` |
233 | 234 | ||
234 | **FreeBSD** | 235 | <details> |
236 | <summary><strong>If using FreeBSD</strong></summary> | ||
237 | |||
235 | On FreeBSD, copy the startup script and update rc.conf: | 238 | On FreeBSD, copy the startup script and update rc.conf: |
236 | 239 | ||
237 | ```bash | 240 | ```bash |
@@ -244,8 +247,10 @@ Run: | |||
244 | ```bash | 247 | ```bash |
245 | $ sudo service peertube start | 248 | $ sudo service peertube start |
246 | ``` | 249 | ``` |
250 | </details> | ||
247 | 251 | ||
248 | ### :bricks: OpenRC | 252 | <details> |
253 | <summary><strong>If using OpenRC</strong></summary> | ||
249 | 254 | ||
250 | If your OS uses OpenRC, copy the service script: | 255 | If your OS uses OpenRC, copy the service script: |
251 | 256 | ||
@@ -265,6 +270,7 @@ Run and print last logs: | |||
265 | $ sudo /etc/init.d/peertube start | 270 | $ sudo /etc/init.d/peertube start |
266 | $ tail -f /var/log/peertube/peertube.log | 271 | $ tail -f /var/log/peertube/peertube.log |
267 | ``` | 272 | ``` |
273 | </details> | ||
268 | 274 | ||
269 | ### :technologist: Administrator | 275 | ### :technologist: Administrator |
270 | 276 | ||
@@ -291,16 +297,15 @@ Now your instance is up you can: | |||
291 | 297 | ||
292 | **Check the changelog (in particular the *IMPORTANT NOTES* section):** https://github.com/Chocobozzz/PeerTube/blob/develop/CHANGELOG.md | 298 | **Check the changelog (in particular the *IMPORTANT NOTES* section):** https://github.com/Chocobozzz/PeerTube/blob/develop/CHANGELOG.md |
293 | 299 | ||
294 | #### Auto | 300 | Run the upgrade script (the password it asks is PeerTube's database user password): |
295 | |||
296 | The password it asks is PeerTube's database user password. | ||
297 | 301 | ||
298 | ```bash | 302 | ```bash |
299 | $ cd /var/www/peertube/peertube-latest/scripts && sudo -H -u peertube ./upgrade.sh | 303 | $ cd /var/www/peertube/peertube-latest/scripts && sudo -H -u peertube ./upgrade.sh |
300 | $ sudo systemctl restart peertube # Or use your OS command to restart PeerTube if you don't use systemd | 304 | $ sudo systemctl restart peertube # Or use your OS command to restart PeerTube if you don't use systemd |
301 | ``` | 305 | ``` |
302 | 306 | ||
303 | #### Manually | 307 | <details> |
308 | <summary><strong>Prefer manual upgrade?</strong></summary> | ||
304 | 309 | ||
305 | Make a SQL backup | 310 | Make a SQL backup |
306 | 311 | ||
@@ -346,17 +351,18 @@ $ cd /var/www/peertube && \ | |||
346 | sudo unlink ./peertube-latest && \ | 351 | sudo unlink ./peertube-latest && \ |
347 | sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest | 352 | sudo -u peertube ln -s versions/peertube-${VERSION} ./peertube-latest |
348 | ``` | 353 | ``` |
354 | </details> | ||
349 | 355 | ||
350 | ### Configuration | 356 | ### Update PeerTube configuration |
351 | 357 | ||
352 | You can check for configuration changes, and report them in your `config/production.yaml` file: | 358 | Check for configuration changes, and report them in your `config/production.yaml` file: |
353 | 359 | ||
354 | ```bash | 360 | ```bash |
355 | $ cd /var/www/peertube/versions | 361 | $ cd /var/www/peertube/versions |
356 | $ diff -u "$(ls --sort=t | head -2 | tail -1)/config/production.yaml.example" "$(ls --sort=t | head -1)/config/production.yaml.example" | 362 | $ diff -u "$(ls --sort=t | head -2 | tail -1)/config/production.yaml.example" "$(ls --sort=t | head -1)/config/production.yaml.example" |
357 | ``` | 363 | ``` |
358 | 364 | ||
359 | ### nginx | 365 | ### Update nginx configuration |
360 | 366 | ||
361 | Check changes in nginx configuration: | 367 | Check changes in nginx configuration: |
362 | 368 | ||
@@ -365,7 +371,7 @@ $ cd /var/www/peertube/versions | |||
365 | $ diff -u "$(ls --sort=t | head -2 | tail -1)/support/nginx/peertube" "$(ls --sort=t | head -1)/support/nginx/peertube" | 371 | $ diff -u "$(ls --sort=t | head -2 | tail -1)/support/nginx/peertube" "$(ls --sort=t | head -1)/support/nginx/peertube" |
366 | ``` | 372 | ``` |
367 | 373 | ||
368 | ### systemd | 374 | ### Update systemd service |
369 | 375 | ||
370 | Check changes in systemd configuration: | 376 | Check changes in systemd configuration: |
371 | 377 | ||
diff --git a/tsconfig.json b/tsconfig.json index 993acf81d..8bcd944e3 100644 --- a/tsconfig.json +++ b/tsconfig.json | |||
@@ -8,7 +8,6 @@ | |||
8 | "@shared/*": [ "shared/*" ] | 8 | "@shared/*": [ "shared/*" ] |
9 | }, | 9 | }, |
10 | "typeRoots": [ | 10 | "typeRoots": [ |
11 | "server/typings", | ||
12 | "node_modules/@types" | 11 | "node_modules/@types" |
13 | ] | 12 | ] |
14 | }, | 13 | }, |
@@ -17,5 +16,5 @@ | |||
17 | { "path": "./server" }, | 16 | { "path": "./server" }, |
18 | { "path": "./scripts" } | 17 | { "path": "./scripts" } |
19 | ], | 18 | ], |
20 | "files": [ "server.ts", "server/types/express.d.ts" ] | 19 | "files": [ "server.ts", "server/types/express.d.ts", "server/types/lib.d.ts" ] |
21 | } | 20 | } |
@@ -9099,7 +9099,7 @@ typedarray@^0.0.6: | |||
9099 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" | 9099 | resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" |
9100 | integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== | 9100 | integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== |
9101 | 9101 | ||
9102 | typescript@^4.0.5: | 9102 | typescript@~4.8: |
9103 | version "4.8.4" | 9103 | version "4.8.4" |
9104 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" | 9104 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" |
9105 | integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== | 9105 | integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== |