aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared
diff options
context:
space:
mode:
Diffstat (limited to 'shared')
-rw-r--r--shared/core-utils/common/date.ts (renamed from shared/core-utils/miscs/date.ts)47
-rw-r--r--shared/core-utils/common/index.ts6
-rw-r--r--shared/core-utils/common/miscs.ts (renamed from shared/core-utils/miscs/miscs.ts)10
-rw-r--r--shared/core-utils/common/promises.ts12
-rw-r--r--shared/core-utils/common/regexp.ts5
-rw-r--r--shared/core-utils/common/types.ts (renamed from shared/core-utils/miscs/types.ts)0
-rw-r--r--shared/core-utils/common/url.ts130
-rw-r--r--shared/core-utils/index.ts4
-rw-r--r--shared/core-utils/logs/index.ts1
-rw-r--r--shared/core-utils/logs/logs.ts25
-rw-r--r--shared/core-utils/miscs/index.ts5
-rw-r--r--shared/core-utils/plugins/hooks.ts2
-rw-r--r--shared/core-utils/utils/index.ts1
-rw-r--r--shared/core-utils/utils/object.ts15
-rw-r--r--shared/extra-utils/bulk/bulk-command.ts20
-rw-r--r--shared/extra-utils/bulk/bulk.ts25
-rw-r--r--shared/extra-utils/bulk/index.ts1
-rw-r--r--shared/extra-utils/cli/cli-command.ts23
-rw-r--r--shared/extra-utils/cli/cli.ts24
-rw-r--r--shared/extra-utils/cli/index.ts1
-rw-r--r--shared/extra-utils/custom-pages/custom-pages-command.ts33
-rw-r--r--shared/extra-utils/custom-pages/custom-pages.ts31
-rw-r--r--shared/extra-utils/custom-pages/index.ts1
-rw-r--r--shared/extra-utils/feeds/feeds-command.ts44
-rw-r--r--shared/extra-utils/feeds/feeds.ts33
-rw-r--r--shared/extra-utils/feeds/index.ts1
-rw-r--r--shared/extra-utils/index.ts66
-rw-r--r--shared/extra-utils/logs/index.ts1
-rw-r--r--shared/extra-utils/logs/logs-command.ts43
-rw-r--r--shared/extra-utils/logs/logs.ts32
-rw-r--r--shared/extra-utils/miscs/checks.ts46
-rw-r--r--shared/extra-utils/miscs/generate.ts61
-rw-r--r--shared/extra-utils/miscs/index.ts5
-rw-r--r--shared/extra-utils/miscs/miscs.ts170
-rw-r--r--shared/extra-utils/miscs/sql-command.ts142
-rw-r--r--shared/extra-utils/miscs/sql.ts161
-rw-r--r--shared/extra-utils/miscs/stubs.ts14
-rw-r--r--shared/extra-utils/miscs/tests.ts94
-rw-r--r--shared/extra-utils/miscs/webtorrent.ts31
-rw-r--r--shared/extra-utils/mock-servers/index.ts4
-rw-r--r--shared/extra-utils/mock-servers/mock-email.ts (renamed from shared/extra-utils/miscs/email.ts)4
-rw-r--r--shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts (renamed from shared/extra-utils/mock-servers/joinpeertube-versions.ts)0
-rw-r--r--shared/extra-utils/mock-servers/mock-plugin-blocklist.ts (renamed from shared/extra-utils/plugins/mock-blocklist.ts)0
-rw-r--r--shared/extra-utils/moderation/abuses-command.ts228
-rw-r--r--shared/extra-utils/moderation/abuses.ts244
-rw-r--r--shared/extra-utils/moderation/index.ts1
-rw-r--r--shared/extra-utils/overviews/index.ts1
-rw-r--r--shared/extra-utils/overviews/overviews-command.ts23
-rw-r--r--shared/extra-utils/overviews/overviews.ts34
-rw-r--r--shared/extra-utils/requests/activitypub.ts2
-rw-r--r--shared/extra-utils/requests/check-api-params.ts19
-rw-r--r--shared/extra-utils/requests/index.ts3
-rw-r--r--shared/extra-utils/requests/requests.ts238
-rw-r--r--shared/extra-utils/search/index.ts1
-rw-r--r--shared/extra-utils/search/search-command.ts98
-rw-r--r--shared/extra-utils/search/video-channels.ts36
-rw-r--r--shared/extra-utils/search/video-playlists.ts36
-rw-r--r--shared/extra-utils/search/videos.ts64
-rw-r--r--shared/extra-utils/server/activitypub.ts15
-rw-r--r--shared/extra-utils/server/clients.ts20
-rw-r--r--shared/extra-utils/server/config-command.ts263
-rw-r--r--shared/extra-utils/server/config.ts260
-rw-r--r--shared/extra-utils/server/contact-form-command.ts31
-rw-r--r--shared/extra-utils/server/contact-form.ts31
-rw-r--r--shared/extra-utils/server/debug-command.ts33
-rw-r--r--shared/extra-utils/server/debug.ts33
-rw-r--r--shared/extra-utils/server/directories.ts34
-rw-r--r--shared/extra-utils/server/follows-command.ts139
-rw-r--r--shared/extra-utils/server/follows.ts133
-rw-r--r--shared/extra-utils/server/index.ts15
-rw-r--r--shared/extra-utils/server/jobs-command.ts36
-rw-r--r--shared/extra-utils/server/jobs.ts82
-rw-r--r--shared/extra-utils/server/plugins-command.ts256
-rw-r--r--shared/extra-utils/server/plugins.ts299
-rw-r--r--shared/extra-utils/server/redundancy-command.ts80
-rw-r--r--shared/extra-utils/server/redundancy.ts88
-rw-r--r--shared/extra-utils/server/server.ts369
-rw-r--r--shared/extra-utils/server/servers-command.ts88
-rw-r--r--shared/extra-utils/server/servers.ts375
-rw-r--r--shared/extra-utils/server/stats-command.ts25
-rw-r--r--shared/extra-utils/server/stats.ts23
-rw-r--r--shared/extra-utils/shared/abstract-command.ts199
-rw-r--r--shared/extra-utils/shared/index.ts1
-rw-r--r--shared/extra-utils/socket/index.ts1
-rw-r--r--shared/extra-utils/socket/socket-io-command.ts15
-rw-r--r--shared/extra-utils/socket/socket-io.ts18
-rw-r--r--shared/extra-utils/users/accounts-command.ts56
-rw-r--r--shared/extra-utils/users/accounts.ts87
-rw-r--r--shared/extra-utils/users/actors.ts73
-rw-r--r--shared/extra-utils/users/blocklist-command.ts139
-rw-r--r--shared/extra-utils/users/blocklist.ts238
-rw-r--r--shared/extra-utils/users/index.ts9
-rw-r--r--shared/extra-utils/users/login-command.ts132
-rw-r--r--shared/extra-utils/users/login.ts124
-rw-r--r--shared/extra-utils/users/notifications-command.ts86
-rw-r--r--shared/extra-utils/users/notifications.ts (renamed from shared/extra-utils/users/user-notifications.ts)628
-rw-r--r--shared/extra-utils/users/subscriptions-command.ts99
-rw-r--r--shared/extra-utils/users/user-subscriptions.ts93
-rw-r--r--shared/extra-utils/users/users-command.ts415
-rw-r--r--shared/extra-utils/users/users.ts415
-rw-r--r--shared/extra-utils/videos/blacklist-command.ts76
-rw-r--r--shared/extra-utils/videos/captions-command.ts65
-rw-r--r--shared/extra-utils/videos/captions.ts17
-rw-r--r--shared/extra-utils/videos/change-ownership-command.ts68
-rw-r--r--shared/extra-utils/videos/channels-command.ts156
-rw-r--r--shared/extra-utils/videos/channels.ts18
-rw-r--r--shared/extra-utils/videos/comments-command.ts152
-rw-r--r--shared/extra-utils/videos/history-command.ts58
-rw-r--r--shared/extra-utils/videos/imports-command.ts47
-rw-r--r--shared/extra-utils/videos/index.ts19
-rw-r--r--shared/extra-utils/videos/live-command.ts154
-rw-r--r--shared/extra-utils/videos/live.ts159
-rw-r--r--shared/extra-utils/videos/playlists-command.ts280
-rw-r--r--shared/extra-utils/videos/playlists.ts25
-rw-r--r--shared/extra-utils/videos/services-command.ts29
-rw-r--r--shared/extra-utils/videos/services.ts24
-rw-r--r--shared/extra-utils/videos/streaming-playlists-command.ts44
-rw-r--r--shared/extra-utils/videos/streaming-playlists.ts78
-rw-r--r--shared/extra-utils/videos/video-blacklist.ts79
-rw-r--r--shared/extra-utils/videos/video-captions.ts72
-rw-r--r--shared/extra-utils/videos/video-change-ownership.ts72
-rw-r--r--shared/extra-utils/videos/video-channels.ts192
-rw-r--r--shared/extra-utils/videos/video-comments.ts138
-rw-r--r--shared/extra-utils/videos/video-history.ts49
-rw-r--r--shared/extra-utils/videos/video-imports.ts90
-rw-r--r--shared/extra-utils/videos/video-playlists.ts320
-rw-r--r--shared/extra-utils/videos/video-streaming-playlists.ts82
-rw-r--r--shared/extra-utils/videos/videos-command.ts599
-rw-r--r--shared/extra-utils/videos/videos.ts816
-rw-r--r--shared/models/http/http-error-codes.ts (renamed from shared/core-utils/miscs/http-error-codes.ts)0
-rw-r--r--shared/models/http/http-methods.ts (renamed from shared/core-utils/miscs/http-methods.ts)0
-rw-r--r--shared/models/http/index.ts2
-rw-r--r--shared/models/index.ts1
-rw-r--r--shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts12
-rw-r--r--shared/models/plugins/server/managers/plugin-video-category-manager.model.ts10
-rw-r--r--shared/models/plugins/server/managers/plugin-video-language-manager.model.ts10
-rw-r--r--shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts10
-rw-r--r--shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts14
-rw-r--r--shared/models/plugins/server/plugin-constant-manager.model.ts7
-rw-r--r--shared/models/plugins/server/server-hook.model.ts4
-rw-r--r--shared/models/search/video-channels-search-query.model.ts11
-rw-r--r--shared/models/search/video-playlists-search-query.model.ts13
-rw-r--r--shared/models/search/videos-common-query.model.ts8
-rw-r--r--shared/models/search/videos-search-query.model.ts11
-rw-r--r--shared/models/server/debug.model.ts1
-rw-r--r--shared/models/server/index.ts1
-rw-r--r--shared/models/server/peertube-problem-document.model.ts2
-rw-r--r--shared/models/server/server-follow-create.model.ts4
-rw-r--r--shared/models/users/index.ts1
-rw-r--r--shared/models/users/user-create-result.model.ts7
-rw-r--r--shared/models/users/user-notification.model.ts7
-rw-r--r--shared/models/videos/channel/index.ts1
-rw-r--r--shared/models/videos/channel/video-channel-create-result.model.ts3
-rw-r--r--shared/models/videos/comment/index.ts1
-rw-r--r--shared/models/videos/comment/video-comment-create.model.ts3
-rw-r--r--shared/models/videos/comment/video-comment.model.ts7
-rw-r--r--shared/models/videos/playlist/index.ts1
-rw-r--r--shared/models/videos/playlist/video-playlist-element-create-result.model.ts3
-rw-r--r--shared/models/videos/video-update.model.ts1
159 files changed, 6320 insertions, 5772 deletions
diff --git a/shared/core-utils/miscs/date.ts b/shared/core-utils/common/date.ts
index 4f92f758f..3e4a3c08c 100644
--- a/shared/core-utils/miscs/date.ts
+++ b/shared/core-utils/common/date.ts
@@ -43,6 +43,49 @@ function isLastWeek (d: Date) {
43 return getDaysDifferences(now, d) <= 7 43 return getDaysDifferences(now, d) <= 7
44} 44}
45 45
46function timeToInt (time: number | string) {
47 if (!time) return 0
48 if (typeof time === 'number') return time
49
50 const reg = /^((\d+)[h:])?((\d+)[m:])?((\d+)s?)?$/
51 const matches = time.match(reg)
52
53 if (!matches) return 0
54
55 const hours = parseInt(matches[2] || '0', 10)
56 const minutes = parseInt(matches[4] || '0', 10)
57 const seconds = parseInt(matches[6] || '0', 10)
58
59 return hours * 3600 + minutes * 60 + seconds
60}
61
62function secondsToTime (seconds: number, full = false, symbol?: string) {
63 let time = ''
64
65 if (seconds === 0 && !full) return '0s'
66
67 const hourSymbol = (symbol || 'h')
68 const minuteSymbol = (symbol || 'm')
69 const secondsSymbol = full ? '' : 's'
70
71 const hours = Math.floor(seconds / 3600)
72 if (hours >= 1) time = hours + hourSymbol
73 else if (full) time = '0' + hourSymbol
74
75 seconds %= 3600
76 const minutes = Math.floor(seconds / 60)
77 if (minutes >= 1 && minutes < 10 && full) time += '0' + minutes + minuteSymbol
78 else if (minutes >= 1) time += minutes + minuteSymbol
79 else if (full) time += '00' + minuteSymbol
80
81 seconds %= 60
82 if (seconds >= 1 && seconds < 10 && full) time += '0' + seconds + secondsSymbol
83 else if (seconds >= 1) time += seconds + secondsSymbol
84 else if (full) time += '00'
85
86 return time
87}
88
46// --------------------------------------------------------------------------- 89// ---------------------------------------------------------------------------
47 90
48export { 91export {
@@ -51,7 +94,9 @@ export {
51 isThisMonth, 94 isThisMonth,
52 isToday, 95 isToday,
53 isLastMonth, 96 isLastMonth,
54 isLastWeek 97 isLastWeek,
98 timeToInt,
99 secondsToTime
55} 100}
56 101
57// --------------------------------------------------------------------------- 102// ---------------------------------------------------------------------------
diff --git a/shared/core-utils/common/index.ts b/shared/core-utils/common/index.ts
new file mode 100644
index 000000000..0908ff981
--- /dev/null
+++ b/shared/core-utils/common/index.ts
@@ -0,0 +1,6 @@
1export * from './date'
2export * from './miscs'
3export * from './regexp'
4export * from './promises'
5export * from './types'
6export * from './url'
diff --git a/shared/core-utils/miscs/miscs.ts b/shared/core-utils/common/miscs.ts
index 4780ca922..bc65dc338 100644
--- a/shared/core-utils/miscs/miscs.ts
+++ b/shared/core-utils/common/miscs.ts
@@ -20,14 +20,6 @@ function compareSemVer (a: string, b: string) {
20 return segmentsA.length - segmentsB.length 20 return segmentsA.length - segmentsB.length
21} 21}
22 22
23function isPromise (value: any) {
24 return value && typeof value.then === 'function'
25}
26
27function isCatchable (value: any) {
28 return value && typeof value.catch === 'function'
29}
30
31function sortObjectComparator (key: string, order: 'asc' | 'desc') { 23function sortObjectComparator (key: string, order: 'asc' | 'desc') {
32 return (a: any, b: any) => { 24 return (a: any, b: any) => {
33 if (a[key] < b[key]) { 25 if (a[key] < b[key]) {
@@ -45,7 +37,5 @@ function sortObjectComparator (key: string, order: 'asc' | 'desc') {
45export { 37export {
46 randomInt, 38 randomInt,
47 compareSemVer, 39 compareSemVer,
48 isPromise,
49 isCatchable,
50 sortObjectComparator 40 sortObjectComparator
51} 41}
diff --git a/shared/core-utils/common/promises.ts b/shared/core-utils/common/promises.ts
new file mode 100644
index 000000000..7ef9d60b6
--- /dev/null
+++ b/shared/core-utils/common/promises.ts
@@ -0,0 +1,12 @@
1function isPromise (value: any) {
2 return value && typeof value.then === 'function'
3}
4
5function isCatchable (value: any) {
6 return value && typeof value.catch === 'function'
7}
8
9export {
10 isPromise,
11 isCatchable
12}
diff --git a/shared/core-utils/common/regexp.ts b/shared/core-utils/common/regexp.ts
new file mode 100644
index 000000000..59eb87eb6
--- /dev/null
+++ b/shared/core-utils/common/regexp.ts
@@ -0,0 +1,5 @@
1export const uuidRegex = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
2
3export function removeFragmentedMP4Ext (path: string) {
4 return path.replace(/-fragmented.mp4$/i, '')
5}
diff --git a/shared/core-utils/miscs/types.ts b/shared/core-utils/common/types.ts
index bd2a97b98..bd2a97b98 100644
--- a/shared/core-utils/miscs/types.ts
+++ b/shared/core-utils/common/types.ts
diff --git a/shared/core-utils/common/url.ts b/shared/core-utils/common/url.ts
new file mode 100644
index 000000000..52ed247c4
--- /dev/null
+++ b/shared/core-utils/common/url.ts
@@ -0,0 +1,130 @@
1import { Video, VideoPlaylist } from '../../models'
2import { secondsToTime } from './date'
3
4function buildPlaylistLink (playlist: Pick<VideoPlaylist, 'shortUUID'>, base?: string) {
5 return (base ?? window.location.origin) + buildPlaylistWatchPath(playlist)
6}
7
8function buildPlaylistWatchPath (playlist: Pick<VideoPlaylist, 'shortUUID'>) {
9 return '/w/p/' + playlist.shortUUID
10}
11
12function buildVideoWatchPath (video: Pick<Video, 'shortUUID'>) {
13 return '/w/' + video.shortUUID
14}
15
16function buildVideoLink (video: Pick<Video, 'shortUUID'>, base?: string) {
17 return (base ?? window.location.origin) + buildVideoWatchPath(video)
18}
19
20function buildPlaylistEmbedPath (playlist: Pick<VideoPlaylist, 'uuid'>) {
21 return '/video-playlists/embed/' + playlist.uuid
22}
23
24function buildPlaylistEmbedLink (playlist: Pick<VideoPlaylist, 'uuid'>, base?: string) {
25 return (base ?? window.location.origin) + buildPlaylistEmbedPath(playlist)
26}
27
28function buildVideoEmbedPath (video: Pick<Video, 'uuid'>) {
29 return '/videos/embed/' + video.uuid
30}
31
32function buildVideoEmbedLink (video: Pick<Video, 'uuid'>, base?: string) {
33 return (base ?? window.location.origin) + buildVideoEmbedPath(video)
34}
35
36function decorateVideoLink (options: {
37 url: string
38
39 startTime?: number
40 stopTime?: number
41
42 subtitle?: string
43
44 loop?: boolean
45 autoplay?: boolean
46 muted?: boolean
47
48 // Embed options
49 title?: boolean
50 warningTitle?: boolean
51 controls?: boolean
52 peertubeLink?: boolean
53}) {
54 const { url } = options
55
56 const params = generateParams(window.location.search)
57
58 if (options.startTime !== undefined && options.startTime !== null) {
59 const startTimeInt = Math.floor(options.startTime)
60 params.set('start', secondsToTime(startTimeInt))
61 }
62
63 if (options.stopTime) {
64 const stopTimeInt = Math.floor(options.stopTime)
65 params.set('stop', secondsToTime(stopTimeInt))
66 }
67
68 if (options.subtitle) params.set('subtitle', options.subtitle)
69
70 if (options.loop === true) params.set('loop', '1')
71 if (options.autoplay === true) params.set('autoplay', '1')
72 if (options.muted === true) params.set('muted', '1')
73 if (options.title === false) params.set('title', '0')
74 if (options.warningTitle === false) params.set('warningTitle', '0')
75 if (options.controls === false) params.set('controls', '0')
76 if (options.peertubeLink === false) params.set('peertubeLink', '0')
77
78 return buildUrl(url, params)
79}
80
81function decoratePlaylistLink (options: {
82 url: string
83
84 playlistPosition?: number
85}) {
86 const { url } = options
87
88 const params = generateParams(window.location.search)
89
90 if (options.playlistPosition) params.set('playlistPosition', '' + options.playlistPosition)
91
92 return buildUrl(url, params)
93}
94
95// ---------------------------------------------------------------------------
96
97export {
98 buildPlaylistLink,
99 buildVideoLink,
100
101 buildVideoWatchPath,
102 buildPlaylistWatchPath,
103
104 buildPlaylistEmbedPath,
105 buildVideoEmbedPath,
106
107 buildPlaylistEmbedLink,
108 buildVideoEmbedLink,
109
110 decorateVideoLink,
111 decoratePlaylistLink
112}
113
114function buildUrl (url: string, params: URLSearchParams) {
115 let hasParams = false
116 params.forEach(() => { hasParams = true })
117
118 if (hasParams) return url + '?' + params.toString()
119
120 return url
121}
122
123function generateParams (url: string) {
124 const params = new URLSearchParams(window.location.search)
125 // Unused parameters in embed
126 params.delete('videoId')
127 params.delete('resume')
128
129 return params
130}
diff --git a/shared/core-utils/index.ts b/shared/core-utils/index.ts
index 42d7cab1d..2a7d4d982 100644
--- a/shared/core-utils/index.ts
+++ b/shared/core-utils/index.ts
@@ -1,7 +1,7 @@
1export * from './abuse' 1export * from './abuse'
2export * from './common'
2export * from './i18n' 3export * from './i18n'
3export * from './logs'
4export * from './miscs'
5export * from './plugins' 4export * from './plugins'
6export * from './renderer' 5export * from './renderer'
7export * from './users' 6export * from './users'
7export * from './utils'
diff --git a/shared/core-utils/logs/index.ts b/shared/core-utils/logs/index.ts
deleted file mode 100644
index ceb5d7a7f..000000000
--- a/shared/core-utils/logs/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './logs'
diff --git a/shared/core-utils/logs/logs.ts b/shared/core-utils/logs/logs.ts
deleted file mode 100644
index d0996cf55..000000000
--- a/shared/core-utils/logs/logs.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import { stat } from 'fs-extra'
2
3async function mtimeSortFilesDesc (files: string[], basePath: string) {
4 const promises = []
5 const out: { file: string, mtime: number }[] = []
6
7 for (const file of files) {
8 const p = stat(basePath + '/' + file)
9 .then(stats => {
10 if (stats.isFile()) out.push({ file, mtime: stats.mtime.getTime() })
11 })
12
13 promises.push(p)
14 }
15
16 await Promise.all(promises)
17
18 out.sort((a, b) => b.mtime - a.mtime)
19
20 return out
21}
22
23export {
24 mtimeSortFilesDesc
25}
diff --git a/shared/core-utils/miscs/index.ts b/shared/core-utils/miscs/index.ts
deleted file mode 100644
index 251df1de2..000000000
--- a/shared/core-utils/miscs/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
1export * from './date'
2export * from './miscs'
3export * from './types'
4export * from './http-error-codes'
5export * from './http-methods'
diff --git a/shared/core-utils/plugins/hooks.ts b/shared/core-utils/plugins/hooks.ts
index 5405e0529..92cb5ad68 100644
--- a/shared/core-utils/plugins/hooks.ts
+++ b/shared/core-utils/plugins/hooks.ts
@@ -1,5 +1,5 @@
1import { HookType } from '../../models/plugins/hook-type.enum' 1import { HookType } from '../../models/plugins/hook-type.enum'
2import { isCatchable, isPromise } from '../miscs/miscs' 2import { isCatchable, isPromise } from '../common/promises'
3 3
4function getHookType (hookName: string) { 4function getHookType (hookName: string) {
5 if (hookName.startsWith('filter:')) return HookType.FILTER 5 if (hookName.startsWith('filter:')) return HookType.FILTER
diff --git a/shared/core-utils/utils/index.ts b/shared/core-utils/utils/index.ts
new file mode 100644
index 000000000..a71977d88
--- /dev/null
+++ b/shared/core-utils/utils/index.ts
@@ -0,0 +1 @@
export * from './object'
diff --git a/shared/core-utils/utils/object.ts b/shared/core-utils/utils/object.ts
new file mode 100644
index 000000000..9a8a98f9b
--- /dev/null
+++ b/shared/core-utils/utils/object.ts
@@ -0,0 +1,15 @@
1function pick <O extends object, K extends keyof O> (object: O, keys: K[]): Pick<O, K> {
2 const result: any = {}
3
4 for (const key of keys) {
5 if (Object.prototype.hasOwnProperty.call(object, key)) {
6 result[key] = object[key]
7 }
8 }
9
10 return result
11}
12
13export {
14 pick
15}
diff --git a/shared/extra-utils/bulk/bulk-command.ts b/shared/extra-utils/bulk/bulk-command.ts
new file mode 100644
index 000000000..b5c5673ce
--- /dev/null
+++ b/shared/extra-utils/bulk/bulk-command.ts
@@ -0,0 +1,20 @@
1import { BulkRemoveCommentsOfBody, HttpStatusCode } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class BulkCommand extends AbstractCommand {
5
6 removeCommentsOf (options: OverrideCommandOptions & {
7 attributes: BulkRemoveCommentsOfBody
8 }) {
9 const { attributes } = options
10
11 return this.postBodyRequest({
12 ...options,
13
14 path: '/api/v1/bulk/remove-comments-of',
15 fields: attributes,
16 implicitToken: true,
17 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
18 })
19 }
20}
diff --git a/shared/extra-utils/bulk/bulk.ts b/shared/extra-utils/bulk/bulk.ts
deleted file mode 100644
index b6f437b8b..000000000
--- a/shared/extra-utils/bulk/bulk.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1import { BulkRemoveCommentsOfBody } from "@shared/models/bulk/bulk-remove-comments-of-body.model"
2import { makePostBodyRequest } from "../requests/requests"
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function bulkRemoveCommentsOf (options: {
6 url: string
7 token: string
8 attributes: BulkRemoveCommentsOfBody
9 expectedStatus?: number
10}) {
11 const { url, token, attributes, expectedStatus } = options
12 const path = '/api/v1/bulk/remove-comments-of'
13
14 return makePostBodyRequest({
15 url,
16 path,
17 token,
18 fields: attributes,
19 statusCodeExpected: expectedStatus || HttpStatusCode.NO_CONTENT_204
20 })
21}
22
23export {
24 bulkRemoveCommentsOf
25}
diff --git a/shared/extra-utils/bulk/index.ts b/shared/extra-utils/bulk/index.ts
new file mode 100644
index 000000000..391597243
--- /dev/null
+++ b/shared/extra-utils/bulk/index.ts
@@ -0,0 +1 @@
export * from './bulk-command'
diff --git a/shared/extra-utils/cli/cli-command.ts b/shared/extra-utils/cli/cli-command.ts
new file mode 100644
index 000000000..bc1dddc68
--- /dev/null
+++ b/shared/extra-utils/cli/cli-command.ts
@@ -0,0 +1,23 @@
1import { exec } from 'child_process'
2import { AbstractCommand } from '../shared'
3
4export class CLICommand extends AbstractCommand {
5
6 static exec (command: string) {
7 return new Promise<string>((res, rej) => {
8 exec(command, (err, stdout, _stderr) => {
9 if (err) return rej(err)
10
11 return res(stdout)
12 })
13 })
14 }
15
16 getEnv () {
17 return `NODE_ENV=test NODE_APP_INSTANCE=${this.server.internalServerNumber}`
18 }
19
20 async execWithEnv (command: string) {
21 return CLICommand.exec(`${this.getEnv()} ${command}`)
22 }
23}
diff --git a/shared/extra-utils/cli/cli.ts b/shared/extra-utils/cli/cli.ts
deleted file mode 100644
index c62e170bb..000000000
--- a/shared/extra-utils/cli/cli.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import { exec } from 'child_process'
2
3import { ServerInfo } from '../server/servers'
4
5function getEnvCli (server?: ServerInfo) {
6 return `NODE_ENV=test NODE_APP_INSTANCE=${server.internalServerNumber}`
7}
8
9async function execCLI (command: string) {
10 return new Promise<string>((res, rej) => {
11 exec(command, (err, stdout, stderr) => {
12 if (err) return rej(err)
13
14 return res(stdout)
15 })
16 })
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 execCLI,
23 getEnvCli
24}
diff --git a/shared/extra-utils/cli/index.ts b/shared/extra-utils/cli/index.ts
new file mode 100644
index 000000000..91b5abfbe
--- /dev/null
+++ b/shared/extra-utils/cli/index.ts
@@ -0,0 +1 @@
export * from './cli-command'
diff --git a/shared/extra-utils/custom-pages/custom-pages-command.ts b/shared/extra-utils/custom-pages/custom-pages-command.ts
new file mode 100644
index 000000000..cd869a8de
--- /dev/null
+++ b/shared/extra-utils/custom-pages/custom-pages-command.ts
@@ -0,0 +1,33 @@
1import { CustomPage, HttpStatusCode } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class CustomPagesCommand extends AbstractCommand {
5
6 getInstanceHomepage (options: OverrideCommandOptions = {}) {
7 const path = '/api/v1/custom-pages/homepage/instance'
8
9 return this.getRequestBody<CustomPage>({
10 ...options,
11
12 path,
13 implicitToken: false,
14 defaultExpectedStatus: HttpStatusCode.OK_200
15 })
16 }
17
18 updateInstanceHomepage (options: OverrideCommandOptions & {
19 content: string
20 }) {
21 const { content } = options
22 const path = '/api/v1/custom-pages/homepage/instance'
23
24 return this.putBodyRequest({
25 ...options,
26
27 path,
28 fields: { content },
29 implicitToken: true,
30 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
31 })
32 }
33}
diff --git a/shared/extra-utils/custom-pages/custom-pages.ts b/shared/extra-utils/custom-pages/custom-pages.ts
deleted file mode 100644
index bf2d16c70..000000000
--- a/shared/extra-utils/custom-pages/custom-pages.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
2import { makeGetRequest, makePutBodyRequest } from '../requests/requests'
3
4function getInstanceHomepage (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
5 const path = '/api/v1/custom-pages/homepage/instance'
6
7 return makeGetRequest({
8 url,
9 path,
10 statusCodeExpected
11 })
12}
13
14function updateInstanceHomepage (url: string, token: string, content: string) {
15 const path = '/api/v1/custom-pages/homepage/instance'
16
17 return makePutBodyRequest({
18 url,
19 path,
20 token,
21 fields: { content },
22 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
23 })
24}
25
26// ---------------------------------------------------------------------------
27
28export {
29 getInstanceHomepage,
30 updateInstanceHomepage
31}
diff --git a/shared/extra-utils/custom-pages/index.ts b/shared/extra-utils/custom-pages/index.ts
new file mode 100644
index 000000000..58aed04f2
--- /dev/null
+++ b/shared/extra-utils/custom-pages/index.ts
@@ -0,0 +1 @@
export * from './custom-pages-command'
diff --git a/shared/extra-utils/feeds/feeds-command.ts b/shared/extra-utils/feeds/feeds-command.ts
new file mode 100644
index 000000000..3c95f9536
--- /dev/null
+++ b/shared/extra-utils/feeds/feeds-command.ts
@@ -0,0 +1,44 @@
1
2import { HttpStatusCode } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5type FeedType = 'videos' | 'video-comments' | 'subscriptions'
6
7export class FeedCommand extends AbstractCommand {
8
9 getXML (options: OverrideCommandOptions & {
10 feed: FeedType
11 format?: string
12 }) {
13 const { feed, format } = options
14 const path = '/feeds/' + feed + '.xml'
15
16 return this.getRequestText({
17 ...options,
18
19 path,
20 query: format ? { format } : undefined,
21 accept: 'application/xml',
22 implicitToken: false,
23 defaultExpectedStatus: HttpStatusCode.OK_200
24 })
25 }
26
27 getJSON (options: OverrideCommandOptions & {
28 feed: FeedType
29 query?: { [ id: string ]: any }
30 }) {
31 const { feed, query } = options
32 const path = '/feeds/' + feed + '.json'
33
34 return this.getRequestText({
35 ...options,
36
37 path,
38 query,
39 accept: 'application/json',
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 })
43 }
44}
diff --git a/shared/extra-utils/feeds/feeds.ts b/shared/extra-utils/feeds/feeds.ts
deleted file mode 100644
index ce0a98c6d..000000000
--- a/shared/extra-utils/feeds/feeds.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4type FeedType = 'videos' | 'video-comments' | 'subscriptions'
5
6function getXMLfeed (url: string, feed: FeedType, format?: string) {
7 const path = '/feeds/' + feed + '.xml'
8
9 return request(url)
10 .get(path)
11 .query((format) ? { format: format } : {})
12 .set('Accept', 'application/xml')
13 .expect(HttpStatusCode.OK_200)
14 .expect('Content-Type', /xml/)
15}
16
17function getJSONfeed (url: string, feed: FeedType, query: any = {}, statusCodeExpected = HttpStatusCode.OK_200) {
18 const path = '/feeds/' + feed + '.json'
19
20 return request(url)
21 .get(path)
22 .query(query)
23 .set('Accept', 'application/json')
24 .expect(statusCodeExpected)
25 .expect('Content-Type', /json/)
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 getXMLfeed,
32 getJSONfeed
33}
diff --git a/shared/extra-utils/feeds/index.ts b/shared/extra-utils/feeds/index.ts
new file mode 100644
index 000000000..662a22b6f
--- /dev/null
+++ b/shared/extra-utils/feeds/index.ts
@@ -0,0 +1 @@
export * from './feeds-command'
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts
index 87ee8abba..4b3636d06 100644
--- a/shared/extra-utils/index.ts
+++ b/shared/extra-utils/index.ts
@@ -1,51 +1,15 @@
1export * from './bulk/bulk' 1export * from './bulk'
2 2export * from './cli'
3export * from './cli/cli' 3export * from './custom-pages'
4 4export * from './feeds'
5export * from './custom-pages/custom-pages' 5export * from './logs'
6 6export * from './miscs'
7export * from './feeds/feeds' 7export * from './mock-servers'
8 8export * from './moderation'
9export * from './mock-servers/mock-instances-index' 9export * from './overviews'
10 10export * from './requests'
11export * from './miscs/email' 11export * from './search'
12export * from './miscs/sql' 12export * from './server'
13export * from './miscs/miscs' 13export * from './socket'
14export * from './miscs/stubs' 14export * from './users'
15 15export * from './videos'
16export * from './moderation/abuses'
17export * from './plugins/mock-blocklist'
18
19export * from './requests/check-api-params'
20export * from './requests/requests'
21
22export * from './search/video-channels'
23export * from './search/video-playlists'
24export * from './search/videos'
25
26export * from './server/activitypub'
27export * from './server/clients'
28export * from './server/config'
29export * from './server/debug'
30export * from './server/follows'
31export * from './server/jobs'
32export * from './server/plugins'
33export * from './server/servers'
34
35export * from './users/accounts'
36export * from './users/blocklist'
37export * from './users/login'
38export * from './users/user-notifications'
39export * from './users/user-subscriptions'
40export * from './users/users'
41
42export * from './videos/live'
43export * from './videos/services'
44export * from './videos/video-blacklist'
45export * from './videos/video-captions'
46export * from './videos/video-change-ownership'
47export * from './videos/video-channels'
48export * from './videos/video-comments'
49export * from './videos/video-playlists'
50export * from './videos/video-streaming-playlists'
51export * from './videos/videos'
diff --git a/shared/extra-utils/logs/index.ts b/shared/extra-utils/logs/index.ts
new file mode 100644
index 000000000..69452d7f0
--- /dev/null
+++ b/shared/extra-utils/logs/index.ts
@@ -0,0 +1 @@
export * from './logs-command'
diff --git a/shared/extra-utils/logs/logs-command.ts b/shared/extra-utils/logs/logs-command.ts
new file mode 100644
index 000000000..5912e814f
--- /dev/null
+++ b/shared/extra-utils/logs/logs-command.ts
@@ -0,0 +1,43 @@
1import { HttpStatusCode } from '@shared/models'
2import { LogLevel } from '../../models/server/log-level.type'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class LogsCommand extends AbstractCommand {
6
7 getLogs (options: OverrideCommandOptions & {
8 startDate: Date
9 endDate?: Date
10 level?: LogLevel
11 }) {
12 const { startDate, endDate, level } = options
13 const path = '/api/v1/server/logs'
14
15 return this.getRequestBody({
16 ...options,
17
18 path,
19 query: { startDate, endDate, level },
20 implicitToken: true,
21 defaultExpectedStatus: HttpStatusCode.OK_200
22 })
23 }
24
25 getAuditLogs (options: OverrideCommandOptions & {
26 startDate: Date
27 endDate?: Date
28 }) {
29 const { startDate, endDate } = options
30
31 const path = '/api/v1/server/audit-logs'
32
33 return this.getRequestBody({
34 ...options,
35
36 path,
37 query: { startDate, endDate },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.OK_200
40 })
41 }
42
43}
diff --git a/shared/extra-utils/logs/logs.ts b/shared/extra-utils/logs/logs.ts
deleted file mode 100644
index 8d741276c..000000000
--- a/shared/extra-utils/logs/logs.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import { makeGetRequest } from '../requests/requests'
2import { LogLevel } from '../../models/server/log-level.type'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function getLogs (url: string, accessToken: string, startDate: Date, endDate?: Date, level?: LogLevel) {
6 const path = '/api/v1/server/logs'
7
8 return makeGetRequest({
9 url,
10 path,
11 token: accessToken,
12 query: { startDate, endDate, level },
13 statusCodeExpected: HttpStatusCode.OK_200
14 })
15}
16
17function getAuditLogs (url: string, accessToken: string, startDate: Date, endDate?: Date) {
18 const path = '/api/v1/server/audit-logs'
19
20 return makeGetRequest({
21 url,
22 path,
23 token: accessToken,
24 query: { startDate, endDate },
25 statusCodeExpected: HttpStatusCode.OK_200
26 })
27}
28
29export {
30 getLogs,
31 getAuditLogs
32}
diff --git a/shared/extra-utils/miscs/checks.ts b/shared/extra-utils/miscs/checks.ts
new file mode 100644
index 000000000..7fc92f804
--- /dev/null
+++ b/shared/extra-utils/miscs/checks.ts
@@ -0,0 +1,46 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { pathExists, readFile } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@server/helpers/core-utils'
7import { HttpStatusCode } from '@shared/models'
8import { makeGetRequest } from '../requests'
9import { PeerTubeServer } from '../server'
10
11// Default interval -> 5 minutes
12function dateIsValid (dateString: string, interval = 300000) {
13 const dateToCheck = new Date(dateString)
14 const now = new Date()
15
16 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
17}
18
19async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
20 const res = await makeGetRequest({
21 url,
22 path: imagePath,
23 expectedStatus: HttpStatusCode.OK_200
24 })
25
26 const body = res.body
27
28 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
29 const minLength = body.length - ((30 * body.length) / 100)
30 const maxLength = body.length + ((30 * body.length) / 100)
31
32 expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
33 expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
34}
35
36async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
37 const base = server.servers.buildDirectory(directory)
38
39 expect(await pathExists(join(base, filePath))).to.equal(exist)
40}
41
42export {
43 dateIsValid,
44 testImage,
45 testFileExistsOrNot
46}
diff --git a/shared/extra-utils/miscs/generate.ts b/shared/extra-utils/miscs/generate.ts
new file mode 100644
index 000000000..8d6435481
--- /dev/null
+++ b/shared/extra-utils/miscs/generate.ts
@@ -0,0 +1,61 @@
1import * as ffmpeg from 'fluent-ffmpeg'
2import { ensureDir, pathExists } from 'fs-extra'
3import { dirname } from 'path'
4import { buildAbsoluteFixturePath } from './tests'
5
6async function generateHighBitrateVideo () {
7 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
8
9 await ensureDir(dirname(tempFixturePath))
10
11 const exists = await pathExists(tempFixturePath)
12 if (!exists) {
13 console.log('Generating high bitrate video.')
14
15 // Generate a random, high bitrate video on the fly, so we don't have to include
16 // a large file in the repo. The video needs to have a certain minimum length so
17 // that FFmpeg properly applies bitrate limits.
18 // https://stackoverflow.com/a/15795112
19 return new Promise<string>((res, rej) => {
20 ffmpeg()
21 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
22 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
23 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
24 .output(tempFixturePath)
25 .on('error', rej)
26 .on('end', () => res(tempFixturePath))
27 .run()
28 })
29 }
30
31 return tempFixturePath
32}
33
34async function generateVideoWithFramerate (fps = 60) {
35 const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
36
37 await ensureDir(dirname(tempFixturePath))
38
39 const exists = await pathExists(tempFixturePath)
40 if (!exists) {
41 console.log('Generating video with framerate %d.', fps)
42
43 return new Promise<string>((res, rej) => {
44 ffmpeg()
45 .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
46 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
47 .outputOptions([ `-r ${fps}` ])
48 .output(tempFixturePath)
49 .on('error', rej)
50 .on('end', () => res(tempFixturePath))
51 .run()
52 })
53 }
54
55 return tempFixturePath
56}
57
58export {
59 generateHighBitrateVideo,
60 generateVideoWithFramerate
61}
diff --git a/shared/extra-utils/miscs/index.ts b/shared/extra-utils/miscs/index.ts
new file mode 100644
index 000000000..4474661de
--- /dev/null
+++ b/shared/extra-utils/miscs/index.ts
@@ -0,0 +1,5 @@
1export * from './checks'
2export * from './generate'
3export * from './sql-command'
4export * from './tests'
5export * from './webtorrent'
diff --git a/shared/extra-utils/miscs/miscs.ts b/shared/extra-utils/miscs/miscs.ts
deleted file mode 100644
index 462b914d4..000000000
--- a/shared/extra-utils/miscs/miscs.ts
+++ /dev/null
@@ -1,170 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import * as chai from 'chai'
4import * as ffmpeg from 'fluent-ffmpeg'
5import { ensureDir, pathExists, readFile, stat } from 'fs-extra'
6import { basename, dirname, isAbsolute, join, resolve } from 'path'
7import * as request from 'supertest'
8import * as WebTorrent from 'webtorrent'
9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
10
11const expect = chai.expect
12let webtorrent: WebTorrent.Instance
13
14function immutableAssign<T, U> (target: T, source: U) {
15 return Object.assign<{}, T, U>({}, target, source)
16}
17
18// Default interval -> 5 minutes
19function dateIsValid (dateString: string, interval = 300000) {
20 const dateToCheck = new Date(dateString)
21 const now = new Date()
22
23 return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval
24}
25
26function wait (milliseconds: number) {
27 return new Promise(resolve => setTimeout(resolve, milliseconds))
28}
29
30function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
31 const WebTorrent = require('webtorrent')
32
33 if (!webtorrent) webtorrent = new WebTorrent()
34 if (refreshWebTorrent === true) webtorrent = new WebTorrent()
35
36 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
37}
38
39function root () {
40 // We are in /miscs
41 let root = join(__dirname, '..', '..', '..')
42
43 if (basename(root) === 'dist') root = resolve(root, '..')
44
45 return root
46}
47
48function buildServerDirectory (server: { internalServerNumber: number }, directory: string) {
49 return join(root(), 'test' + server.internalServerNumber, directory)
50}
51
52async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
53 const res = await request(url)
54 .get(imagePath)
55 .expect(HttpStatusCode.OK_200)
56
57 const body = res.body
58
59 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
60 const minLength = body.length - ((30 * body.length) / 100)
61 const maxLength = body.length + ((30 * body.length) / 100)
62
63 expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
64 expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
65}
66
67async function testFileExistsOrNot (server: { internalServerNumber: number }, directory: string, filePath: string, exist: boolean) {
68 const base = buildServerDirectory(server, directory)
69
70 expect(await pathExists(join(base, filePath))).to.equal(exist)
71}
72
73function isGithubCI () {
74 return !!process.env.GITHUB_WORKSPACE
75}
76
77function buildAbsoluteFixturePath (path: string, customCIPath = false) {
78 if (isAbsolute(path)) return path
79
80 if (customCIPath && process.env.GITHUB_WORKSPACE) {
81 return join(process.env.GITHUB_WORKSPACE, 'fixtures', path)
82 }
83
84 return join(root(), 'server', 'tests', 'fixtures', path)
85}
86
87function areHttpImportTestsDisabled () {
88 const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true'
89
90 if (disabled) console.log('Import tests are disabled')
91
92 return disabled
93}
94
95async function generateHighBitrateVideo () {
96 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
97
98 await ensureDir(dirname(tempFixturePath))
99
100 const exists = await pathExists(tempFixturePath)
101 if (!exists) {
102 console.log('Generating high bitrate video.')
103
104 // Generate a random, high bitrate video on the fly, so we don't have to include
105 // a large file in the repo. The video needs to have a certain minimum length so
106 // that FFmpeg properly applies bitrate limits.
107 // https://stackoverflow.com/a/15795112
108 return new Promise<string>((res, rej) => {
109 ffmpeg()
110 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
111 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
112 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
113 .output(tempFixturePath)
114 .on('error', rej)
115 .on('end', () => res(tempFixturePath))
116 .run()
117 })
118 }
119
120 return tempFixturePath
121}
122
123async function generateVideoWithFramerate (fps = 60) {
124 const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
125
126 await ensureDir(dirname(tempFixturePath))
127
128 const exists = await pathExists(tempFixturePath)
129 if (!exists) {
130 console.log('Generating video with framerate %d.', fps)
131
132 return new Promise<string>((res, rej) => {
133 ffmpeg()
134 .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
135 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
136 .outputOptions([ `-r ${fps}` ])
137 .output(tempFixturePath)
138 .on('error', rej)
139 .on('end', () => res(tempFixturePath))
140 .run()
141 })
142 }
143
144 return tempFixturePath
145}
146
147async function getFileSize (path: string) {
148 const stats = await stat(path)
149
150 return stats.size
151}
152
153// ---------------------------------------------------------------------------
154
155export {
156 dateIsValid,
157 wait,
158 areHttpImportTestsDisabled,
159 buildServerDirectory,
160 webtorrentAdd,
161 getFileSize,
162 immutableAssign,
163 testImage,
164 isGithubCI,
165 buildAbsoluteFixturePath,
166 testFileExistsOrNot,
167 root,
168 generateHighBitrateVideo,
169 generateVideoWithFramerate
170}
diff --git a/shared/extra-utils/miscs/sql-command.ts b/shared/extra-utils/miscs/sql-command.ts
new file mode 100644
index 000000000..80c8cd271
--- /dev/null
+++ b/shared/extra-utils/miscs/sql-command.ts
@@ -0,0 +1,142 @@
1import { QueryTypes, Sequelize } from 'sequelize'
2import { AbstractCommand } from '../shared/abstract-command'
3
4export class SQLCommand extends AbstractCommand {
5 private sequelize: Sequelize
6
7 deleteAll (table: string) {
8 const seq = this.getSequelize()
9
10 const options = { type: QueryTypes.DELETE }
11
12 return seq.query(`DELETE FROM "${table}"`, options)
13 }
14
15 async getCount (table: string) {
16 const seq = this.getSequelize()
17
18 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
19
20 const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options)
21 if (total === null) return 0
22
23 return parseInt(total, 10)
24 }
25
26 setActorField (to: string, field: string, value: string) {
27 const seq = this.getSequelize()
28
29 const options = { type: QueryTypes.UPDATE }
30
31 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
32 }
33
34 setVideoField (uuid: string, field: string, value: string) {
35 const seq = this.getSequelize()
36
37 const options = { type: QueryTypes.UPDATE }
38
39 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
40 }
41
42 setPlaylistField (uuid: string, field: string, value: string) {
43 const seq = this.getSequelize()
44
45 const options = { type: QueryTypes.UPDATE }
46
47 return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
48 }
49
50 async countVideoViewsOf (uuid: string) {
51 const seq = this.getSequelize()
52
53 // tslint:disable
54 const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
55 `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
56
57 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
58 const [ { total } ] = await seq.query<{ total: number }>(query, options)
59
60 if (!total) return 0
61
62 return parseInt(total + '', 10)
63 }
64
65 getActorImage (filename: string) {
66 return this.selectQuery(`SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
67 .then(rows => rows[0])
68 }
69
70 selectQuery (query: string) {
71 const seq = this.getSequelize()
72 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
73
74 return seq.query<any>(query, options)
75 }
76
77 updateQuery (query: string) {
78 const seq = this.getSequelize()
79 const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE }
80
81 return seq.query(query, options)
82 }
83
84 setPluginField (pluginName: string, field: string, value: string) {
85 const seq = this.getSequelize()
86
87 const options = { type: QueryTypes.UPDATE }
88
89 return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
90 }
91
92 setPluginVersion (pluginName: string, newVersion: string) {
93 return this.setPluginField(pluginName, 'version', newVersion)
94 }
95
96 setPluginLatestVersion (pluginName: string, newVersion: string) {
97 return this.setPluginField(pluginName, 'latestVersion', newVersion)
98 }
99
100 setActorFollowScores (newScore: number) {
101 const seq = this.getSequelize()
102
103 const options = { type: QueryTypes.UPDATE }
104
105 return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
106 }
107
108 setTokenField (accessToken: string, field: string, value: string) {
109 const seq = this.getSequelize()
110
111 const options = { type: QueryTypes.UPDATE }
112
113 return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
114 }
115
116 async cleanup () {
117 if (!this.sequelize) return
118
119 await this.sequelize.close()
120 this.sequelize = undefined
121 }
122
123 private getSequelize () {
124 if (this.sequelize) return this.sequelize
125
126 const dbname = 'peertube_test' + this.server.internalServerNumber
127 const username = 'peertube'
128 const password = 'peertube'
129 const host = 'localhost'
130 const port = 5432
131
132 this.sequelize = new Sequelize(dbname, username, password, {
133 dialect: 'postgres',
134 host,
135 port,
136 logging: false
137 })
138
139 return this.sequelize
140 }
141
142}
diff --git a/shared/extra-utils/miscs/sql.ts b/shared/extra-utils/miscs/sql.ts
deleted file mode 100644
index 65a0aa5fe..000000000
--- a/shared/extra-utils/miscs/sql.ts
+++ /dev/null
@@ -1,161 +0,0 @@
1import { QueryTypes, Sequelize } from 'sequelize'
2import { ServerInfo } from '../server/servers'
3
4const sequelizes: { [ id: number ]: Sequelize } = {}
5
6function getSequelize (internalServerNumber: number) {
7 if (sequelizes[internalServerNumber]) return sequelizes[internalServerNumber]
8
9 const dbname = 'peertube_test' + internalServerNumber
10 const username = 'peertube'
11 const password = 'peertube'
12 const host = 'localhost'
13 const port = 5432
14
15 const seq = new Sequelize(dbname, username, password, {
16 dialect: 'postgres',
17 host,
18 port,
19 logging: false
20 })
21
22 sequelizes[internalServerNumber] = seq
23
24 return seq
25}
26
27function deleteAll (internalServerNumber: number, table: string) {
28 const seq = getSequelize(internalServerNumber)
29
30 const options = { type: QueryTypes.DELETE }
31
32 return seq.query(`DELETE FROM "${table}"`, options)
33}
34
35async function getCount (internalServerNumber: number, table: string) {
36 const seq = getSequelize(internalServerNumber)
37
38 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
39
40 const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options)
41 if (total === null) return 0
42
43 return parseInt(total, 10)
44}
45
46function setActorField (internalServerNumber: number, to: string, field: string, value: string) {
47 const seq = getSequelize(internalServerNumber)
48
49 const options = { type: QueryTypes.UPDATE }
50
51 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
52}
53
54function setVideoField (internalServerNumber: number, uuid: string, field: string, value: string) {
55 const seq = getSequelize(internalServerNumber)
56
57 const options = { type: QueryTypes.UPDATE }
58
59 return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
60}
61
62function setPlaylistField (internalServerNumber: number, uuid: string, field: string, value: string) {
63 const seq = getSequelize(internalServerNumber)
64
65 const options = { type: QueryTypes.UPDATE }
66
67 return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options)
68}
69
70async function countVideoViewsOf (internalServerNumber: number, uuid: string) {
71 const seq = getSequelize(internalServerNumber)
72
73 // tslint:disable
74 const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
75 `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
76
77 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
78 const [ { total } ] = await seq.query<{ total: number }>(query, options)
79
80 if (!total) return 0
81
82 return parseInt(total + '', 10)
83}
84
85function getActorImage (internalServerNumber: number, filename: string) {
86 return selectQuery(internalServerNumber, `SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
87 .then(rows => rows[0])
88}
89
90function selectQuery (internalServerNumber: number, query: string) {
91 const seq = getSequelize(internalServerNumber)
92 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
93
94 return seq.query<any>(query, options)
95}
96
97function updateQuery (internalServerNumber: number, query: string) {
98 const seq = getSequelize(internalServerNumber)
99 const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE }
100
101 return seq.query(query, options)
102}
103
104async function closeAllSequelize (servers: ServerInfo[]) {
105 for (const server of servers) {
106 if (sequelizes[server.internalServerNumber]) {
107 await sequelizes[server.internalServerNumber].close()
108 // eslint-disable-next-line
109 delete sequelizes[server.internalServerNumber]
110 }
111 }
112}
113
114function setPluginField (internalServerNumber: number, pluginName: string, field: string, value: string) {
115 const seq = getSequelize(internalServerNumber)
116
117 const options = { type: QueryTypes.UPDATE }
118
119 return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
120}
121
122function setPluginVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
123 return setPluginField(internalServerNumber, pluginName, 'version', newVersion)
124}
125
126function setPluginLatestVersion (internalServerNumber: number, pluginName: string, newVersion: string) {
127 return setPluginField(internalServerNumber, pluginName, 'latestVersion', newVersion)
128}
129
130function setActorFollowScores (internalServerNumber: number, newScore: number) {
131 const seq = getSequelize(internalServerNumber)
132
133 const options = { type: QueryTypes.UPDATE }
134
135 return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
136}
137
138function setTokenField (internalServerNumber: number, accessToken: string, field: string, value: string) {
139 const seq = getSequelize(internalServerNumber)
140
141 const options = { type: QueryTypes.UPDATE }
142
143 return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
144}
145
146export {
147 setVideoField,
148 setPlaylistField,
149 setActorField,
150 countVideoViewsOf,
151 setPluginVersion,
152 setPluginLatestVersion,
153 selectQuery,
154 getActorImage,
155 deleteAll,
156 setTokenField,
157 updateQuery,
158 setActorFollowScores,
159 closeAllSequelize,
160 getCount
161}
diff --git a/shared/extra-utils/miscs/stubs.ts b/shared/extra-utils/miscs/stubs.ts
deleted file mode 100644
index d1eb0e3b2..000000000
--- a/shared/extra-utils/miscs/stubs.ts
+++ /dev/null
@@ -1,14 +0,0 @@
1function buildRequestStub (): any {
2 return { }
3}
4
5function buildResponseStub (): any {
6 return {
7 locals: {}
8 }
9}
10
11export {
12 buildResponseStub,
13 buildRequestStub
14}
diff --git a/shared/extra-utils/miscs/tests.ts b/shared/extra-utils/miscs/tests.ts
new file mode 100644
index 000000000..3dfb2487e
--- /dev/null
+++ b/shared/extra-utils/miscs/tests.ts
@@ -0,0 +1,94 @@
1import { stat } from 'fs-extra'
2import { basename, isAbsolute, join, resolve } from 'path'
3
4const FIXTURE_URLS = {
5 youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM',
6
7 /**
8 * The video is used to check format-selection correctness wrt. HDR,
9 * which brings its own set of oddities outside of a MediaSource.
10 * FIXME: refactor once HDR is supported at playback
11 *
12 * The video needs to have the following format_ids:
13 * (which you can check by using `youtube-dl <url> -F`):
14 * - 303 (1080p webm vp9)
15 * - 299 (1080p mp4 avc1)
16 * - 335 (1080p webm vp9.2 HDR)
17 *
18 * 15 jan. 2021: TEST VIDEO NOT CURRENTLY PROVIDING
19 * - 400 (1080p mp4 av01)
20 * - 315 (2160p webm vp9 HDR)
21 * - 337 (2160p webm vp9.2 HDR)
22 * - 401 (2160p mp4 av01 HDR)
23 */
24 youtubeHDR: 'https://www.youtube.com/watch?v=qR5vOXbZsI4',
25
26 // eslint-disable-next-line max-len
27 magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4',
28
29 badVideo: 'https://download.cpy.re/peertube/bad_video.mp4',
30 goodVideo: 'https://download.cpy.re/peertube/good_video.mp4',
31 video4K: 'https://download.cpy.re/peertube/4k_file.txt'
32}
33
34function parallelTests () {
35 return process.env.MOCHA_PARALLEL === 'true'
36}
37
38function isGithubCI () {
39 return !!process.env.GITHUB_WORKSPACE
40}
41
42function areHttpImportTestsDisabled () {
43 const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true'
44
45 if (disabled) console.log('Import tests are disabled')
46
47 return disabled
48}
49
50function buildAbsoluteFixturePath (path: string, customCIPath = false) {
51 if (isAbsolute(path)) return path
52
53 if (customCIPath && process.env.GITHUB_WORKSPACE) {
54 return join(process.env.GITHUB_WORKSPACE, 'fixtures', path)
55 }
56
57 return join(root(), 'server', 'tests', 'fixtures', path)
58}
59
60function root () {
61 // We are in /miscs
62 let root = join(__dirname, '..', '..', '..')
63
64 if (basename(root) === 'dist') root = resolve(root, '..')
65
66 return root
67}
68
69function wait (milliseconds: number) {
70 return new Promise(resolve => setTimeout(resolve, milliseconds))
71}
72
73async function getFileSize (path: string) {
74 const stats = await stat(path)
75
76 return stats.size
77}
78
79function buildRequestStub (): any {
80 return { }
81}
82
83export {
84 FIXTURE_URLS,
85
86 parallelTests,
87 isGithubCI,
88 areHttpImportTestsDisabled,
89 buildAbsoluteFixturePath,
90 getFileSize,
91 buildRequestStub,
92 wait,
93 root
94}
diff --git a/shared/extra-utils/miscs/webtorrent.ts b/shared/extra-utils/miscs/webtorrent.ts
new file mode 100644
index 000000000..a1097effe
--- /dev/null
+++ b/shared/extra-utils/miscs/webtorrent.ts
@@ -0,0 +1,31 @@
1import { readFile } from 'fs-extra'
2import * as parseTorrent from 'parse-torrent'
3import { basename, join } from 'path'
4import * as WebTorrent from 'webtorrent'
5import { VideoFile } from '@shared/models'
6import { PeerTubeServer } from '../server'
7
8let webtorrent: WebTorrent.Instance
9
10function webtorrentAdd (torrent: string, refreshWebTorrent = false) {
11 const WebTorrent = require('webtorrent')
12
13 if (!webtorrent) webtorrent = new WebTorrent()
14 if (refreshWebTorrent === true) webtorrent = new WebTorrent()
15
16 return new Promise<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))
17}
18
19async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) {
20 const torrentName = basename(file.torrentUrl)
21 const torrentPath = server.servers.buildDirectory(join('torrents', torrentName))
22
23 const data = await readFile(torrentPath)
24
25 return parseTorrent(data)
26}
27
28export {
29 webtorrentAdd,
30 parseTorrentVideo
31}
diff --git a/shared/extra-utils/mock-servers/index.ts b/shared/extra-utils/mock-servers/index.ts
new file mode 100644
index 000000000..0ec07f685
--- /dev/null
+++ b/shared/extra-utils/mock-servers/index.ts
@@ -0,0 +1,4 @@
1export * from './mock-email'
2export * from './mock-instances-index'
3export * from './mock-joinpeertube-versions'
4export * from './mock-plugin-blocklist'
diff --git a/shared/extra-utils/miscs/email.ts b/shared/extra-utils/mock-servers/mock-email.ts
index 9fc9a5ad0..ffd62e325 100644
--- a/shared/extra-utils/miscs/email.ts
+++ b/shared/extra-utils/mock-servers/mock-email.ts
@@ -1,6 +1,6 @@
1import { ChildProcess } from 'child_process' 1import { ChildProcess } from 'child_process'
2import { randomInt } from '../../core-utils/miscs/miscs' 2import { randomInt } from '@shared/core-utils'
3import { parallelTests } from '../server/servers' 3import { parallelTests } from '../miscs'
4 4
5const MailDev = require('maildev') 5const MailDev = require('maildev')
6 6
diff --git a/shared/extra-utils/mock-servers/joinpeertube-versions.ts b/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts
index 5ea432ecf..5ea432ecf 100644
--- a/shared/extra-utils/mock-servers/joinpeertube-versions.ts
+++ b/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts
diff --git a/shared/extra-utils/plugins/mock-blocklist.ts b/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts
index d18f8224f..d18f8224f 100644
--- a/shared/extra-utils/plugins/mock-blocklist.ts
+++ b/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts
diff --git a/shared/extra-utils/moderation/abuses-command.ts b/shared/extra-utils/moderation/abuses-command.ts
new file mode 100644
index 000000000..0db32ba46
--- /dev/null
+++ b/shared/extra-utils/moderation/abuses-command.ts
@@ -0,0 +1,228 @@
1import { pick } from '@shared/core-utils'
2import {
3 AbuseFilter,
4 AbuseMessage,
5 AbusePredefinedReasonsString,
6 AbuseState,
7 AbuseUpdate,
8 AbuseVideoIs,
9 AdminAbuse,
10 HttpStatusCode,
11 ResultList,
12 UserAbuse
13} from '@shared/models'
14import { unwrapBody } from '../requests/requests'
15import { AbstractCommand, OverrideCommandOptions } from '../shared'
16
17export class AbusesCommand extends AbstractCommand {
18
19 report (options: OverrideCommandOptions & {
20 reason: string
21
22 accountId?: number
23 videoId?: number
24 commentId?: number
25
26 predefinedReasons?: AbusePredefinedReasonsString[]
27
28 startAt?: number
29 endAt?: number
30 }) {
31 const path = '/api/v1/abuses'
32
33 const video = options.videoId
34 ? {
35 id: options.videoId,
36 startAt: options.startAt,
37 endAt: options.endAt
38 }
39 : undefined
40
41 const comment = options.commentId
42 ? { id: options.commentId }
43 : undefined
44
45 const account = options.accountId
46 ? { id: options.accountId }
47 : undefined
48
49 const body = {
50 account,
51 video,
52 comment,
53
54 reason: options.reason,
55 predefinedReasons: options.predefinedReasons
56 }
57
58 return unwrapBody<{ abuse: { id: number } }>(this.postBodyRequest({
59 ...options,
60
61 path,
62 fields: body,
63 implicitToken: true,
64 defaultExpectedStatus: HttpStatusCode.OK_200
65 }))
66 }
67
68 getAdminList (options: OverrideCommandOptions & {
69 start?: number
70 count?: number
71 sort?: string
72
73 id?: number
74 predefinedReason?: AbusePredefinedReasonsString
75 search?: string
76 filter?: AbuseFilter
77 state?: AbuseState
78 videoIs?: AbuseVideoIs
79 searchReporter?: string
80 searchReportee?: string
81 searchVideo?: string
82 searchVideoChannel?: string
83 } = {}) {
84 const toPick: (keyof typeof options)[] = [
85 'count',
86 'filter',
87 'id',
88 'predefinedReason',
89 'search',
90 'searchReportee',
91 'searchReporter',
92 'searchVideo',
93 'searchVideoChannel',
94 'sort',
95 'start',
96 'state',
97 'videoIs'
98 ]
99
100 const path = '/api/v1/abuses'
101
102 const defaultQuery = { sort: 'createdAt' }
103 const query = { ...defaultQuery, ...pick(options, toPick) }
104
105 return this.getRequestBody<ResultList<AdminAbuse>>({
106 ...options,
107
108 path,
109 query,
110 implicitToken: true,
111 defaultExpectedStatus: HttpStatusCode.OK_200
112 })
113 }
114
115 getUserList (options: OverrideCommandOptions & {
116 start?: number
117 count?: number
118 sort?: string
119
120 id?: number
121 search?: string
122 state?: AbuseState
123 }) {
124 const toPick: (keyof typeof options)[] = [
125 'id',
126 'search',
127 'state',
128 'start',
129 'count',
130 'sort'
131 ]
132
133 const path = '/api/v1/users/me/abuses'
134
135 const defaultQuery = { sort: 'createdAt' }
136 const query = { ...defaultQuery, ...pick(options, toPick) }
137
138 return this.getRequestBody<ResultList<UserAbuse>>({
139 ...options,
140
141 path,
142 query,
143 implicitToken: true,
144 defaultExpectedStatus: HttpStatusCode.OK_200
145 })
146 }
147
148 update (options: OverrideCommandOptions & {
149 abuseId: number
150 body: AbuseUpdate
151 }) {
152 const { abuseId, body } = options
153 const path = '/api/v1/abuses/' + abuseId
154
155 return this.putBodyRequest({
156 ...options,
157
158 path,
159 fields: body,
160 implicitToken: true,
161 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
162 })
163 }
164
165 delete (options: OverrideCommandOptions & {
166 abuseId: number
167 }) {
168 const { abuseId } = options
169 const path = '/api/v1/abuses/' + abuseId
170
171 return this.deleteRequest({
172 ...options,
173
174 path,
175 implicitToken: true,
176 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
177 })
178 }
179
180 listMessages (options: OverrideCommandOptions & {
181 abuseId: number
182 }) {
183 const { abuseId } = options
184 const path = '/api/v1/abuses/' + abuseId + '/messages'
185
186 return this.getRequestBody<ResultList<AbuseMessage>>({
187 ...options,
188
189 path,
190 implicitToken: true,
191 defaultExpectedStatus: HttpStatusCode.OK_200
192 })
193 }
194
195 deleteMessage (options: OverrideCommandOptions & {
196 abuseId: number
197 messageId: number
198 }) {
199 const { abuseId, messageId } = options
200 const path = '/api/v1/abuses/' + abuseId + '/messages/' + messageId
201
202 return this.deleteRequest({
203 ...options,
204
205 path,
206 implicitToken: true,
207 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
208 })
209 }
210
211 addMessage (options: OverrideCommandOptions & {
212 abuseId: number
213 message: string
214 }) {
215 const { abuseId, message } = options
216 const path = '/api/v1/abuses/' + abuseId + '/messages'
217
218 return this.postBodyRequest({
219 ...options,
220
221 path,
222 fields: { message },
223 implicitToken: true,
224 defaultExpectedStatus: HttpStatusCode.OK_200
225 })
226 }
227
228}
diff --git a/shared/extra-utils/moderation/abuses.ts b/shared/extra-utils/moderation/abuses.ts
deleted file mode 100644
index c0fda722f..000000000
--- a/shared/extra-utils/moderation/abuses.ts
+++ /dev/null
@@ -1,244 +0,0 @@
1import { AbuseFilter, AbusePredefinedReasonsString, AbuseState, AbuseUpdate, AbuseVideoIs } from '@shared/models'
2import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function reportAbuse (options: {
6 url: string
7 token: string
8
9 reason: string
10
11 accountId?: number
12 videoId?: number
13 commentId?: number
14
15 predefinedReasons?: AbusePredefinedReasonsString[]
16
17 startAt?: number
18 endAt?: number
19
20 statusCodeExpected?: number
21}) {
22 const path = '/api/v1/abuses'
23
24 const video = options.videoId
25 ? {
26 id: options.videoId,
27 startAt: options.startAt,
28 endAt: options.endAt
29 }
30 : undefined
31
32 const comment = options.commentId
33 ? { id: options.commentId }
34 : undefined
35
36 const account = options.accountId
37 ? { id: options.accountId }
38 : undefined
39
40 const body = {
41 account,
42 video,
43 comment,
44
45 reason: options.reason,
46 predefinedReasons: options.predefinedReasons
47 }
48
49 return makePostBodyRequest({
50 url: options.url,
51 path,
52 token: options.token,
53
54 fields: body,
55 statusCodeExpected: options.statusCodeExpected || HttpStatusCode.OK_200
56 })
57}
58
59function getAdminAbusesList (options: {
60 url: string
61 token: string
62
63 start?: number
64 count?: number
65 sort?: string
66
67 id?: number
68 predefinedReason?: AbusePredefinedReasonsString
69 search?: string
70 filter?: AbuseFilter
71 state?: AbuseState
72 videoIs?: AbuseVideoIs
73 searchReporter?: string
74 searchReportee?: string
75 searchVideo?: string
76 searchVideoChannel?: string
77}) {
78 const {
79 url,
80 token,
81 start,
82 count,
83 sort,
84 id,
85 predefinedReason,
86 search,
87 filter,
88 state,
89 videoIs,
90 searchReporter,
91 searchReportee,
92 searchVideo,
93 searchVideoChannel
94 } = options
95 const path = '/api/v1/abuses'
96
97 const query = {
98 id,
99 predefinedReason,
100 search,
101 state,
102 filter,
103 videoIs,
104 start,
105 count,
106 sort: sort || 'createdAt',
107 searchReporter,
108 searchReportee,
109 searchVideo,
110 searchVideoChannel
111 }
112
113 return makeGetRequest({
114 url,
115 path,
116 token,
117 query,
118 statusCodeExpected: HttpStatusCode.OK_200
119 })
120}
121
122function getUserAbusesList (options: {
123 url: string
124 token: string
125
126 start?: number
127 count?: number
128 sort?: string
129
130 id?: number
131 search?: string
132 state?: AbuseState
133}) {
134 const {
135 url,
136 token,
137 start,
138 count,
139 sort,
140 id,
141 search,
142 state
143 } = options
144 const path = '/api/v1/users/me/abuses'
145
146 const query = {
147 id,
148 search,
149 state,
150 start,
151 count,
152 sort: sort || 'createdAt'
153 }
154
155 return makeGetRequest({
156 url,
157 path,
158 token,
159 query,
160 statusCodeExpected: HttpStatusCode.OK_200
161 })
162}
163
164function updateAbuse (
165 url: string,
166 token: string,
167 abuseId: number,
168 body: AbuseUpdate,
169 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
170) {
171 const path = '/api/v1/abuses/' + abuseId
172
173 return makePutBodyRequest({
174 url,
175 token,
176 path,
177 fields: body,
178 statusCodeExpected
179 })
180}
181
182function deleteAbuse (url: string, token: string, abuseId: number, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
183 const path = '/api/v1/abuses/' + abuseId
184
185 return makeDeleteRequest({
186 url,
187 token,
188 path,
189 statusCodeExpected
190 })
191}
192
193function listAbuseMessages (url: string, token: string, abuseId: number, statusCodeExpected = HttpStatusCode.OK_200) {
194 const path = '/api/v1/abuses/' + abuseId + '/messages'
195
196 return makeGetRequest({
197 url,
198 token,
199 path,
200 statusCodeExpected
201 })
202}
203
204function deleteAbuseMessage (
205 url: string,
206 token: string,
207 abuseId: number,
208 messageId: number,
209 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
210) {
211 const path = '/api/v1/abuses/' + abuseId + '/messages/' + messageId
212
213 return makeDeleteRequest({
214 url,
215 token,
216 path,
217 statusCodeExpected
218 })
219}
220
221function addAbuseMessage (url: string, token: string, abuseId: number, message: string, statusCodeExpected = HttpStatusCode.OK_200) {
222 const path = '/api/v1/abuses/' + abuseId + '/messages'
223
224 return makePostBodyRequest({
225 url,
226 token,
227 path,
228 fields: { message },
229 statusCodeExpected
230 })
231}
232
233// ---------------------------------------------------------------------------
234
235export {
236 reportAbuse,
237 getAdminAbusesList,
238 updateAbuse,
239 deleteAbuse,
240 getUserAbusesList,
241 listAbuseMessages,
242 deleteAbuseMessage,
243 addAbuseMessage
244}
diff --git a/shared/extra-utils/moderation/index.ts b/shared/extra-utils/moderation/index.ts
new file mode 100644
index 000000000..b37643956
--- /dev/null
+++ b/shared/extra-utils/moderation/index.ts
@@ -0,0 +1 @@
export * from './abuses-command'
diff --git a/shared/extra-utils/overviews/index.ts b/shared/extra-utils/overviews/index.ts
new file mode 100644
index 000000000..e19551907
--- /dev/null
+++ b/shared/extra-utils/overviews/index.ts
@@ -0,0 +1 @@
export * from './overviews-command'
diff --git a/shared/extra-utils/overviews/overviews-command.ts b/shared/extra-utils/overviews/overviews-command.ts
new file mode 100644
index 000000000..06b4892d2
--- /dev/null
+++ b/shared/extra-utils/overviews/overviews-command.ts
@@ -0,0 +1,23 @@
1import { HttpStatusCode, VideosOverview } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class OverviewsCommand extends AbstractCommand {
5
6 getVideos (options: OverrideCommandOptions & {
7 page: number
8 }) {
9 const { page } = options
10 const path = '/api/v1/overviews/videos'
11
12 const query = { page }
13
14 return this.getRequestBody<VideosOverview>({
15 ...options,
16
17 path,
18 query,
19 implicitToken: false,
20 defaultExpectedStatus: HttpStatusCode.OK_200
21 })
22 }
23}
diff --git a/shared/extra-utils/overviews/overviews.ts b/shared/extra-utils/overviews/overviews.ts
deleted file mode 100644
index 5e1a13e5e..000000000
--- a/shared/extra-utils/overviews/overviews.ts
+++ /dev/null
@@ -1,34 +0,0 @@
1import { makeGetRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function getVideosOverview (url: string, page: number, statusCodeExpected = HttpStatusCode.OK_200) {
5 const path = '/api/v1/overviews/videos'
6
7 const query = { page }
8
9 return makeGetRequest({
10 url,
11 path,
12 query,
13 statusCodeExpected
14 })
15}
16
17function getVideosOverviewWithToken (url: string, page: number, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
18 const path = '/api/v1/overviews/videos'
19
20 const query = { page }
21
22 return makeGetRequest({
23 url,
24 path,
25 query,
26 token,
27 statusCodeExpected
28 })
29}
30
31export {
32 getVideosOverview,
33 getVideosOverviewWithToken
34}
diff --git a/shared/extra-utils/requests/activitypub.ts b/shared/extra-utils/requests/activitypub.ts
index ecd8ce823..4ae878384 100644
--- a/shared/extra-utils/requests/activitypub.ts
+++ b/shared/extra-utils/requests/activitypub.ts
@@ -1,7 +1,7 @@
1import { activityPubContextify } from '../../../server/helpers/activitypub'
1import { doRequest } from '../../../server/helpers/requests' 2import { doRequest } from '../../../server/helpers/requests'
2import { HTTP_SIGNATURE } from '../../../server/initializers/constants' 3import { HTTP_SIGNATURE } from '../../../server/initializers/constants'
3import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils' 4import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../server/helpers/activitypub'
5 5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) { 6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = { 7 const options = {
diff --git a/shared/extra-utils/requests/check-api-params.ts b/shared/extra-utils/requests/check-api-params.ts
index 7f5ff775c..26ba1e913 100644
--- a/shared/extra-utils/requests/check-api-params.ts
+++ b/shared/extra-utils/requests/check-api-params.ts
@@ -1,14 +1,13 @@
1import { HttpStatusCode } from '@shared/models'
1import { makeGetRequest } from './requests' 2import { makeGetRequest } from './requests'
2import { immutableAssign } from '../miscs/miscs'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4 3
5function checkBadStartPagination (url: string, path: string, token?: string, query = {}) { 4function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
6 return makeGetRequest({ 5 return makeGetRequest({
7 url, 6 url,
8 path, 7 path,
9 token, 8 token,
10 query: immutableAssign(query, { start: 'hello' }), 9 query: { ...query, start: 'hello' },
11 statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 10 expectedStatus: HttpStatusCode.BAD_REQUEST_400
12 }) 11 })
13} 12}
14 13
@@ -17,16 +16,16 @@ async function checkBadCountPagination (url: string, path: string, token?: strin
17 url, 16 url,
18 path, 17 path,
19 token, 18 token,
20 query: immutableAssign(query, { count: 'hello' }), 19 query: { ...query, count: 'hello' },
21 statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 20 expectedStatus: HttpStatusCode.BAD_REQUEST_400
22 }) 21 })
23 22
24 await makeGetRequest({ 23 await makeGetRequest({
25 url, 24 url,
26 path, 25 path,
27 token, 26 token,
28 query: immutableAssign(query, { count: 2000 }), 27 query: { ...query, count: 2000 },
29 statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 28 expectedStatus: HttpStatusCode.BAD_REQUEST_400
30 }) 29 })
31} 30}
32 31
@@ -35,8 +34,8 @@ function checkBadSortPagination (url: string, path: string, token?: string, quer
35 url, 34 url,
36 path, 35 path,
37 token, 36 token,
38 query: immutableAssign(query, { sort: 'hello' }), 37 query: { ...query, sort: 'hello' },
39 statusCodeExpected: HttpStatusCode.BAD_REQUEST_400 38 expectedStatus: HttpStatusCode.BAD_REQUEST_400
40 }) 39 })
41} 40}
42 41
diff --git a/shared/extra-utils/requests/index.ts b/shared/extra-utils/requests/index.ts
new file mode 100644
index 000000000..501163f92
--- /dev/null
+++ b/shared/extra-utils/requests/index.ts
@@ -0,0 +1,3 @@
1// Don't include activitypub that import stuff from server
2export * from './check-api-params'
3export * from './requests'
diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts
index 38e24d897..70f790222 100644
--- a/shared/extra-utils/requests/requests.ts
+++ b/shared/extra-utils/requests/requests.ts
@@ -1,103 +1,82 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1/* eslint-disable @typescript-eslint/no-floating-promises */
2 2
3import { decode } from 'querystring'
3import * as request from 'supertest' 4import * as request from 'supertest'
4import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
5import { isAbsolute, join } from 'path'
6import { URL } from 'url' 5import { URL } from 'url'
7import { decode } from 'querystring' 6import { HttpStatusCode } from '@shared/models'
8import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 7import { buildAbsoluteFixturePath } from '../miscs/tests'
9 8
10function get4KFileUrl () { 9export type CommonRequestParams = {
11 return 'https://download.cpy.re/peertube/4k_file.txt' 10 url: string
11 path?: string
12 contentType?: string
13 range?: string
14 redirects?: number
15 accept?: string
16 host?: string
17 token?: string
18 headers?: { [ name: string ]: string }
19 type?: string
20 xForwardedFor?: string
21 expectedStatus?: HttpStatusCode
12} 22}
13 23
14function makeRawRequest (url: string, statusCodeExpected?: HttpStatusCode, range?: string) { 24function makeRawRequest (url: string, expectedStatus?: HttpStatusCode, range?: string) {
15 const { host, protocol, pathname } = new URL(url) 25 const { host, protocol, pathname } = new URL(url)
16 26
17 return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range }) 27 return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, expectedStatus, range })
18} 28}
19 29
20function makeGetRequest (options: { 30function makeGetRequest (options: CommonRequestParams & {
21 url: string
22 path?: string
23 query?: any 31 query?: any
24 token?: string
25 statusCodeExpected?: HttpStatusCode
26 contentType?: string
27 range?: string
28 redirects?: number
29 accept?: string
30}) { 32}) {
31 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400
32 if (options.contentType === undefined) options.contentType = 'application/json'
33
34 const req = request(options.url).get(options.path) 33 const req = request(options.url).get(options.path)
34 .query(options.query)
35 35
36 if (options.contentType) req.set('Accept', options.contentType) 36 return buildRequest(req, { contentType: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
37 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
38 if (options.query) req.query(options.query)
39 if (options.range) req.set('Range', options.range)
40 if (options.accept) req.set('Accept', options.accept)
41 if (options.redirects) req.redirects(options.redirects)
42
43 return req.expect(options.statusCodeExpected)
44} 37}
45 38
46function makeDeleteRequest (options: { 39function makeHTMLRequest (url: string, path: string) {
47 url: string 40 return makeGetRequest({
48 path: string 41 url,
49 token?: string 42 path,
50 statusCodeExpected?: HttpStatusCode 43 accept: 'text/html',
51}) { 44 expectedStatus: HttpStatusCode.OK_200
52 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 45 })
46}
53 47
54 const req = request(options.url) 48function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
55 .delete(options.path) 49 return makeGetRequest({
56 .set('Accept', 'application/json') 50 url,
51 path,
52 expectedStatus: expectedStatus,
53 accept: 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8'
54 })
55}
57 56
58 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 57function makeDeleteRequest (options: CommonRequestParams) {
58 const req = request(options.url).delete(options.path)
59 59
60 return req.expect(options.statusCodeExpected) 60 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
61} 61}
62 62
63function makeUploadRequest (options: { 63function makeUploadRequest (options: CommonRequestParams & {
64 url: string
65 method?: 'POST' | 'PUT' 64 method?: 'POST' | 'PUT'
66 path: string 65
67 token?: string
68 fields: { [ fieldName: string ]: any } 66 fields: { [ fieldName: string ]: any }
69 attaches?: { [ attachName: string ]: any | any[] } 67 attaches?: { [ attachName: string ]: any | any[] }
70 statusCodeExpected?: HttpStatusCode
71}) { 68}) {
72 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 69 let req = options.method === 'PUT'
73 70 ? request(options.url).put(options.path)
74 let req: request.Test 71 : request(options.url).post(options.path)
75 if (options.method === 'PUT') {
76 req = request(options.url).put(options.path)
77 } else {
78 req = request(options.url).post(options.path)
79 }
80
81 req.set('Accept', 'application/json')
82 72
83 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 73 req = buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
84 74
85 Object.keys(options.fields).forEach(field => { 75 buildFields(req, options.fields)
86 const value = options.fields[field]
87
88 if (value === undefined) return
89
90 if (Array.isArray(value)) {
91 for (let i = 0; i < value.length; i++) {
92 req.field(field + '[' + i + ']', value[i])
93 }
94 } else {
95 req.field(field, value)
96 }
97 })
98 76
99 Object.keys(options.attaches || {}).forEach(attach => { 77 Object.keys(options.attaches || {}).forEach(attach => {
100 const value = options.attaches[attach] 78 const value = options.attaches[attach]
79
101 if (Array.isArray(value)) { 80 if (Array.isArray(value)) {
102 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1]) 81 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
103 } else { 82 } else {
@@ -105,27 +84,16 @@ function makeUploadRequest (options: {
105 } 84 }
106 }) 85 })
107 86
108 return req.expect(options.statusCodeExpected) 87 return req
109} 88}
110 89
111function makePostBodyRequest (options: { 90function makePostBodyRequest (options: CommonRequestParams & {
112 url: string
113 path: string
114 token?: string
115 fields?: { [ fieldName: string ]: any } 91 fields?: { [ fieldName: string ]: any }
116 statusCodeExpected?: HttpStatusCode
117}) { 92}) {
118 if (!options.fields) options.fields = {} 93 const req = request(options.url).post(options.path)
119 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 94 .send(options.fields)
120
121 const req = request(options.url)
122 .post(options.path)
123 .set('Accept', 'application/json')
124 95
125 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 96 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
126
127 return req.send(options.fields)
128 .expect(options.statusCodeExpected)
129} 97}
130 98
131function makePutBodyRequest (options: { 99function makePutBodyRequest (options: {
@@ -133,59 +101,29 @@ function makePutBodyRequest (options: {
133 path: string 101 path: string
134 token?: string 102 token?: string
135 fields: { [ fieldName: string ]: any } 103 fields: { [ fieldName: string ]: any }
136 statusCodeExpected?: HttpStatusCode 104 expectedStatus?: HttpStatusCode
137}) { 105}) {
138 if (!options.statusCodeExpected) options.statusCodeExpected = HttpStatusCode.BAD_REQUEST_400 106 const req = request(options.url).put(options.path)
139 107 .send(options.fields)
140 const req = request(options.url)
141 .put(options.path)
142 .set('Accept', 'application/json')
143 108
144 if (options.token) req.set('Authorization', 'Bearer ' + options.token) 109 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
145
146 return req.send(options.fields)
147 .expect(options.statusCodeExpected)
148} 110}
149 111
150function makeHTMLRequest (url: string, path: string) { 112function decodeQueryString (path: string) {
151 return request(url) 113 return decode(path.split('?')[1])
152 .get(path)
153 .set('Accept', 'text/html')
154 .expect(HttpStatusCode.OK_200)
155} 114}
156 115
157function updateImageRequest (options: { 116function unwrapBody <T> (test: request.Test): Promise<T> {
158 url: string 117 return test.then(res => res.body)
159 path: string
160 accessToken: string
161 fixture: string
162 fieldname: string
163}) {
164 let filePath = ''
165 if (isAbsolute(options.fixture)) {
166 filePath = options.fixture
167 } else {
168 filePath = join(root(), 'server', 'tests', 'fixtures', options.fixture)
169 }
170
171 return makeUploadRequest({
172 url: options.url,
173 path: options.path,
174 token: options.accessToken,
175 fields: {},
176 attaches: { [options.fieldname]: filePath },
177 statusCodeExpected: HttpStatusCode.OK_200
178 })
179} 118}
180 119
181function decodeQueryString (path: string) { 120function unwrapText (test: request.Test): Promise<string> {
182 return decode(path.split('?')[1]) 121 return test.then(res => res.text)
183} 122}
184 123
185// --------------------------------------------------------------------------- 124// ---------------------------------------------------------------------------
186 125
187export { 126export {
188 get4KFileUrl,
189 makeHTMLRequest, 127 makeHTMLRequest,
190 makeGetRequest, 128 makeGetRequest,
191 decodeQueryString, 129 decodeQueryString,
@@ -194,5 +132,51 @@ export {
194 makePutBodyRequest, 132 makePutBodyRequest,
195 makeDeleteRequest, 133 makeDeleteRequest,
196 makeRawRequest, 134 makeRawRequest,
197 updateImageRequest 135 makeActivityPubGetRequest,
136 unwrapBody,
137 unwrapText
138}
139
140// ---------------------------------------------------------------------------
141
142function buildRequest (req: request.Test, options: CommonRequestParams) {
143 if (options.contentType) req.set('Accept', options.contentType)
144 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
145 if (options.range) req.set('Range', options.range)
146 if (options.accept) req.set('Accept', options.accept)
147 if (options.host) req.set('Host', options.host)
148 if (options.redirects) req.redirects(options.redirects)
149 if (options.expectedStatus) req.expect(options.expectedStatus)
150 if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor)
151 if (options.type) req.type(options.type)
152
153 Object.keys(options.headers || {}).forEach(name => {
154 req.set(name, options.headers[name])
155 })
156
157 return req
158}
159
160function buildFields (req: request.Test, fields: { [ fieldName: string ]: any }, namespace?: string) {
161 if (!fields) return
162
163 let formKey: string
164
165 for (const key of Object.keys(fields)) {
166 if (namespace) formKey = `${namespace}[${key}]`
167 else formKey = key
168
169 if (fields[key] === undefined) continue
170
171 if (Array.isArray(fields[key]) && fields[key].length === 0) {
172 req.field(key, null)
173 continue
174 }
175
176 if (fields[key] !== null && typeof fields[key] === 'object') {
177 buildFields(req, fields[key], formKey)
178 } else {
179 req.field(formKey, fields[key])
180 }
181 }
198} 182}
diff --git a/shared/extra-utils/search/index.ts b/shared/extra-utils/search/index.ts
new file mode 100644
index 000000000..48dbe8ae9
--- /dev/null
+++ b/shared/extra-utils/search/index.ts
@@ -0,0 +1 @@
export * from './search-command'
diff --git a/shared/extra-utils/search/search-command.ts b/shared/extra-utils/search/search-command.ts
new file mode 100644
index 000000000..0fbbcd6ef
--- /dev/null
+++ b/shared/extra-utils/search/search-command.ts
@@ -0,0 +1,98 @@
1import {
2 HttpStatusCode,
3 ResultList,
4 Video,
5 VideoChannel,
6 VideoChannelsSearchQuery,
7 VideoPlaylist,
8 VideoPlaylistsSearchQuery,
9 VideosSearchQuery
10} from '@shared/models'
11import { AbstractCommand, OverrideCommandOptions } from '../shared'
12
13export class SearchCommand extends AbstractCommand {
14
15 searchChannels (options: OverrideCommandOptions & {
16 search: string
17 }) {
18 return this.advancedChannelSearch({
19 ...options,
20
21 search: { search: options.search }
22 })
23 }
24
25 advancedChannelSearch (options: OverrideCommandOptions & {
26 search: VideoChannelsSearchQuery
27 }) {
28 const { search } = options
29 const path = '/api/v1/search/video-channels'
30
31 return this.getRequestBody<ResultList<VideoChannel>>({
32 ...options,
33
34 path,
35 query: search,
36 implicitToken: false,
37 defaultExpectedStatus: HttpStatusCode.OK_200
38 })
39 }
40
41 searchPlaylists (options: OverrideCommandOptions & {
42 search: string
43 }) {
44 return this.advancedPlaylistSearch({
45 ...options,
46
47 search: { search: options.search }
48 })
49 }
50
51 advancedPlaylistSearch (options: OverrideCommandOptions & {
52 search: VideoPlaylistsSearchQuery
53 }) {
54 const { search } = options
55 const path = '/api/v1/search/video-playlists'
56
57 return this.getRequestBody<ResultList<VideoPlaylist>>({
58 ...options,
59
60 path,
61 query: search,
62 implicitToken: false,
63 defaultExpectedStatus: HttpStatusCode.OK_200
64 })
65 }
66
67 searchVideos (options: OverrideCommandOptions & {
68 search: string
69 sort?: string
70 }) {
71 const { search, sort } = options
72
73 return this.advancedVideoSearch({
74 ...options,
75
76 search: {
77 search: search,
78 sort: sort ?? '-publishedAt'
79 }
80 })
81 }
82
83 advancedVideoSearch (options: OverrideCommandOptions & {
84 search: VideosSearchQuery
85 }) {
86 const { search } = options
87 const path = '/api/v1/search/videos'
88
89 return this.getRequestBody<ResultList<Video>>({
90 ...options,
91
92 path,
93 query: search,
94 implicitToken: false,
95 defaultExpectedStatus: HttpStatusCode.OK_200
96 })
97 }
98}
diff --git a/shared/extra-utils/search/video-channels.ts b/shared/extra-utils/search/video-channels.ts
deleted file mode 100644
index 8e0f42578..000000000
--- a/shared/extra-utils/search/video-channels.ts
+++ /dev/null
@@ -1,36 +0,0 @@
1import { VideoChannelsSearchQuery } from '@shared/models'
2import { makeGetRequest } from '../requests/requests'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function searchVideoChannel (url: string, search: string, token?: string, statusCodeExpected = HttpStatusCode.OK_200) {
6 const path = '/api/v1/search/video-channels'
7
8 return makeGetRequest({
9 url,
10 path,
11 query: {
12 sort: '-createdAt',
13 search
14 },
15 token,
16 statusCodeExpected
17 })
18}
19
20function advancedVideoChannelSearch (url: string, search: VideoChannelsSearchQuery) {
21 const path = '/api/v1/search/video-channels'
22
23 return makeGetRequest({
24 url,
25 path,
26 query: search,
27 statusCodeExpected: HttpStatusCode.OK_200
28 })
29}
30
31// ---------------------------------------------------------------------------
32
33export {
34 searchVideoChannel,
35 advancedVideoChannelSearch
36}
diff --git a/shared/extra-utils/search/video-playlists.ts b/shared/extra-utils/search/video-playlists.ts
deleted file mode 100644
index c22831df7..000000000
--- a/shared/extra-utils/search/video-playlists.ts
+++ /dev/null
@@ -1,36 +0,0 @@
1import { VideoPlaylistsSearchQuery } from '@shared/models'
2import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
3import { makeGetRequest } from '../requests/requests'
4
5function searchVideoPlaylists (url: string, search: string, token?: string, statusCodeExpected = HttpStatusCode.OK_200) {
6 const path = '/api/v1/search/video-playlists'
7
8 return makeGetRequest({
9 url,
10 path,
11 query: {
12 sort: '-createdAt',
13 search
14 },
15 token,
16 statusCodeExpected
17 })
18}
19
20function advancedVideoPlaylistSearch (url: string, search: VideoPlaylistsSearchQuery) {
21 const path = '/api/v1/search/video-playlists'
22
23 return makeGetRequest({
24 url,
25 path,
26 query: search,
27 statusCodeExpected: HttpStatusCode.OK_200
28 })
29}
30
31// ---------------------------------------------------------------------------
32
33export {
34 searchVideoPlaylists,
35 advancedVideoPlaylistSearch
36}
diff --git a/shared/extra-utils/search/videos.ts b/shared/extra-utils/search/videos.ts
deleted file mode 100644
index db6edbd58..000000000
--- a/shared/extra-utils/search/videos.ts
+++ /dev/null
@@ -1,64 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import * as request from 'supertest'
4import { VideosSearchQuery } from '../../models/search'
5import { immutableAssign } from '../miscs/miscs'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7
8function searchVideo (url: string, search: string, sort = '-publishedAt') {
9 const path = '/api/v1/search/videos'
10
11 const query = { sort, search: search }
12 const req = request(url)
13 .get(path)
14 .query(query)
15 .set('Accept', 'application/json')
16
17 return req.expect(HttpStatusCode.OK_200)
18 .expect('Content-Type', /json/)
19}
20
21function searchVideoWithToken (url: string, search: string, token: string, query: { nsfw?: boolean } = {}) {
22 const path = '/api/v1/search/videos'
23 const req = request(url)
24 .get(path)
25 .set('Authorization', 'Bearer ' + token)
26 .query(immutableAssign(query, { sort: '-publishedAt', search }))
27 .set('Accept', 'application/json')
28
29 return req.expect(HttpStatusCode.OK_200)
30 .expect('Content-Type', /json/)
31}
32
33function searchVideoWithSort (url: string, search: string, sort: string) {
34 const path = '/api/v1/search/videos'
35
36 const query = { search, sort }
37
38 return request(url)
39 .get(path)
40 .query(query)
41 .set('Accept', 'application/json')
42 .expect(HttpStatusCode.OK_200)
43 .expect('Content-Type', /json/)
44}
45
46function advancedVideosSearch (url: string, options: VideosSearchQuery) {
47 const path = '/api/v1/search/videos'
48
49 return request(url)
50 .get(path)
51 .query(options)
52 .set('Accept', 'application/json')
53 .expect(HttpStatusCode.OK_200)
54 .expect('Content-Type', /json/)
55}
56
57// ---------------------------------------------------------------------------
58
59export {
60 searchVideo,
61 advancedVideosSearch,
62 searchVideoWithToken,
63 searchVideoWithSort
64}
diff --git a/shared/extra-utils/server/activitypub.ts b/shared/extra-utils/server/activitypub.ts
deleted file mode 100644
index cf967ed7d..000000000
--- a/shared/extra-utils/server/activitypub.ts
+++ /dev/null
@@ -1,15 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
5 return request(url)
6 .get(path)
7 .set('Accept', 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8')
8 .expect(expectedStatus)
9}
10
11// ---------------------------------------------------------------------------
12
13export {
14 makeActivityPubGetRequest
15}
diff --git a/shared/extra-utils/server/clients.ts b/shared/extra-utils/server/clients.ts
deleted file mode 100644
index 894fe4911..000000000
--- a/shared/extra-utils/server/clients.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import * as request from 'supertest'
2import { URL } from 'url'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function getClient (url: string) {
6 const path = '/api/v1/oauth-clients/local'
7
8 return request(url)
9 .get(path)
10 .set('Host', new URL(url).host)
11 .set('Accept', 'application/json')
12 .expect(HttpStatusCode.OK_200)
13 .expect('Content-Type', /json/)
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 getClient
20}
diff --git a/shared/extra-utils/server/config-command.ts b/shared/extra-utils/server/config-command.ts
new file mode 100644
index 000000000..11148aa46
--- /dev/null
+++ b/shared/extra-utils/server/config-command.ts
@@ -0,0 +1,263 @@
1import { merge } from 'lodash'
2import { DeepPartial } from '@shared/core-utils'
3import { About, HttpStatusCode, ServerConfig } from '@shared/models'
4import { CustomConfig } from '../../models/server/custom-config.model'
5import { AbstractCommand, OverrideCommandOptions } from '../shared'
6
7export class ConfigCommand extends AbstractCommand {
8
9 static getCustomConfigResolutions (enabled: boolean) {
10 return {
11 '240p': enabled,
12 '360p': enabled,
13 '480p': enabled,
14 '720p': enabled,
15 '1080p': enabled,
16 '1440p': enabled,
17 '2160p': enabled
18 }
19 }
20
21 getConfig (options: OverrideCommandOptions = {}) {
22 const path = '/api/v1/config'
23
24 return this.getRequestBody<ServerConfig>({
25 ...options,
26
27 path,
28 implicitToken: false,
29 defaultExpectedStatus: HttpStatusCode.OK_200
30 })
31 }
32
33 getAbout (options: OverrideCommandOptions = {}) {
34 const path = '/api/v1/config/about'
35
36 return this.getRequestBody<About>({
37 ...options,
38
39 path,
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 })
43 }
44
45 getCustomConfig (options: OverrideCommandOptions = {}) {
46 const path = '/api/v1/config/custom'
47
48 return this.getRequestBody<CustomConfig>({
49 ...options,
50
51 path,
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56
57 updateCustomConfig (options: OverrideCommandOptions & {
58 newCustomConfig: CustomConfig
59 }) {
60 const path = '/api/v1/config/custom'
61
62 return this.putBodyRequest({
63 ...options,
64
65 path,
66 fields: options.newCustomConfig,
67 implicitToken: true,
68 defaultExpectedStatus: HttpStatusCode.OK_200
69 })
70 }
71
72 deleteCustomConfig (options: OverrideCommandOptions = {}) {
73 const path = '/api/v1/config/custom'
74
75 return this.deleteRequest({
76 ...options,
77
78 path,
79 implicitToken: true,
80 defaultExpectedStatus: HttpStatusCode.OK_200
81 })
82 }
83
84 updateCustomSubConfig (options: OverrideCommandOptions & {
85 newConfig: DeepPartial<CustomConfig>
86 }) {
87 const newCustomConfig: CustomConfig = {
88 instance: {
89 name: 'PeerTube updated',
90 shortDescription: 'my short description',
91 description: 'my super description',
92 terms: 'my super terms',
93 codeOfConduct: 'my super coc',
94
95 creationReason: 'my super creation reason',
96 moderationInformation: 'my super moderation information',
97 administrator: 'Kuja',
98 maintenanceLifetime: 'forever',
99 businessModel: 'my super business model',
100 hardwareInformation: '2vCore 3GB RAM',
101
102 languages: [ 'en', 'es' ],
103 categories: [ 1, 2 ],
104
105 isNSFW: true,
106 defaultNSFWPolicy: 'blur',
107
108 defaultClientRoute: '/videos/recently-added',
109
110 customizations: {
111 javascript: 'alert("coucou")',
112 css: 'body { background-color: red; }'
113 }
114 },
115 theme: {
116 default: 'default'
117 },
118 services: {
119 twitter: {
120 username: '@MySuperUsername',
121 whitelisted: true
122 }
123 },
124 cache: {
125 previews: {
126 size: 2
127 },
128 captions: {
129 size: 3
130 },
131 torrents: {
132 size: 4
133 }
134 },
135 signup: {
136 enabled: false,
137 limit: 5,
138 requiresEmailVerification: false,
139 minimumAge: 16
140 },
141 admin: {
142 email: 'superadmin1@example.com'
143 },
144 contactForm: {
145 enabled: true
146 },
147 user: {
148 videoQuota: 5242881,
149 videoQuotaDaily: 318742
150 },
151 transcoding: {
152 enabled: true,
153 allowAdditionalExtensions: true,
154 allowAudioFiles: true,
155 threads: 1,
156 concurrency: 3,
157 profile: 'default',
158 resolutions: {
159 '0p': false,
160 '240p': false,
161 '360p': true,
162 '480p': true,
163 '720p': false,
164 '1080p': false,
165 '1440p': false,
166 '2160p': false
167 },
168 webtorrent: {
169 enabled: true
170 },
171 hls: {
172 enabled: false
173 }
174 },
175 live: {
176 enabled: true,
177 allowReplay: false,
178 maxDuration: -1,
179 maxInstanceLives: -1,
180 maxUserLives: 50,
181 transcoding: {
182 enabled: true,
183 threads: 4,
184 profile: 'default',
185 resolutions: {
186 '240p': true,
187 '360p': true,
188 '480p': true,
189 '720p': true,
190 '1080p': true,
191 '1440p': true,
192 '2160p': true
193 }
194 }
195 },
196 import: {
197 videos: {
198 concurrency: 3,
199 http: {
200 enabled: false
201 },
202 torrent: {
203 enabled: false
204 }
205 }
206 },
207 trending: {
208 videos: {
209 algorithms: {
210 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
211 default: 'hot'
212 }
213 }
214 },
215 autoBlacklist: {
216 videos: {
217 ofUsers: {
218 enabled: false
219 }
220 }
221 },
222 followers: {
223 instance: {
224 enabled: true,
225 manualApproval: false
226 }
227 },
228 followings: {
229 instance: {
230 autoFollowBack: {
231 enabled: false
232 },
233 autoFollowIndex: {
234 indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
235 enabled: false
236 }
237 }
238 },
239 broadcastMessage: {
240 enabled: true,
241 level: 'warning',
242 message: 'hello',
243 dismissable: true
244 },
245 search: {
246 remoteUri: {
247 users: true,
248 anonymous: true
249 },
250 searchIndex: {
251 enabled: true,
252 url: 'https://search.joinpeertube.org',
253 disableLocalSearch: true,
254 isDefaultSearch: true
255 }
256 }
257 }
258
259 merge(newCustomConfig, options.newConfig)
260
261 return this.updateCustomConfig({ ...options, newCustomConfig })
262 }
263}
diff --git a/shared/extra-utils/server/config.ts b/shared/extra-utils/server/config.ts
deleted file mode 100644
index 9fcfb31fd..000000000
--- a/shared/extra-utils/server/config.ts
+++ /dev/null
@@ -1,260 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePutBodyRequest } from '../requests/requests'
2import { CustomConfig } from '../../models/server/custom-config.model'
3import { DeepPartial, HttpStatusCode } from '@shared/core-utils'
4import { merge } from 'lodash'
5
6function getConfig (url: string) {
7 const path = '/api/v1/config'
8
9 return makeGetRequest({
10 url,
11 path,
12 statusCodeExpected: HttpStatusCode.OK_200
13 })
14}
15
16function getAbout (url: string) {
17 const path = '/api/v1/config/about'
18
19 return makeGetRequest({
20 url,
21 path,
22 statusCodeExpected: HttpStatusCode.OK_200
23 })
24}
25
26function getCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
27 const path = '/api/v1/config/custom'
28
29 return makeGetRequest({
30 url,
31 token,
32 path,
33 statusCodeExpected
34 })
35}
36
37function updateCustomConfig (url: string, token: string, newCustomConfig: CustomConfig, statusCodeExpected = HttpStatusCode.OK_200) {
38 const path = '/api/v1/config/custom'
39
40 return makePutBodyRequest({
41 url,
42 token,
43 path,
44 fields: newCustomConfig,
45 statusCodeExpected
46 })
47}
48
49function updateCustomSubConfig (url: string, token: string, newConfig: DeepPartial<CustomConfig>) {
50 const updateParams: CustomConfig = {
51 instance: {
52 name: 'PeerTube updated',
53 shortDescription: 'my short description',
54 description: 'my super description',
55 terms: 'my super terms',
56 codeOfConduct: 'my super coc',
57
58 creationReason: 'my super creation reason',
59 moderationInformation: 'my super moderation information',
60 administrator: 'Kuja',
61 maintenanceLifetime: 'forever',
62 businessModel: 'my super business model',
63 hardwareInformation: '2vCore 3GB RAM',
64
65 languages: [ 'en', 'es' ],
66 categories: [ 1, 2 ],
67
68 isNSFW: true,
69 defaultNSFWPolicy: 'blur',
70
71 defaultClientRoute: '/videos/recently-added',
72
73 customizations: {
74 javascript: 'alert("coucou")',
75 css: 'body { background-color: red; }'
76 }
77 },
78 theme: {
79 default: 'default'
80 },
81 services: {
82 twitter: {
83 username: '@MySuperUsername',
84 whitelisted: true
85 }
86 },
87 cache: {
88 previews: {
89 size: 2
90 },
91 captions: {
92 size: 3
93 },
94 torrents: {
95 size: 4
96 }
97 },
98 signup: {
99 enabled: false,
100 limit: 5,
101 requiresEmailVerification: false,
102 minimumAge: 16
103 },
104 admin: {
105 email: 'superadmin1@example.com'
106 },
107 contactForm: {
108 enabled: true
109 },
110 user: {
111 videoQuota: 5242881,
112 videoQuotaDaily: 318742
113 },
114 transcoding: {
115 enabled: true,
116 allowAdditionalExtensions: true,
117 allowAudioFiles: true,
118 threads: 1,
119 concurrency: 3,
120 profile: 'default',
121 resolutions: {
122 '0p': false,
123 '240p': false,
124 '360p': true,
125 '480p': true,
126 '720p': false,
127 '1080p': false,
128 '1440p': false,
129 '2160p': false
130 },
131 webtorrent: {
132 enabled: true
133 },
134 hls: {
135 enabled: false
136 }
137 },
138 live: {
139 enabled: true,
140 allowReplay: false,
141 maxDuration: -1,
142 maxInstanceLives: -1,
143 maxUserLives: 50,
144 transcoding: {
145 enabled: true,
146 threads: 4,
147 profile: 'default',
148 resolutions: {
149 '240p': true,
150 '360p': true,
151 '480p': true,
152 '720p': true,
153 '1080p': true,
154 '1440p': true,
155 '2160p': true
156 }
157 }
158 },
159 import: {
160 videos: {
161 concurrency: 3,
162 http: {
163 enabled: false
164 },
165 torrent: {
166 enabled: false
167 }
168 }
169 },
170 trending: {
171 videos: {
172 algorithms: {
173 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
174 default: 'hot'
175 }
176 }
177 },
178 autoBlacklist: {
179 videos: {
180 ofUsers: {
181 enabled: false
182 }
183 }
184 },
185 followers: {
186 instance: {
187 enabled: true,
188 manualApproval: false
189 }
190 },
191 followings: {
192 instance: {
193 autoFollowBack: {
194 enabled: false
195 },
196 autoFollowIndex: {
197 indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
198 enabled: false
199 }
200 }
201 },
202 broadcastMessage: {
203 enabled: true,
204 level: 'warning',
205 message: 'hello',
206 dismissable: true
207 },
208 search: {
209 remoteUri: {
210 users: true,
211 anonymous: true
212 },
213 searchIndex: {
214 enabled: true,
215 url: 'https://search.joinpeertube.org',
216 disableLocalSearch: true,
217 isDefaultSearch: true
218 }
219 }
220 }
221
222 merge(updateParams, newConfig)
223
224 return updateCustomConfig(url, token, updateParams)
225}
226
227function getCustomConfigResolutions (enabled: boolean) {
228 return {
229 '240p': enabled,
230 '360p': enabled,
231 '480p': enabled,
232 '720p': enabled,
233 '1080p': enabled,
234 '1440p': enabled,
235 '2160p': enabled
236 }
237}
238
239function deleteCustomConfig (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
240 const path = '/api/v1/config/custom'
241
242 return makeDeleteRequest({
243 url,
244 token,
245 path,
246 statusCodeExpected
247 })
248}
249
250// ---------------------------------------------------------------------------
251
252export {
253 getConfig,
254 getCustomConfig,
255 updateCustomConfig,
256 getAbout,
257 deleteCustomConfig,
258 updateCustomSubConfig,
259 getCustomConfigResolutions
260}
diff --git a/shared/extra-utils/server/contact-form-command.ts b/shared/extra-utils/server/contact-form-command.ts
new file mode 100644
index 000000000..0e8fd6d84
--- /dev/null
+++ b/shared/extra-utils/server/contact-form-command.ts
@@ -0,0 +1,31 @@
1import { HttpStatusCode } from '@shared/models'
2import { ContactForm } from '../../models/server'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class ContactFormCommand extends AbstractCommand {
6
7 send (options: OverrideCommandOptions & {
8 fromEmail: string
9 fromName: string
10 subject: string
11 body: string
12 }) {
13 const path = '/api/v1/server/contact'
14
15 const body: ContactForm = {
16 fromEmail: options.fromEmail,
17 fromName: options.fromName,
18 subject: options.subject,
19 body: options.body
20 }
21
22 return this.postBodyRequest({
23 ...options,
24
25 path,
26 fields: body,
27 implicitToken: false,
28 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
29 })
30 }
31}
diff --git a/shared/extra-utils/server/contact-form.ts b/shared/extra-utils/server/contact-form.ts
deleted file mode 100644
index 6c9232cc6..000000000
--- a/shared/extra-utils/server/contact-form.ts
+++ /dev/null
@@ -1,31 +0,0 @@
1import * as request from 'supertest'
2import { ContactForm } from '../../models/server'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function sendContactForm (options: {
6 url: string
7 fromEmail: string
8 fromName: string
9 subject: string
10 body: string
11 expectedStatus?: number
12}) {
13 const path = '/api/v1/server/contact'
14
15 const body: ContactForm = {
16 fromEmail: options.fromEmail,
17 fromName: options.fromName,
18 subject: options.subject,
19 body: options.body
20 }
21 return request(options.url)
22 .post(path)
23 .send(body)
24 .expect(options.expectedStatus || HttpStatusCode.NO_CONTENT_204)
25}
26
27// ---------------------------------------------------------------------------
28
29export {
30 sendContactForm
31}
diff --git a/shared/extra-utils/server/debug-command.ts b/shared/extra-utils/server/debug-command.ts
new file mode 100644
index 000000000..3c5a785bb
--- /dev/null
+++ b/shared/extra-utils/server/debug-command.ts
@@ -0,0 +1,33 @@
1import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class DebugCommand extends AbstractCommand {
5
6 getDebug (options: OverrideCommandOptions = {}) {
7 const path = '/api/v1/server/debug'
8
9 return this.getRequestBody<Debug>({
10 ...options,
11
12 path,
13 implicitToken: true,
14 defaultExpectedStatus: HttpStatusCode.OK_200
15 })
16 }
17
18 sendCommand (options: OverrideCommandOptions & {
19 body: SendDebugCommand
20 }) {
21 const { body } = options
22 const path = '/api/v1/server/debug/run-command'
23
24 return this.postBodyRequest({
25 ...options,
26
27 path,
28 fields: body,
29 implicitToken: true,
30 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
31 })
32 }
33}
diff --git a/shared/extra-utils/server/debug.ts b/shared/extra-utils/server/debug.ts
deleted file mode 100644
index f196812b7..000000000
--- a/shared/extra-utils/server/debug.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import { makeGetRequest, makePostBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../core-utils/miscs/http-error-codes'
3import { SendDebugCommand } from '@shared/models'
4
5function getDebug (url: string, token: string) {
6 const path = '/api/v1/server/debug'
7
8 return makeGetRequest({
9 url,
10 path,
11 token,
12 statusCodeExpected: HttpStatusCode.OK_200
13 })
14}
15
16function sendDebugCommand (url: string, token: string, body: SendDebugCommand) {
17 const path = '/api/v1/server/debug/run-command'
18
19 return makePostBodyRequest({
20 url,
21 path,
22 token,
23 fields: body,
24 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
25 })
26}
27
28// ---------------------------------------------------------------------------
29
30export {
31 getDebug,
32 sendDebugCommand
33}
diff --git a/shared/extra-utils/server/directories.ts b/shared/extra-utils/server/directories.ts
new file mode 100644
index 000000000..b6465cbf4
--- /dev/null
+++ b/shared/extra-utils/server/directories.ts
@@ -0,0 +1,34 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@server/helpers/core-utils'
7import { PeerTubeServer } from './server'
8
9async function checkTmpIsEmpty (server: PeerTubeServer) {
10 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ])
11
12 if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) {
13 await checkDirectoryIsEmpty(server, 'tmp/hls')
14 }
15}
16
17async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) {
18 const testDirectory = 'test' + server.internalServerNumber
19
20 const directoryPath = join(root(), testDirectory, directory)
21
22 const directoryExists = await pathExists(directoryPath)
23 expect(directoryExists).to.be.true
24
25 const files = await readdir(directoryPath)
26 const filtered = files.filter(f => exceptions.includes(f) === false)
27
28 expect(filtered).to.have.lengthOf(0)
29}
30
31export {
32 checkTmpIsEmpty,
33 checkDirectoryIsEmpty
34}
diff --git a/shared/extra-utils/server/follows-command.ts b/shared/extra-utils/server/follows-command.ts
new file mode 100644
index 000000000..01ef6f179
--- /dev/null
+++ b/shared/extra-utils/server/follows-command.ts
@@ -0,0 +1,139 @@
1import { pick } from '@shared/core-utils'
2import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4import { PeerTubeServer } from './server'
5
6export class FollowsCommand extends AbstractCommand {
7
8 getFollowers (options: OverrideCommandOptions & {
9 start: number
10 count: number
11 sort: string
12 search?: string
13 actorType?: ActivityPubActorType
14 state?: FollowState
15 }) {
16 const path = '/api/v1/server/followers'
17
18 const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
19
20 return this.getRequestBody<ResultList<ActorFollow>>({
21 ...options,
22
23 path,
24 query,
25 implicitToken: false,
26 defaultExpectedStatus: HttpStatusCode.OK_200
27 })
28 }
29
30 getFollowings (options: OverrideCommandOptions & {
31 start?: number
32 count?: number
33 sort?: string
34 search?: string
35 actorType?: ActivityPubActorType
36 state?: FollowState
37 } = {}) {
38 const path = '/api/v1/server/following'
39
40 const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
41
42 return this.getRequestBody<ResultList<ActorFollow>>({
43 ...options,
44
45 path,
46 query,
47 implicitToken: false,
48 defaultExpectedStatus: HttpStatusCode.OK_200
49 })
50 }
51
52 follow (options: OverrideCommandOptions & {
53 hosts?: string[]
54 handles?: string[]
55 }) {
56 const path = '/api/v1/server/following'
57
58 const fields: ServerFollowCreate = {}
59
60 if (options.hosts) {
61 fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, ''))
62 }
63
64 if (options.handles) {
65 fields.handles = options.handles
66 }
67
68 return this.postBodyRequest({
69 ...options,
70
71 path,
72 fields,
73 implicitToken: true,
74 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
75 })
76 }
77
78 async unfollow (options: OverrideCommandOptions & {
79 target: PeerTubeServer | string
80 }) {
81 const { target } = options
82
83 const handle = typeof target === 'string'
84 ? target
85 : target.host
86
87 const path = '/api/v1/server/following/' + handle
88
89 return this.deleteRequest({
90 ...options,
91
92 path,
93 implicitToken: true,
94 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
95 })
96 }
97
98 acceptFollower (options: OverrideCommandOptions & {
99 follower: string
100 }) {
101 const path = '/api/v1/server/followers/' + options.follower + '/accept'
102
103 return this.postBodyRequest({
104 ...options,
105
106 path,
107 implicitToken: true,
108 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
109 })
110 }
111
112 rejectFollower (options: OverrideCommandOptions & {
113 follower: string
114 }) {
115 const path = '/api/v1/server/followers/' + options.follower + '/reject'
116
117 return this.postBodyRequest({
118 ...options,
119
120 path,
121 implicitToken: true,
122 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
123 })
124 }
125
126 removeFollower (options: OverrideCommandOptions & {
127 follower: PeerTubeServer
128 }) {
129 const path = '/api/v1/server/followers/peertube@' + options.follower.host
130
131 return this.deleteRequest({
132 ...options,
133
134 path,
135 implicitToken: true,
136 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
137 })
138 }
139}
diff --git a/shared/extra-utils/server/follows.ts b/shared/extra-utils/server/follows.ts
index 6aae4a31d..698238f29 100644
--- a/shared/extra-utils/server/follows.ts
+++ b/shared/extra-utils/server/follows.ts
@@ -1,126 +1,10 @@
1import * as request from 'supertest'
2import { ServerInfo } from './servers'
3import { waitJobs } from './jobs' 1import { waitJobs } from './jobs'
4import { makePostBodyRequest } from '../requests/requests' 2import { PeerTubeServer } from './server'
5import { ActivityPubActorType, FollowState } from '@shared/models'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7 3
8function getFollowersListPaginationAndSort (options: { 4async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
9 url: string
10 start: number
11 count: number
12 sort: string
13 search?: string
14 actorType?: ActivityPubActorType
15 state?: FollowState
16}) {
17 const { url, start, count, sort, search, state, actorType } = options
18 const path = '/api/v1/server/followers'
19
20 const query = {
21 start,
22 count,
23 sort,
24 search,
25 state,
26 actorType
27 }
28
29 return request(url)
30 .get(path)
31 .query(query)
32 .set('Accept', 'application/json')
33 .expect(HttpStatusCode.OK_200)
34 .expect('Content-Type', /json/)
35}
36
37function acceptFollower (url: string, token: string, follower: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
38 const path = '/api/v1/server/followers/' + follower + '/accept'
39
40 return makePostBodyRequest({
41 url,
42 token,
43 path,
44 statusCodeExpected
45 })
46}
47
48function rejectFollower (url: string, token: string, follower: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
49 const path = '/api/v1/server/followers/' + follower + '/reject'
50
51 return makePostBodyRequest({
52 url,
53 token,
54 path,
55 statusCodeExpected
56 })
57}
58
59function getFollowingListPaginationAndSort (options: {
60 url: string
61 start: number
62 count: number
63 sort: string
64 search?: string
65 actorType?: ActivityPubActorType
66 state?: FollowState
67}) {
68 const { url, start, count, sort, search, state, actorType } = options
69 const path = '/api/v1/server/following'
70
71 const query = {
72 start,
73 count,
74 sort,
75 search,
76 state,
77 actorType
78 }
79
80 return request(url)
81 .get(path)
82 .query(query)
83 .set('Accept', 'application/json')
84 .expect(HttpStatusCode.OK_200)
85 .expect('Content-Type', /json/)
86}
87
88function follow (follower: string, following: string[], accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
89 const path = '/api/v1/server/following'
90
91 const followingHosts = following.map(f => f.replace(/^http:\/\//, ''))
92 return request(follower)
93 .post(path)
94 .set('Accept', 'application/json')
95 .set('Authorization', 'Bearer ' + accessToken)
96 .send({ hosts: followingHosts })
97 .expect(expectedStatus)
98}
99
100async function unfollow (url: string, accessToken: string, target: ServerInfo, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
101 const path = '/api/v1/server/following/' + target.host
102
103 return request(url)
104 .delete(path)
105 .set('Accept', 'application/json')
106 .set('Authorization', 'Bearer ' + accessToken)
107 .expect(expectedStatus)
108}
109
110function removeFollower (url: string, accessToken: string, follower: ServerInfo, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
111 const path = '/api/v1/server/followers/peertube@' + follower.host
112
113 return request(url)
114 .delete(path)
115 .set('Accept', 'application/json')
116 .set('Authorization', 'Bearer ' + accessToken)
117 .expect(expectedStatus)
118}
119
120async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
121 await Promise.all([ 5 await Promise.all([
122 follow(server1.url, [ server2.url ], server1.accessToken), 6 server1.follows.follow({ hosts: [ server2.url ] }),
123 follow(server2.url, [ server1.url ], server2.accessToken) 7 server2.follows.follow({ hosts: [ server1.url ] })
124 ]) 8 ])
125 9
126 // Wait request propagation 10 // Wait request propagation
@@ -132,12 +16,5 @@ async function doubleFollow (server1: ServerInfo, server2: ServerInfo) {
132// --------------------------------------------------------------------------- 16// ---------------------------------------------------------------------------
133 17
134export { 18export {
135 getFollowersListPaginationAndSort, 19 doubleFollow
136 getFollowingListPaginationAndSort,
137 unfollow,
138 removeFollower,
139 follow,
140 doubleFollow,
141 acceptFollower,
142 rejectFollower
143} 20}
diff --git a/shared/extra-utils/server/index.ts b/shared/extra-utils/server/index.ts
new file mode 100644
index 000000000..9055dfc57
--- /dev/null
+++ b/shared/extra-utils/server/index.ts
@@ -0,0 +1,15 @@
1export * from './config-command'
2export * from './contact-form-command'
3export * from './debug-command'
4export * from './directories'
5export * from './follows-command'
6export * from './follows'
7export * from './jobs'
8export * from './jobs-command'
9export * from './plugins-command'
10export * from './plugins'
11export * from './redundancy-command'
12export * from './server'
13export * from './servers-command'
14export * from './servers'
15export * from './stats-command'
diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts
new file mode 100644
index 000000000..c4eb12dc2
--- /dev/null
+++ b/shared/extra-utils/server/jobs-command.ts
@@ -0,0 +1,36 @@
1import { pick } from '@shared/core-utils'
2import { HttpStatusCode } from '@shared/models'
3import { Job, JobState, JobType, ResultList } from '../../models'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class JobsCommand extends AbstractCommand {
7
8 getJobsList (options: OverrideCommandOptions & {
9 state?: JobState
10 jobType?: JobType
11 start?: number
12 count?: number
13 sort?: string
14 } = {}) {
15 const path = this.buildJobsUrl(options.state)
16
17 const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ])
18
19 return this.getRequestBody<ResultList<Job>>({
20 ...options,
21
22 path,
23 query,
24 implicitToken: true,
25 defaultExpectedStatus: HttpStatusCode.OK_200
26 })
27 }
28
29 private buildJobsUrl (state?: JobState) {
30 let path = '/api/v1/jobs'
31
32 if (state) path += '/' + state
33
34 return path
35 }
36}
diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts
index 763374e03..64a0353eb 100644
--- a/shared/extra-utils/server/jobs.ts
+++ b/shared/extra-utils/server/jobs.ts
@@ -1,66 +1,17 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3import { getDebug, makeGetRequest } from '../../../shared/extra-utils'
4import { Job, JobState, JobType, ServerDebug } from '../../models'
5import { wait } from '../miscs/miscs'
6import { ServerInfo } from './servers'
7 1
8function buildJobsUrl (state?: JobState) { 2import { JobState } from '../../models'
9 let path = '/api/v1/jobs' 3import { wait } from '../miscs'
4import { PeerTubeServer } from './server'
10 5
11 if (state) path += '/' + state 6async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer) {
12
13 return path
14}
15
16function getJobsList (url: string, accessToken: string, state?: JobState) {
17 const path = buildJobsUrl(state)
18
19 return request(url)
20 .get(path)
21 .set('Accept', 'application/json')
22 .set('Authorization', 'Bearer ' + accessToken)
23 .expect(HttpStatusCode.OK_200)
24 .expect('Content-Type', /json/)
25}
26
27function getJobsListPaginationAndSort (options: {
28 url: string
29 accessToken: string
30 start: number
31 count: number
32 sort: string
33 state?: JobState
34 jobType?: JobType
35}) {
36 const { url, accessToken, state, start, count, sort, jobType } = options
37 const path = buildJobsUrl(state)
38
39 const query = {
40 start,
41 count,
42 sort,
43 jobType
44 }
45
46 return makeGetRequest({
47 url,
48 path,
49 token: accessToken,
50 statusCodeExpected: HttpStatusCode.OK_200,
51 query
52 })
53}
54
55async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
56 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT 7 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
57 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10) 8 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
58 : 250 9 : 250
59 10
60 let servers: ServerInfo[] 11 let servers: PeerTubeServer[]
61 12
62 if (Array.isArray(serversArg) === false) servers = [ serversArg as ServerInfo ] 13 if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ]
63 else servers = serversArg as ServerInfo[] 14 else servers = serversArg as PeerTubeServer[]
64 15
65 const states: JobState[] = [ 'waiting', 'active', 'delayed' ] 16 const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
66 const repeatableJobs = [ 'videos-views', 'activitypub-cleaner' ] 17 const repeatableJobs = [ 'videos-views', 'activitypub-cleaner' ]
@@ -72,15 +23,13 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
72 // Check if each server has pending request 23 // Check if each server has pending request
73 for (const server of servers) { 24 for (const server of servers) {
74 for (const state of states) { 25 for (const state of states) {
75 const p = getJobsListPaginationAndSort({ 26 const p = server.jobs.getJobsList({
76 url: server.url, 27 state,
77 accessToken: server.accessToken,
78 state: state,
79 start: 0, 28 start: 0,
80 count: 10, 29 count: 10,
81 sort: '-createdAt' 30 sort: '-createdAt'
82 }).then(res => res.body.data) 31 }).then(body => body.data)
83 .then((jobs: Job[]) => jobs.filter(j => !repeatableJobs.includes(j.type))) 32 .then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type)))
84 .then(jobs => { 33 .then(jobs => {
85 if (jobs.length !== 0) { 34 if (jobs.length !== 0) {
86 pendingRequests = true 35 pendingRequests = true
@@ -90,9 +39,8 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
90 tasks.push(p) 39 tasks.push(p)
91 } 40 }
92 41
93 const p = getDebug(server.url, server.accessToken) 42 const p = server.debug.getDebug()
94 .then(res => res.body) 43 .then(obj => {
95 .then((obj: ServerDebug) => {
96 if (obj.activityPubMessagesWaiting !== 0) { 44 if (obj.activityPubMessagesWaiting !== 0) {
97 pendingRequests = true 45 pendingRequests = true
98 } 46 }
@@ -123,7 +71,5 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
123// --------------------------------------------------------------------------- 71// ---------------------------------------------------------------------------
124 72
125export { 73export {
126 getJobsList, 74 waitJobs
127 waitJobs,
128 getJobsListPaginationAndSort
129} 75}
diff --git a/shared/extra-utils/server/plugins-command.ts b/shared/extra-utils/server/plugins-command.ts
new file mode 100644
index 000000000..b944475a2
--- /dev/null
+++ b/shared/extra-utils/server/plugins-command.ts
@@ -0,0 +1,256 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { readJSON, writeJSON } from 'fs-extra'
4import { join } from 'path'
5import { root } from '@server/helpers/core-utils'
6import {
7 HttpStatusCode,
8 PeerTubePlugin,
9 PeerTubePluginIndex,
10 PeertubePluginIndexList,
11 PluginPackageJson,
12 PluginTranslation,
13 PluginType,
14 PublicServerSetting,
15 RegisteredServerSettings,
16 ResultList
17} from '@shared/models'
18import { AbstractCommand, OverrideCommandOptions } from '../shared'
19
20export class PluginsCommand extends AbstractCommand {
21
22 static getPluginTestPath (suffix = '') {
23 return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
24 }
25
26 list (options: OverrideCommandOptions & {
27 start?: number
28 count?: number
29 sort?: string
30 pluginType?: PluginType
31 uninstalled?: boolean
32 }) {
33 const { start, count, sort, pluginType, uninstalled } = options
34 const path = '/api/v1/plugins'
35
36 return this.getRequestBody<ResultList<PeerTubePlugin>>({
37 ...options,
38
39 path,
40 query: {
41 start,
42 count,
43 sort,
44 pluginType,
45 uninstalled
46 },
47 implicitToken: true,
48 defaultExpectedStatus: HttpStatusCode.OK_200
49 })
50 }
51
52 listAvailable (options: OverrideCommandOptions & {
53 start?: number
54 count?: number
55 sort?: string
56 pluginType?: PluginType
57 currentPeerTubeEngine?: string
58 search?: string
59 expectedStatus?: HttpStatusCode
60 }) {
61 const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options
62 const path = '/api/v1/plugins/available'
63
64 const query: PeertubePluginIndexList = {
65 start,
66 count,
67 sort,
68 pluginType,
69 currentPeerTubeEngine,
70 search
71 }
72
73 return this.getRequestBody<ResultList<PeerTubePluginIndex>>({
74 ...options,
75
76 path,
77 query,
78 implicitToken: true,
79 defaultExpectedStatus: HttpStatusCode.OK_200
80 })
81 }
82
83 get (options: OverrideCommandOptions & {
84 npmName: string
85 }) {
86 const path = '/api/v1/plugins/' + options.npmName
87
88 return this.getRequestBody<PeerTubePlugin>({
89 ...options,
90
91 path,
92 implicitToken: true,
93 defaultExpectedStatus: HttpStatusCode.OK_200
94 })
95 }
96
97 updateSettings (options: OverrideCommandOptions & {
98 npmName: string
99 settings: any
100 }) {
101 const { npmName, settings } = options
102 const path = '/api/v1/plugins/' + npmName + '/settings'
103
104 return this.putBodyRequest({
105 ...options,
106
107 path,
108 fields: { settings },
109 implicitToken: true,
110 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
111 })
112 }
113
114 getRegisteredSettings (options: OverrideCommandOptions & {
115 npmName: string
116 }) {
117 const path = '/api/v1/plugins/' + options.npmName + '/registered-settings'
118
119 return this.getRequestBody<RegisteredServerSettings>({
120 ...options,
121
122 path,
123 implicitToken: true,
124 defaultExpectedStatus: HttpStatusCode.OK_200
125 })
126 }
127
128 getPublicSettings (options: OverrideCommandOptions & {
129 npmName: string
130 }) {
131 const { npmName } = options
132 const path = '/api/v1/plugins/' + npmName + '/public-settings'
133
134 return this.getRequestBody<PublicServerSetting>({
135 ...options,
136
137 path,
138 implicitToken: false,
139 defaultExpectedStatus: HttpStatusCode.OK_200
140 })
141 }
142
143 getTranslations (options: OverrideCommandOptions & {
144 locale: string
145 }) {
146 const { locale } = options
147 const path = '/plugins/translations/' + locale + '.json'
148
149 return this.getRequestBody<PluginTranslation>({
150 ...options,
151
152 path,
153 implicitToken: false,
154 defaultExpectedStatus: HttpStatusCode.OK_200
155 })
156 }
157
158 install (options: OverrideCommandOptions & {
159 path?: string
160 npmName?: string
161 }) {
162 const { npmName, path } = options
163 const apiPath = '/api/v1/plugins/install'
164
165 return this.postBodyRequest({
166 ...options,
167
168 path: apiPath,
169 fields: { npmName, path },
170 implicitToken: true,
171 defaultExpectedStatus: HttpStatusCode.OK_200
172 })
173 }
174
175 update (options: OverrideCommandOptions & {
176 path?: string
177 npmName?: string
178 }) {
179 const { npmName, path } = options
180 const apiPath = '/api/v1/plugins/update'
181
182 return this.postBodyRequest({
183 ...options,
184
185 path: apiPath,
186 fields: { npmName, path },
187 implicitToken: true,
188 defaultExpectedStatus: HttpStatusCode.OK_200
189 })
190 }
191
192 uninstall (options: OverrideCommandOptions & {
193 npmName: string
194 }) {
195 const { npmName } = options
196 const apiPath = '/api/v1/plugins/uninstall'
197
198 return this.postBodyRequest({
199 ...options,
200
201 path: apiPath,
202 fields: { npmName },
203 implicitToken: true,
204 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
205 })
206 }
207
208 getCSS (options: OverrideCommandOptions = {}) {
209 const path = '/plugins/global.css'
210
211 return this.getRequestText({
212 ...options,
213
214 path,
215 implicitToken: false,
216 defaultExpectedStatus: HttpStatusCode.OK_200
217 })
218 }
219
220 getExternalAuth (options: OverrideCommandOptions & {
221 npmName: string
222 npmVersion: string
223 authName: string
224 query?: any
225 }) {
226 const { npmName, npmVersion, authName, query } = options
227
228 const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
229
230 return this.getRequest({
231 ...options,
232
233 path,
234 query,
235 implicitToken: false,
236 defaultExpectedStatus: HttpStatusCode.OK_200,
237 redirects: 0
238 })
239 }
240
241 updatePackageJSON (npmName: string, json: any) {
242 const path = this.getPackageJSONPath(npmName)
243
244 return writeJSON(path, json)
245 }
246
247 getPackageJSON (npmName: string): Promise<PluginPackageJson> {
248 const path = this.getPackageJSONPath(npmName)
249
250 return readJSON(path)
251 }
252
253 private getPackageJSONPath (npmName: string) {
254 return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json'))
255 }
256}
diff --git a/shared/extra-utils/server/plugins.ts b/shared/extra-utils/server/plugins.ts
index d53e5b382..0f5fabd5a 100644
--- a/shared/extra-utils/server/plugins.ts
+++ b/shared/extra-utils/server/plugins.ts
@@ -1,307 +1,18 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { readJSON, writeJSON } from 'fs-extra' 4import { PeerTubeServer } from '../server/server'
5import { join } from 'path'
6import { RegisteredServerSettings } from '@shared/models'
7import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
8import { PeertubePluginIndexList } from '../../models/plugins/plugin-index/peertube-plugin-index-list.model'
9import { PluginType } from '../../models/plugins/plugin.type'
10import { buildServerDirectory, root } from '../miscs/miscs'
11import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
12import { ServerInfo } from './servers'
13 5
14function listPlugins (parameters: { 6async function testHelloWorldRegisteredSettings (server: PeerTubeServer) {
15 url: string 7 const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' })
16 accessToken: string
17 start?: number
18 count?: number
19 sort?: string
20 pluginType?: PluginType
21 uninstalled?: boolean
22 expectedStatus?: HttpStatusCode
23}) {
24 const { url, accessToken, start, count, sort, pluginType, uninstalled, expectedStatus = HttpStatusCode.OK_200 } = parameters
25 const path = '/api/v1/plugins'
26
27 return makeGetRequest({
28 url,
29 path,
30 token: accessToken,
31 query: {
32 start,
33 count,
34 sort,
35 pluginType,
36 uninstalled
37 },
38 statusCodeExpected: expectedStatus
39 })
40}
41
42function listAvailablePlugins (parameters: {
43 url: string
44 accessToken: string
45 start?: number
46 count?: number
47 sort?: string
48 pluginType?: PluginType
49 currentPeerTubeEngine?: string
50 search?: string
51 expectedStatus?: HttpStatusCode
52}) {
53 const {
54 url,
55 accessToken,
56 start,
57 count,
58 sort,
59 pluginType,
60 search,
61 currentPeerTubeEngine,
62 expectedStatus = HttpStatusCode.OK_200
63 } = parameters
64 const path = '/api/v1/plugins/available'
65
66 const query: PeertubePluginIndexList = {
67 start,
68 count,
69 sort,
70 pluginType,
71 currentPeerTubeEngine,
72 search
73 }
74
75 return makeGetRequest({
76 url,
77 path,
78 token: accessToken,
79 query,
80 statusCodeExpected: expectedStatus
81 })
82}
83
84function getPlugin (parameters: {
85 url: string
86 accessToken: string
87 npmName: string
88 expectedStatus?: HttpStatusCode
89}) {
90 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
91 const path = '/api/v1/plugins/' + npmName
92
93 return makeGetRequest({
94 url,
95 path,
96 token: accessToken,
97 statusCodeExpected: expectedStatus
98 })
99}
100
101function updatePluginSettings (parameters: {
102 url: string
103 accessToken: string
104 npmName: string
105 settings: any
106 expectedStatus?: HttpStatusCode
107}) {
108 const { url, accessToken, npmName, settings, expectedStatus = HttpStatusCode.NO_CONTENT_204 } = parameters
109 const path = '/api/v1/plugins/' + npmName + '/settings'
110
111 return makePutBodyRequest({
112 url,
113 path,
114 token: accessToken,
115 fields: { settings },
116 statusCodeExpected: expectedStatus
117 })
118}
119
120function getPluginRegisteredSettings (parameters: {
121 url: string
122 accessToken: string
123 npmName: string
124 expectedStatus?: HttpStatusCode
125}) {
126 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
127 const path = '/api/v1/plugins/' + npmName + '/registered-settings'
128
129 return makeGetRequest({
130 url,
131 path,
132 token: accessToken,
133 statusCodeExpected: expectedStatus
134 })
135}
136
137async function testHelloWorldRegisteredSettings (server: ServerInfo) {
138 const res = await getPluginRegisteredSettings({
139 url: server.url,
140 accessToken: server.accessToken,
141 npmName: 'peertube-plugin-hello-world'
142 })
143
144 const registeredSettings = (res.body as RegisteredServerSettings).registeredSettings
145 8
9 const registeredSettings = body.registeredSettings
146 expect(registeredSettings).to.have.length.at.least(1) 10 expect(registeredSettings).to.have.length.at.least(1)
147 11
148 const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name') 12 const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name')
149 expect(adminNameSettings).to.not.be.undefined 13 expect(adminNameSettings).to.not.be.undefined
150} 14}
151 15
152function getPublicSettings (parameters: {
153 url: string
154 npmName: string
155 expectedStatus?: HttpStatusCode
156}) {
157 const { url, npmName, expectedStatus = HttpStatusCode.OK_200 } = parameters
158 const path = '/api/v1/plugins/' + npmName + '/public-settings'
159
160 return makeGetRequest({
161 url,
162 path,
163 statusCodeExpected: expectedStatus
164 })
165}
166
167function getPluginTranslations (parameters: {
168 url: string
169 locale: string
170 expectedStatus?: HttpStatusCode
171}) {
172 const { url, locale, expectedStatus = HttpStatusCode.OK_200 } = parameters
173 const path = '/plugins/translations/' + locale + '.json'
174
175 return makeGetRequest({
176 url,
177 path,
178 statusCodeExpected: expectedStatus
179 })
180}
181
182function installPlugin (parameters: {
183 url: string
184 accessToken: string
185 path?: string
186 npmName?: string
187 expectedStatus?: HttpStatusCode
188}) {
189 const { url, accessToken, npmName, path, expectedStatus = HttpStatusCode.OK_200 } = parameters
190 const apiPath = '/api/v1/plugins/install'
191
192 return makePostBodyRequest({
193 url,
194 path: apiPath,
195 token: accessToken,
196 fields: { npmName, path },
197 statusCodeExpected: expectedStatus
198 })
199}
200
201function updatePlugin (parameters: {
202 url: string
203 accessToken: string
204 path?: string
205 npmName?: string
206 expectedStatus?: HttpStatusCode
207}) {
208 const { url, accessToken, npmName, path, expectedStatus = HttpStatusCode.OK_200 } = parameters
209 const apiPath = '/api/v1/plugins/update'
210
211 return makePostBodyRequest({
212 url,
213 path: apiPath,
214 token: accessToken,
215 fields: { npmName, path },
216 statusCodeExpected: expectedStatus
217 })
218}
219
220function uninstallPlugin (parameters: {
221 url: string
222 accessToken: string
223 npmName: string
224 expectedStatus?: HttpStatusCode
225}) {
226 const { url, accessToken, npmName, expectedStatus = HttpStatusCode.NO_CONTENT_204 } = parameters
227 const apiPath = '/api/v1/plugins/uninstall'
228
229 return makePostBodyRequest({
230 url,
231 path: apiPath,
232 token: accessToken,
233 fields: { npmName },
234 statusCodeExpected: expectedStatus
235 })
236}
237
238function getPluginsCSS (url: string) {
239 const path = '/plugins/global.css'
240
241 return makeGetRequest({
242 url,
243 path,
244 statusCodeExpected: HttpStatusCode.OK_200
245 })
246}
247
248function getPackageJSONPath (server: ServerInfo, npmName: string) {
249 return buildServerDirectory(server, join('plugins', 'node_modules', npmName, 'package.json'))
250}
251
252function updatePluginPackageJSON (server: ServerInfo, npmName: string, json: any) {
253 const path = getPackageJSONPath(server, npmName)
254
255 return writeJSON(path, json)
256}
257
258function getPluginPackageJSON (server: ServerInfo, npmName: string) {
259 const path = getPackageJSONPath(server, npmName)
260
261 return readJSON(path)
262}
263
264function getPluginTestPath (suffix = '') {
265 return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix)
266}
267
268function getExternalAuth (options: {
269 url: string
270 npmName: string
271 npmVersion: string
272 authName: string
273 query?: any
274 statusCodeExpected?: HttpStatusCode
275}) {
276 const { url, npmName, npmVersion, authName, statusCodeExpected, query } = options
277
278 const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
279
280 return makeGetRequest({
281 url,
282 path,
283 query,
284 statusCodeExpected: statusCodeExpected || HttpStatusCode.OK_200,
285 redirects: 0
286 })
287}
288
289export { 16export {
290 listPlugins, 17 testHelloWorldRegisteredSettings
291 listAvailablePlugins,
292 installPlugin,
293 getPluginTranslations,
294 getPluginsCSS,
295 updatePlugin,
296 getPlugin,
297 uninstallPlugin,
298 testHelloWorldRegisteredSettings,
299 updatePluginSettings,
300 getPluginRegisteredSettings,
301 getPackageJSONPath,
302 updatePluginPackageJSON,
303 getPluginPackageJSON,
304 getPluginTestPath,
305 getPublicSettings,
306 getExternalAuth
307} 18}
diff --git a/shared/extra-utils/server/redundancy-command.ts b/shared/extra-utils/server/redundancy-command.ts
new file mode 100644
index 000000000..e7a8b3c29
--- /dev/null
+++ b/shared/extra-utils/server/redundancy-command.ts
@@ -0,0 +1,80 @@
1import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class RedundancyCommand extends AbstractCommand {
5
6 updateRedundancy (options: OverrideCommandOptions & {
7 host: string
8 redundancyAllowed: boolean
9 }) {
10 const { host, redundancyAllowed } = options
11 const path = '/api/v1/server/redundancy/' + host
12
13 return this.putBodyRequest({
14 ...options,
15
16 path,
17 fields: { redundancyAllowed },
18 implicitToken: true,
19 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
20 })
21 }
22
23 listVideos (options: OverrideCommandOptions & {
24 target: VideoRedundanciesTarget
25 start?: number
26 count?: number
27 sort?: string
28 }) {
29 const path = '/api/v1/server/redundancy/videos'
30
31 const { target, start, count, sort } = options
32
33 return this.getRequestBody<ResultList<VideoRedundancy>>({
34 ...options,
35
36 path,
37
38 query: {
39 start: start ?? 0,
40 count: count ?? 5,
41 sort: sort ?? 'name',
42 target
43 },
44
45 implicitToken: true,
46 defaultExpectedStatus: HttpStatusCode.OK_200
47 })
48 }
49
50 addVideo (options: OverrideCommandOptions & {
51 videoId: number
52 }) {
53 const path = '/api/v1/server/redundancy/videos'
54 const { videoId } = options
55
56 return this.postBodyRequest({
57 ...options,
58
59 path,
60 fields: { videoId },
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
63 })
64 }
65
66 removeVideo (options: OverrideCommandOptions & {
67 redundancyId: number
68 }) {
69 const { redundancyId } = options
70 const path = '/api/v1/server/redundancy/videos/' + redundancyId
71
72 return this.deleteRequest({
73 ...options,
74
75 path,
76 implicitToken: true,
77 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
78 })
79 }
80}
diff --git a/shared/extra-utils/server/redundancy.ts b/shared/extra-utils/server/redundancy.ts
deleted file mode 100644
index b83815a37..000000000
--- a/shared/extra-utils/server/redundancy.ts
+++ /dev/null
@@ -1,88 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2import { VideoRedundanciesTarget } from '@shared/models'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4
5function updateRedundancy (
6 url: string,
7 accessToken: string,
8 host: string,
9 redundancyAllowed: boolean,
10 expectedStatus = HttpStatusCode.NO_CONTENT_204
11) {
12 const path = '/api/v1/server/redundancy/' + host
13
14 return makePutBodyRequest({
15 url,
16 path,
17 token: accessToken,
18 fields: { redundancyAllowed },
19 statusCodeExpected: expectedStatus
20 })
21}
22
23function listVideoRedundancies (options: {
24 url: string
25 accessToken: string
26 target: VideoRedundanciesTarget
27 start?: number
28 count?: number
29 sort?: string
30 statusCodeExpected?: HttpStatusCode
31}) {
32 const path = '/api/v1/server/redundancy/videos'
33
34 const { url, accessToken, target, statusCodeExpected, start, count, sort } = options
35
36 return makeGetRequest({
37 url,
38 token: accessToken,
39 path,
40 query: {
41 start: start ?? 0,
42 count: count ?? 5,
43 sort: sort ?? 'name',
44 target
45 },
46 statusCodeExpected: statusCodeExpected || HttpStatusCode.OK_200
47 })
48}
49
50function addVideoRedundancy (options: {
51 url: string
52 accessToken: string
53 videoId: number
54}) {
55 const path = '/api/v1/server/redundancy/videos'
56 const { url, accessToken, videoId } = options
57
58 return makePostBodyRequest({
59 url,
60 token: accessToken,
61 path,
62 fields: { videoId },
63 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
64 })
65}
66
67function removeVideoRedundancy (options: {
68 url: string
69 accessToken: string
70 redundancyId: number
71}) {
72 const { url, accessToken, redundancyId } = options
73 const path = '/api/v1/server/redundancy/videos/' + redundancyId
74
75 return makeDeleteRequest({
76 url,
77 token: accessToken,
78 path,
79 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
80 })
81}
82
83export {
84 updateRedundancy,
85 listVideoRedundancies,
86 addVideoRedundancy,
87 removeVideoRedundancy
88}
diff --git a/shared/extra-utils/server/server.ts b/shared/extra-utils/server/server.ts
new file mode 100644
index 000000000..3c335b8e4
--- /dev/null
+++ b/shared/extra-utils/server/server.ts
@@ -0,0 +1,369 @@
1import { ChildProcess, fork } from 'child_process'
2import { copy } from 'fs-extra'
3import { join } from 'path'
4import { root } from '@server/helpers/core-utils'
5import { randomInt } from '@shared/core-utils'
6import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '../../models/videos'
7import { BulkCommand } from '../bulk'
8import { CLICommand } from '../cli'
9import { CustomPagesCommand } from '../custom-pages'
10import { FeedCommand } from '../feeds'
11import { LogsCommand } from '../logs'
12import { parallelTests, SQLCommand } from '../miscs'
13import { AbusesCommand } from '../moderation'
14import { OverviewsCommand } from '../overviews'
15import { SearchCommand } from '../search'
16import { SocketIOCommand } from '../socket'
17import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users'
18import {
19 BlacklistCommand,
20 CaptionsCommand,
21 ChangeOwnershipCommand,
22 ChannelsCommand,
23 HistoryCommand,
24 ImportsCommand,
25 LiveCommand,
26 PlaylistsCommand,
27 ServicesCommand,
28 StreamingPlaylistsCommand,
29 VideosCommand
30} from '../videos'
31import { CommentsCommand } from '../videos/comments-command'
32import { ConfigCommand } from './config-command'
33import { ContactFormCommand } from './contact-form-command'
34import { DebugCommand } from './debug-command'
35import { FollowsCommand } from './follows-command'
36import { JobsCommand } from './jobs-command'
37import { PluginsCommand } from './plugins-command'
38import { RedundancyCommand } from './redundancy-command'
39import { ServersCommand } from './servers-command'
40import { StatsCommand } from './stats-command'
41
42export type RunServerOptions = {
43 hideLogs?: boolean
44 nodeArgs?: string[]
45 peertubeArgs?: string[]
46}
47
48export class PeerTubeServer {
49 app?: ChildProcess
50
51 url: string
52 host?: string
53 hostname?: string
54 port?: number
55
56 rtmpPort?: number
57
58 parallel?: boolean
59 internalServerNumber: number
60
61 serverNumber?: number
62 customConfigFile?: string
63
64 store?: {
65 client?: {
66 id?: string
67 secret?: string
68 }
69
70 user?: {
71 username: string
72 password: string
73 email?: string
74 }
75
76 channel?: VideoChannel
77
78 video?: Video
79 videoCreated?: VideoCreateResult
80 videoDetails?: VideoDetails
81
82 videos?: { id: number, uuid: string }[]
83 }
84
85 accessToken?: string
86 refreshToken?: string
87
88 bulk?: BulkCommand
89 cli?: CLICommand
90 customPage?: CustomPagesCommand
91 feed?: FeedCommand
92 logs?: LogsCommand
93 abuses?: AbusesCommand
94 overviews?: OverviewsCommand
95 search?: SearchCommand
96 contactForm?: ContactFormCommand
97 debug?: DebugCommand
98 follows?: FollowsCommand
99 jobs?: JobsCommand
100 plugins?: PluginsCommand
101 redundancy?: RedundancyCommand
102 stats?: StatsCommand
103 config?: ConfigCommand
104 socketIO?: SocketIOCommand
105 accounts?: AccountsCommand
106 blocklist?: BlocklistCommand
107 subscriptions?: SubscriptionsCommand
108 live?: LiveCommand
109 services?: ServicesCommand
110 blacklist?: BlacklistCommand
111 captions?: CaptionsCommand
112 changeOwnership?: ChangeOwnershipCommand
113 playlists?: PlaylistsCommand
114 history?: HistoryCommand
115 imports?: ImportsCommand
116 streamingPlaylists?: StreamingPlaylistsCommand
117 channels?: ChannelsCommand
118 comments?: CommentsCommand
119 sql?: SQLCommand
120 notifications?: NotificationsCommand
121 servers?: ServersCommand
122 login?: LoginCommand
123 users?: UsersCommand
124 videos?: VideosCommand
125
126 constructor (options: { serverNumber: number } | { url: string }) {
127 if ((options as any).url) {
128 this.setUrl((options as any).url)
129 } else {
130 this.setServerNumber((options as any).serverNumber)
131 }
132
133 this.store = {
134 client: {
135 id: null,
136 secret: null
137 },
138 user: {
139 username: null,
140 password: null
141 }
142 }
143
144 this.assignCommands()
145 }
146
147 setServerNumber (serverNumber: number) {
148 this.serverNumber = serverNumber
149
150 this.parallel = parallelTests()
151
152 this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
153 this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
154 this.port = 9000 + this.internalServerNumber
155
156 this.url = `http://localhost:${this.port}`
157 this.host = `localhost:${this.port}`
158 this.hostname = 'localhost'
159 }
160
161 setUrl (url: string) {
162 const parsed = new URL(url)
163
164 this.url = url
165 this.host = parsed.host
166 this.hostname = parsed.hostname
167 this.port = parseInt(parsed.port)
168 }
169
170 async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) {
171 await ServersCommand.flushTests(this.internalServerNumber)
172
173 return this.run(configOverride, options)
174 }
175
176 async run (configOverrideArg?: any, options: RunServerOptions = {}) {
177 // These actions are async so we need to be sure that they have both been done
178 const serverRunString = {
179 'HTTP server listening': false
180 }
181 const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
182 serverRunString[key] = false
183
184 const regexps = {
185 client_id: 'Client id: (.+)',
186 client_secret: 'Client secret: (.+)',
187 user_username: 'Username: (.+)',
188 user_password: 'User password: (.+)'
189 }
190
191 await this.assignCustomConfigFile()
192
193 const configOverride = this.buildConfigOverride()
194
195 if (configOverrideArg !== undefined) {
196 Object.assign(configOverride, configOverrideArg)
197 }
198
199 // Share the environment
200 const env = Object.create(process.env)
201 env['NODE_ENV'] = 'test'
202 env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
203 env['NODE_CONFIG'] = JSON.stringify(configOverride)
204
205 const forkOptions = {
206 silent: true,
207 env,
208 detached: true,
209 execArgv: options.nodeArgs || []
210 }
211
212 return new Promise<void>(res => {
213 const self = this
214
215 this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
216 this.app.stdout.on('data', function onStdout (data) {
217 let dontContinue = false
218
219 // Capture things if we want to
220 for (const key of Object.keys(regexps)) {
221 const regexp = regexps[key]
222 const matches = data.toString().match(regexp)
223 if (matches !== null) {
224 if (key === 'client_id') self.store.client.id = matches[1]
225 else if (key === 'client_secret') self.store.client.secret = matches[1]
226 else if (key === 'user_username') self.store.user.username = matches[1]
227 else if (key === 'user_password') self.store.user.password = matches[1]
228 }
229 }
230
231 // Check if all required sentences are here
232 for (const key of Object.keys(serverRunString)) {
233 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
234 if (serverRunString[key] === false) dontContinue = true
235 }
236
237 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
238 if (dontContinue === true) return
239
240 if (options.hideLogs === false) {
241 console.log(data.toString())
242 } else {
243 self.app.stdout.removeListener('data', onStdout)
244 }
245
246 process.on('exit', () => {
247 try {
248 process.kill(self.app.pid)
249 } catch { /* empty */ }
250 })
251
252 res()
253 })
254 })
255 }
256
257 async kill () {
258 if (!this.app) return
259
260 await this.sql.cleanup()
261
262 process.kill(-this.app.pid)
263
264 this.app = null
265 }
266
267 private randomServer () {
268 const low = 10
269 const high = 10000
270
271 return randomInt(low, high)
272 }
273
274 private randomRTMP () {
275 const low = 1900
276 const high = 2100
277
278 return randomInt(low, high)
279 }
280
281 private async assignCustomConfigFile () {
282 if (this.internalServerNumber === this.serverNumber) return
283
284 const basePath = join(root(), 'config')
285
286 const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
287 await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
288
289 this.customConfigFile = tmpConfigFile
290 }
291
292 private buildConfigOverride () {
293 if (!this.parallel) return {}
294
295 return {
296 listen: {
297 port: this.port
298 },
299 webserver: {
300 port: this.port
301 },
302 database: {
303 suffix: '_test' + this.internalServerNumber
304 },
305 storage: {
306 tmp: `test${this.internalServerNumber}/tmp/`,
307 avatars: `test${this.internalServerNumber}/avatars/`,
308 videos: `test${this.internalServerNumber}/videos/`,
309 streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`,
310 redundancy: `test${this.internalServerNumber}/redundancy/`,
311 logs: `test${this.internalServerNumber}/logs/`,
312 previews: `test${this.internalServerNumber}/previews/`,
313 thumbnails: `test${this.internalServerNumber}/thumbnails/`,
314 torrents: `test${this.internalServerNumber}/torrents/`,
315 captions: `test${this.internalServerNumber}/captions/`,
316 cache: `test${this.internalServerNumber}/cache/`,
317 plugins: `test${this.internalServerNumber}/plugins/`
318 },
319 admin: {
320 email: `admin${this.internalServerNumber}@example.com`
321 },
322 live: {
323 rtmp: {
324 port: this.rtmpPort
325 }
326 }
327 }
328 }
329
330 private assignCommands () {
331 this.bulk = new BulkCommand(this)
332 this.cli = new CLICommand(this)
333 this.customPage = new CustomPagesCommand(this)
334 this.feed = new FeedCommand(this)
335 this.logs = new LogsCommand(this)
336 this.abuses = new AbusesCommand(this)
337 this.overviews = new OverviewsCommand(this)
338 this.search = new SearchCommand(this)
339 this.contactForm = new ContactFormCommand(this)
340 this.debug = new DebugCommand(this)
341 this.follows = new FollowsCommand(this)
342 this.jobs = new JobsCommand(this)
343 this.plugins = new PluginsCommand(this)
344 this.redundancy = new RedundancyCommand(this)
345 this.stats = new StatsCommand(this)
346 this.config = new ConfigCommand(this)
347 this.socketIO = new SocketIOCommand(this)
348 this.accounts = new AccountsCommand(this)
349 this.blocklist = new BlocklistCommand(this)
350 this.subscriptions = new SubscriptionsCommand(this)
351 this.live = new LiveCommand(this)
352 this.services = new ServicesCommand(this)
353 this.blacklist = new BlacklistCommand(this)
354 this.captions = new CaptionsCommand(this)
355 this.changeOwnership = new ChangeOwnershipCommand(this)
356 this.playlists = new PlaylistsCommand(this)
357 this.history = new HistoryCommand(this)
358 this.imports = new ImportsCommand(this)
359 this.streamingPlaylists = new StreamingPlaylistsCommand(this)
360 this.channels = new ChannelsCommand(this)
361 this.comments = new CommentsCommand(this)
362 this.sql = new SQLCommand(this)
363 this.notifications = new NotificationsCommand(this)
364 this.servers = new ServersCommand(this)
365 this.login = new LoginCommand(this)
366 this.users = new UsersCommand(this)
367 this.videos = new VideosCommand(this)
368 }
369}
diff --git a/shared/extra-utils/server/servers-command.ts b/shared/extra-utils/server/servers-command.ts
new file mode 100644
index 000000000..40a11e8d7
--- /dev/null
+++ b/shared/extra-utils/server/servers-command.ts
@@ -0,0 +1,88 @@
1import { exec } from 'child_process'
2import { copy, ensureDir, readFile, remove } from 'fs-extra'
3import { basename, join } from 'path'
4import { root } from '@server/helpers/core-utils'
5import { HttpStatusCode } from '@shared/models'
6import { getFileSize, isGithubCI, wait } from '../miscs'
7import { AbstractCommand, OverrideCommandOptions } from '../shared'
8
9export class ServersCommand extends AbstractCommand {
10
11 static flushTests (internalServerNumber: number) {
12 return new Promise<void>((res, rej) => {
13 const suffix = ` -- ${internalServerNumber}`
14
15 return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
16 if (err || stderr) return rej(err || new Error(stderr))
17
18 return res()
19 })
20 })
21 }
22
23 ping (options: OverrideCommandOptions = {}) {
24 return this.getRequestBody({
25 ...options,
26
27 path: '/api/v1/ping',
28 implicitToken: false,
29 defaultExpectedStatus: HttpStatusCode.OK_200
30 })
31 }
32
33 async cleanupTests () {
34 const p: Promise<any>[] = []
35
36 if (isGithubCI()) {
37 await ensureDir('artifacts')
38
39 const origin = this.buildDirectory('logs/peertube.log')
40 const destname = `peertube-${this.server.internalServerNumber}.log`
41 console.log('Saving logs %s.', destname)
42
43 await copy(origin, join('artifacts', destname))
44 }
45
46 if (this.server.parallel) {
47 p.push(ServersCommand.flushTests(this.server.internalServerNumber))
48 }
49
50 if (this.server.customConfigFile) {
51 p.push(remove(this.server.customConfigFile))
52 }
53
54 return p
55 }
56
57 async waitUntilLog (str: string, count = 1, strictCount = true) {
58 const logfile = this.server.servers.buildDirectory('logs/peertube.log')
59
60 while (true) {
61 const buf = await readFile(logfile)
62
63 const matches = buf.toString().match(new RegExp(str, 'g'))
64 if (matches && matches.length === count) return
65 if (matches && strictCount === false && matches.length >= count) return
66
67 await wait(1000)
68 }
69 }
70
71 buildDirectory (directory: string) {
72 return join(root(), 'test' + this.server.internalServerNumber, directory)
73 }
74
75 buildWebTorrentFilePath (fileUrl: string) {
76 return this.buildDirectory(join('videos', basename(fileUrl)))
77 }
78
79 buildFragmentedFilePath (videoUUID: string, fileUrl: string) {
80 return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
81 }
82
83 async getServerFileSize (subPath: string) {
84 const path = this.server.servers.buildDirectory(subPath)
85
86 return getFileSize(path)
87 }
88}
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
index 28e431e94..f0622feb0 100644
--- a/shared/extra-utils/server/servers.ts
+++ b/shared/extra-utils/server/servers.ts
@@ -1,384 +1,49 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1import { ensureDir } from 'fs-extra'
2import { isGithubCI } from '../miscs'
3import { PeerTubeServer, RunServerOptions } from './server'
2 4
3import { expect } from 'chai' 5async function createSingleServer (serverNumber: number, configOverride?: Object, options: RunServerOptions = {}) {
4import { ChildProcess, exec, fork } from 'child_process' 6 const server = new PeerTubeServer({ serverNumber })
5import { copy, ensureDir, pathExists, readdir, readFile, remove } from 'fs-extra'
6import { join } from 'path'
7import { randomInt } from '../../core-utils/miscs/miscs'
8import { VideoChannel } from '../../models/videos'
9import { buildServerDirectory, getFileSize, isGithubCI, root, wait } from '../miscs/miscs'
10import { makeGetRequest } from '../requests/requests'
11 7
12interface ServerInfo { 8 await server.flushAndRun(configOverride, options)
13 app: ChildProcess
14
15 url: string
16 host: string
17 hostname: string
18 port: number
19
20 rtmpPort: number
21
22 parallel: boolean
23 internalServerNumber: number
24 serverNumber: number
25
26 client: {
27 id: string
28 secret: string
29 }
30
31 user: {
32 username: string
33 password: string
34 email?: string
35 }
36
37 customConfigFile?: string
38
39 accessToken?: string
40 refreshToken?: string
41 videoChannel?: VideoChannel
42
43 video?: {
44 id: number
45 uuid: string
46 shortUUID: string
47 name?: string
48 url?: string
49
50 account?: {
51 name: string
52 }
53
54 embedPath?: string
55 }
56
57 remoteVideo?: {
58 id: number
59 uuid: string
60 }
61
62 videos?: { id: number, uuid: string }[]
63}
64
65function parallelTests () {
66 return process.env.MOCHA_PARALLEL === 'true'
67}
68
69function flushAndRunMultipleServers (totalServers: number, configOverride?: Object) {
70 const apps = []
71 let i = 0
72
73 return new Promise<ServerInfo[]>(res => {
74 function anotherServerDone (serverNumber, app) {
75 apps[serverNumber - 1] = app
76 i++
77 if (i === totalServers) {
78 return res(apps)
79 }
80 }
81
82 for (let j = 1; j <= totalServers; j++) {
83 flushAndRunServer(j, configOverride).then(app => anotherServerDone(j, app))
84 }
85 })
86}
87
88function flushTests (serverNumber?: number) {
89 return new Promise<void>((res, rej) => {
90 const suffix = serverNumber ? ` -- ${serverNumber}` : ''
91
92 return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
93 if (err || stderr) return rej(err || new Error(stderr))
94
95 return res()
96 })
97 })
98}
99
100function randomServer () {
101 const low = 10
102 const high = 10000
103
104 return randomInt(low, high)
105}
106
107function randomRTMP () {
108 const low = 1900
109 const high = 2100
110
111 return randomInt(low, high)
112}
113
114type RunServerOptions = {
115 hideLogs?: boolean
116 execArgv?: string[]
117}
118
119async function flushAndRunServer (serverNumber: number, configOverride?: Object, args = [], options: RunServerOptions = {}) {
120 const parallel = parallelTests()
121
122 const internalServerNumber = parallel ? randomServer() : serverNumber
123 const rtmpPort = parallel ? randomRTMP() : 1936
124 const port = 9000 + internalServerNumber
125
126 await flushTests(internalServerNumber)
127
128 const server: ServerInfo = {
129 app: null,
130 port,
131 internalServerNumber,
132 rtmpPort,
133 parallel,
134 serverNumber,
135 url: `http://localhost:${port}`,
136 host: `localhost:${port}`,
137 hostname: 'localhost',
138 client: {
139 id: null,
140 secret: null
141 },
142 user: {
143 username: null,
144 password: null
145 }
146 }
147
148 return runServer(server, configOverride, args, options)
149}
150
151async function runServer (server: ServerInfo, configOverrideArg?: any, args = [], options: RunServerOptions = {}) {
152 // These actions are async so we need to be sure that they have both been done
153 const serverRunString = {
154 'HTTP server listening': false
155 }
156 const key = 'Database peertube_test' + server.internalServerNumber + ' is ready'
157 serverRunString[key] = false
158
159 const regexps = {
160 client_id: 'Client id: (.+)',
161 client_secret: 'Client secret: (.+)',
162 user_username: 'Username: (.+)',
163 user_password: 'User password: (.+)'
164 }
165
166 if (server.internalServerNumber !== server.serverNumber) {
167 const basePath = join(root(), 'config')
168
169 const tmpConfigFile = join(basePath, `test-${server.internalServerNumber}.yaml`)
170 await copy(join(basePath, `test-${server.serverNumber}.yaml`), tmpConfigFile)
171
172 server.customConfigFile = tmpConfigFile
173 }
174
175 const configOverride: any = {}
176
177 if (server.parallel) {
178 Object.assign(configOverride, {
179 listen: {
180 port: server.port
181 },
182 webserver: {
183 port: server.port
184 },
185 database: {
186 suffix: '_test' + server.internalServerNumber
187 },
188 storage: {
189 tmp: `test${server.internalServerNumber}/tmp/`,
190 avatars: `test${server.internalServerNumber}/avatars/`,
191 videos: `test${server.internalServerNumber}/videos/`,
192 streaming_playlists: `test${server.internalServerNumber}/streaming-playlists/`,
193 redundancy: `test${server.internalServerNumber}/redundancy/`,
194 logs: `test${server.internalServerNumber}/logs/`,
195 previews: `test${server.internalServerNumber}/previews/`,
196 thumbnails: `test${server.internalServerNumber}/thumbnails/`,
197 torrents: `test${server.internalServerNumber}/torrents/`,
198 captions: `test${server.internalServerNumber}/captions/`,
199 cache: `test${server.internalServerNumber}/cache/`,
200 plugins: `test${server.internalServerNumber}/plugins/`
201 },
202 admin: {
203 email: `admin${server.internalServerNumber}@example.com`
204 },
205 live: {
206 rtmp: {
207 port: server.rtmpPort
208 }
209 }
210 })
211 }
212
213 if (configOverrideArg !== undefined) {
214 Object.assign(configOverride, configOverrideArg)
215 }
216
217 // Share the environment
218 const env = Object.create(process.env)
219 env['NODE_ENV'] = 'test'
220 env['NODE_APP_INSTANCE'] = server.internalServerNumber.toString()
221 env['NODE_CONFIG'] = JSON.stringify(configOverride)
222
223 const forkOptions = {
224 silent: true,
225 env,
226 detached: true,
227 execArgv: options.execArgv || []
228 }
229
230 return new Promise<ServerInfo>(res => {
231 server.app = fork(join(root(), 'dist', 'server.js'), args, forkOptions)
232 server.app.stdout.on('data', function onStdout (data) {
233 let dontContinue = false
234
235 // Capture things if we want to
236 for (const key of Object.keys(regexps)) {
237 const regexp = regexps[key]
238 const matches = data.toString().match(regexp)
239 if (matches !== null) {
240 if (key === 'client_id') server.client.id = matches[1]
241 else if (key === 'client_secret') server.client.secret = matches[1]
242 else if (key === 'user_username') server.user.username = matches[1]
243 else if (key === 'user_password') server.user.password = matches[1]
244 }
245 }
246
247 // Check if all required sentences are here
248 for (const key of Object.keys(serverRunString)) {
249 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
250 if (serverRunString[key] === false) dontContinue = true
251 }
252
253 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
254 if (dontContinue === true) return
255
256 if (options.hideLogs === false) {
257 console.log(data.toString())
258 } else {
259 server.app.stdout.removeListener('data', onStdout)
260 }
261
262 process.on('exit', () => {
263 try {
264 process.kill(server.app.pid)
265 } catch { /* empty */ }
266 })
267
268 res(server)
269 })
270 })
271}
272
273async function reRunServer (server: ServerInfo, configOverride?: any) {
274 const newServer = await runServer(server, configOverride)
275 server.app = newServer.app
276 9
277 return server 10 return server
278} 11}
279 12
280async function checkTmpIsEmpty (server: ServerInfo) { 13function createMultipleServers (totalServers: number, configOverride?: Object) {
281 await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ]) 14 const serverPromises: Promise<PeerTubeServer>[] = []
282 15
283 if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) { 16 for (let i = 1; i <= totalServers; i++) {
284 await checkDirectoryIsEmpty(server, 'tmp/hls') 17 serverPromises.push(createSingleServer(i, configOverride))
285 } 18 }
286}
287
288async function checkDirectoryIsEmpty (server: ServerInfo, directory: string, exceptions: string[] = []) {
289 const testDirectory = 'test' + server.internalServerNumber
290
291 const directoryPath = join(root(), testDirectory, directory)
292 19
293 const directoryExists = await pathExists(directoryPath) 20 return Promise.all(serverPromises)
294 expect(directoryExists).to.be.true
295
296 const files = await readdir(directoryPath)
297 const filtered = files.filter(f => exceptions.includes(f) === false)
298
299 expect(filtered).to.have.lengthOf(0)
300} 21}
301 22
302function killallServers (servers: ServerInfo[]) { 23async function killallServers (servers: PeerTubeServer[]) {
303 for (const server of servers) { 24 return Promise.all(servers.map(s => s.kill()))
304 if (!server.app) continue
305
306 process.kill(-server.app.pid)
307 server.app = null
308 }
309} 25}
310 26
311async function cleanupTests (servers: ServerInfo[]) { 27async function cleanupTests (servers: PeerTubeServer[]) {
312 killallServers(servers) 28 await killallServers(servers)
313 29
314 if (isGithubCI()) { 30 if (isGithubCI()) {
315 await ensureDir('artifacts') 31 await ensureDir('artifacts')
316 } 32 }
317 33
318 const p: Promise<any>[] = [] 34 let p: Promise<any>[] = []
319 for (const server of servers) { 35 for (const server of servers) {
320 if (isGithubCI()) { 36 p = p.concat(server.servers.cleanupTests())
321 const origin = await buildServerDirectory(server, 'logs/peertube.log')
322 const destname = `peertube-${server.internalServerNumber}.log`
323 console.log('Saving logs %s.', destname)
324
325 await copy(origin, join('artifacts', destname))
326 }
327
328 if (server.parallel) {
329 p.push(flushTests(server.internalServerNumber))
330 }
331
332 if (server.customConfigFile) {
333 p.push(remove(server.customConfigFile))
334 }
335 } 37 }
336 38
337 return Promise.all(p) 39 return Promise.all(p)
338} 40}
339 41
340async function waitUntilLog (server: ServerInfo, str: string, count = 1, strictCount = true) {
341 const logfile = buildServerDirectory(server, 'logs/peertube.log')
342
343 while (true) {
344 const buf = await readFile(logfile)
345
346 const matches = buf.toString().match(new RegExp(str, 'g'))
347 if (matches && matches.length === count) return
348 if (matches && strictCount === false && matches.length >= count) return
349
350 await wait(1000)
351 }
352}
353
354async function getServerFileSize (server: ServerInfo, subPath: string) {
355 const path = buildServerDirectory(server, subPath)
356
357 return getFileSize(path)
358}
359
360function makePingRequest (server: ServerInfo) {
361 return makeGetRequest({
362 url: server.url,
363 path: '/api/v1/ping',
364 statusCodeExpected: 200
365 })
366}
367
368// --------------------------------------------------------------------------- 42// ---------------------------------------------------------------------------
369 43
370export { 44export {
371 checkDirectoryIsEmpty, 45 createSingleServer,
372 checkTmpIsEmpty, 46 createMultipleServers,
373 getServerFileSize,
374 ServerInfo,
375 parallelTests,
376 cleanupTests, 47 cleanupTests,
377 flushAndRunMultipleServers, 48 killallServers
378 flushTests,
379 makePingRequest,
380 flushAndRunServer,
381 killallServers,
382 reRunServer,
383 waitUntilLog
384} 49}
diff --git a/shared/extra-utils/server/stats-command.ts b/shared/extra-utils/server/stats-command.ts
new file mode 100644
index 000000000..64a452306
--- /dev/null
+++ b/shared/extra-utils/server/stats-command.ts
@@ -0,0 +1,25 @@
1import { HttpStatusCode, ServerStats } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class StatsCommand extends AbstractCommand {
5
6 get (options: OverrideCommandOptions & {
7 useCache?: boolean // default false
8 } = {}) {
9 const { useCache = false } = options
10 const path = '/api/v1/server/stats'
11
12 const query = {
13 t: useCache ? undefined : new Date().getTime()
14 }
15
16 return this.getRequestBody<ServerStats>({
17 ...options,
18
19 path,
20 query,
21 implicitToken: false,
22 defaultExpectedStatus: HttpStatusCode.OK_200
23 })
24 }
25}
diff --git a/shared/extra-utils/server/stats.ts b/shared/extra-utils/server/stats.ts
deleted file mode 100644
index b9dae24e2..000000000
--- a/shared/extra-utils/server/stats.ts
+++ /dev/null
@@ -1,23 +0,0 @@
1import { makeGetRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function getStats (url: string, useCache = false) {
5 const path = '/api/v1/server/stats'
6
7 const query = {
8 t: useCache ? undefined : new Date().getTime()
9 }
10
11 return makeGetRequest({
12 url,
13 path,
14 query,
15 statusCodeExpected: HttpStatusCode.OK_200
16 })
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 getStats
23}
diff --git a/shared/extra-utils/shared/abstract-command.ts b/shared/extra-utils/shared/abstract-command.ts
new file mode 100644
index 000000000..021045e49
--- /dev/null
+++ b/shared/extra-utils/shared/abstract-command.ts
@@ -0,0 +1,199 @@
1import { isAbsolute, join } from 'path'
2import { root } from '../miscs/tests'
3import {
4 makeDeleteRequest,
5 makeGetRequest,
6 makePostBodyRequest,
7 makePutBodyRequest,
8 makeUploadRequest,
9 unwrapBody,
10 unwrapText
11} from '../requests/requests'
12import { PeerTubeServer } from '../server/server'
13
14export interface OverrideCommandOptions {
15 token?: string
16 expectedStatus?: number
17}
18
19interface InternalCommonCommandOptions extends OverrideCommandOptions {
20 // Default to server.url
21 url?: string
22
23 path: string
24 // If we automatically send the server token if the token is not provided
25 implicitToken: boolean
26 defaultExpectedStatus: number
27
28 // Common optional request parameters
29 contentType?: string
30 accept?: string
31 redirects?: number
32 range?: string
33 host?: string
34 headers?: { [ name: string ]: string }
35 requestType?: string
36 xForwardedFor?: string
37}
38
39interface InternalGetCommandOptions extends InternalCommonCommandOptions {
40 query?: { [ id: string ]: any }
41}
42
43abstract class AbstractCommand {
44
45 constructor (
46 protected server: PeerTubeServer
47 ) {
48
49 }
50
51 protected getRequestBody <T> (options: InternalGetCommandOptions) {
52 return unwrapBody<T>(this.getRequest(options))
53 }
54
55 protected getRequestText (options: InternalGetCommandOptions) {
56 return unwrapText(this.getRequest(options))
57 }
58
59 protected getRawRequest (options: Omit<InternalGetCommandOptions, 'path'>) {
60 const { url, range } = options
61 const { host, protocol, pathname } = new URL(url)
62
63 return this.getRequest({
64 ...options,
65
66 token: this.buildCommonRequestToken(options),
67 defaultExpectedStatus: this.buildExpectedStatus(options),
68
69 url: `${protocol}//${host}`,
70 path: pathname,
71 range
72 })
73 }
74
75 protected getRequest (options: InternalGetCommandOptions) {
76 const { query } = options
77
78 return makeGetRequest({
79 ...this.buildCommonRequestOptions(options),
80
81 query
82 })
83 }
84
85 protected deleteRequest (options: InternalCommonCommandOptions) {
86 return makeDeleteRequest(this.buildCommonRequestOptions(options))
87 }
88
89 protected putBodyRequest (options: InternalCommonCommandOptions & {
90 fields?: { [ fieldName: string ]: any }
91 }) {
92 const { fields } = options
93
94 return makePutBodyRequest({
95 ...this.buildCommonRequestOptions(options),
96
97 fields
98 })
99 }
100
101 protected postBodyRequest (options: InternalCommonCommandOptions & {
102 fields?: { [ fieldName: string ]: any }
103 }) {
104 const { fields } = options
105
106 return makePostBodyRequest({
107 ...this.buildCommonRequestOptions(options),
108
109 fields
110 })
111 }
112
113 protected postUploadRequest (options: InternalCommonCommandOptions & {
114 fields?: { [ fieldName: string ]: any }
115 attaches?: { [ fieldName: string ]: any }
116 }) {
117 const { fields, attaches } = options
118
119 return makeUploadRequest({
120 ...this.buildCommonRequestOptions(options),
121
122 method: 'POST',
123 fields,
124 attaches
125 })
126 }
127
128 protected putUploadRequest (options: InternalCommonCommandOptions & {
129 fields?: { [ fieldName: string ]: any }
130 attaches?: { [ fieldName: string ]: any }
131 }) {
132 const { fields, attaches } = options
133
134 return makeUploadRequest({
135 ...this.buildCommonRequestOptions(options),
136
137 method: 'PUT',
138 fields,
139 attaches
140 })
141 }
142
143 protected updateImageRequest (options: InternalCommonCommandOptions & {
144 fixture: string
145 fieldname: string
146 }) {
147 const filePath = isAbsolute(options.fixture)
148 ? options.fixture
149 : join(root(), 'server', 'tests', 'fixtures', options.fixture)
150
151 return this.postUploadRequest({
152 ...options,
153
154 fields: {},
155 attaches: { [options.fieldname]: filePath }
156 })
157 }
158
159 protected buildCommonRequestOptions (options: InternalCommonCommandOptions) {
160 const { url, path, redirects, contentType, accept, range, host, headers, requestType, xForwardedFor } = options
161
162 return {
163 url: url ?? this.server.url,
164 path,
165
166 token: this.buildCommonRequestToken(options),
167 expectedStatus: this.buildExpectedStatus(options),
168
169 redirects,
170 contentType,
171 range,
172 host,
173 accept,
174 headers,
175 type: requestType,
176 xForwardedFor
177 }
178 }
179
180 protected buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
181 const { token } = options
182
183 const fallbackToken = options.implicitToken
184 ? this.server.accessToken
185 : undefined
186
187 return token !== undefined ? token : fallbackToken
188 }
189
190 protected buildExpectedStatus (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
191 const { expectedStatus, defaultExpectedStatus } = options
192
193 return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus
194 }
195}
196
197export {
198 AbstractCommand
199}
diff --git a/shared/extra-utils/shared/index.ts b/shared/extra-utils/shared/index.ts
new file mode 100644
index 000000000..e807ab4f7
--- /dev/null
+++ b/shared/extra-utils/shared/index.ts
@@ -0,0 +1 @@
export * from './abstract-command'
diff --git a/shared/extra-utils/socket/index.ts b/shared/extra-utils/socket/index.ts
new file mode 100644
index 000000000..594329b2f
--- /dev/null
+++ b/shared/extra-utils/socket/index.ts
@@ -0,0 +1 @@
export * from './socket-io-command'
diff --git a/shared/extra-utils/socket/socket-io-command.ts b/shared/extra-utils/socket/socket-io-command.ts
new file mode 100644
index 000000000..c277ead28
--- /dev/null
+++ b/shared/extra-utils/socket/socket-io-command.ts
@@ -0,0 +1,15 @@
1import { io } from 'socket.io-client'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class SocketIOCommand extends AbstractCommand {
5
6 getUserNotificationSocket (options: OverrideCommandOptions = {}) {
7 return io(this.server.url + '/user-notifications', {
8 query: { accessToken: options.token ?? this.server.accessToken }
9 })
10 }
11
12 getLiveNotificationSocket () {
13 return io(this.server.url + '/live-videos')
14 }
15}
diff --git a/shared/extra-utils/socket/socket-io.ts b/shared/extra-utils/socket/socket-io.ts
deleted file mode 100644
index 4ca93f453..000000000
--- a/shared/extra-utils/socket/socket-io.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1import { io } from 'socket.io-client'
2
3function getUserNotificationSocket (serverUrl: string, accessToken: string) {
4 return io(serverUrl + '/user-notifications', {
5 query: { accessToken }
6 })
7}
8
9function getLiveNotificationSocket (serverUrl: string) {
10 return io(serverUrl + '/live-videos')
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 getUserNotificationSocket,
17 getLiveNotificationSocket
18}
diff --git a/shared/extra-utils/users/accounts-command.ts b/shared/extra-utils/users/accounts-command.ts
new file mode 100644
index 000000000..2f586104e
--- /dev/null
+++ b/shared/extra-utils/users/accounts-command.ts
@@ -0,0 +1,56 @@
1import { HttpStatusCode, ResultList } from '@shared/models'
2import { Account } from '../../models/actors'
3import { AccountVideoRate, VideoRateType } from '../../models/videos'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class AccountsCommand extends AbstractCommand {
7
8 list (options: OverrideCommandOptions & {
9 sort?: string // default -createdAt
10 } = {}) {
11 const { sort = '-createdAt' } = options
12 const path = '/api/v1/accounts'
13
14 return this.getRequestBody<ResultList<Account>>({
15 ...options,
16
17 path,
18 query: { sort },
19 implicitToken: false,
20 defaultExpectedStatus: HttpStatusCode.OK_200
21 })
22 }
23
24 get (options: OverrideCommandOptions & {
25 accountName: string
26 }) {
27 const path = '/api/v1/accounts/' + options.accountName
28
29 return this.getRequestBody<Account>({
30 ...options,
31
32 path,
33 implicitToken: false,
34 defaultExpectedStatus: HttpStatusCode.OK_200
35 })
36 }
37
38 listRatings (options: OverrideCommandOptions & {
39 accountName: string
40 rating?: VideoRateType
41 }) {
42 const { rating, accountName } = options
43 const path = '/api/v1/accounts/' + accountName + '/ratings'
44
45 const query = { rating }
46
47 return this.getRequestBody<ResultList<AccountVideoRate>>({
48 ...options,
49
50 path,
51 query,
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56}
diff --git a/shared/extra-utils/users/accounts.ts b/shared/extra-utils/users/accounts.ts
deleted file mode 100644
index 4ea7f1402..000000000
--- a/shared/extra-utils/users/accounts.ts
+++ /dev/null
@@ -1,87 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import * as request from 'supertest'
4import { expect } from 'chai'
5import { existsSync, readdir } from 'fs-extra'
6import { join } from 'path'
7import { Account } from '../../models/actors'
8import { root } from '../miscs/miscs'
9import { makeGetRequest } from '../requests/requests'
10import { VideoRateType } from '../../models/videos'
11import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
12
13function getAccountsList (url: string, sort = '-createdAt', statusCodeExpected = HttpStatusCode.OK_200) {
14 const path = '/api/v1/accounts'
15
16 return makeGetRequest({
17 url,
18 query: { sort },
19 path,
20 statusCodeExpected
21 })
22}
23
24function getAccount (url: string, accountName: string, statusCodeExpected = HttpStatusCode.OK_200) {
25 const path = '/api/v1/accounts/' + accountName
26
27 return makeGetRequest({
28 url,
29 path,
30 statusCodeExpected
31 })
32}
33
34async function expectAccountFollows (url: string, nameWithDomain: string, followersCount: number, followingCount: number) {
35 const res = await getAccountsList(url)
36 const account = res.body.data.find((a: Account) => a.name + '@' + a.host === nameWithDomain)
37
38 const message = `${nameWithDomain} on ${url}`
39 expect(account.followersCount).to.equal(followersCount, message)
40 expect(account.followingCount).to.equal(followingCount, message)
41}
42
43async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
44 const testDirectory = 'test' + serverNumber
45
46 for (const directory of [ 'avatars' ]) {
47 const directoryPath = join(root(), testDirectory, directory)
48
49 const directoryExists = existsSync(directoryPath)
50 expect(directoryExists).to.be.true
51
52 const files = await readdir(directoryPath)
53 for (const file of files) {
54 expect(file).to.not.contain(filename)
55 }
56 }
57}
58
59function getAccountRatings (
60 url: string,
61 accountName: string,
62 accessToken: string,
63 rating?: VideoRateType,
64 statusCodeExpected = HttpStatusCode.OK_200
65) {
66 const path = '/api/v1/accounts/' + accountName + '/ratings'
67
68 const query = rating ? { rating } : {}
69
70 return request(url)
71 .get(path)
72 .query(query)
73 .set('Accept', 'application/json')
74 .set('Authorization', 'Bearer ' + accessToken)
75 .expect(statusCodeExpected)
76 .expect('Content-Type', /json/)
77}
78
79// ---------------------------------------------------------------------------
80
81export {
82 getAccount,
83 expectAccountFollows,
84 getAccountsList,
85 checkActorFilesWereRemoved,
86 getAccountRatings
87}
diff --git a/shared/extra-utils/users/actors.ts b/shared/extra-utils/users/actors.ts
new file mode 100644
index 000000000..cfcc7d0a7
--- /dev/null
+++ b/shared/extra-utils/users/actors.ts
@@ -0,0 +1,73 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { join } from 'path'
6import { root } from '@server/helpers/core-utils'
7import { Account, VideoChannel } from '@shared/models'
8import { PeerTubeServer } from '../server'
9
10async function expectChannelsFollows (options: {
11 server: PeerTubeServer
12 handle: string
13 followers: number
14 following: number
15}) {
16 const { server } = options
17 const { data } = await server.channels.list()
18
19 return expectActorFollow({ ...options, data })
20}
21
22async function expectAccountFollows (options: {
23 server: PeerTubeServer
24 handle: string
25 followers: number
26 following: number
27}) {
28 const { server } = options
29 const { data } = await server.accounts.list()
30
31 return expectActorFollow({ ...options, data })
32}
33
34async function checkActorFilesWereRemoved (filename: string, serverNumber: number) {
35 const testDirectory = 'test' + serverNumber
36
37 for (const directory of [ 'avatars' ]) {
38 const directoryPath = join(root(), testDirectory, directory)
39
40 const directoryExists = await pathExists(directoryPath)
41 expect(directoryExists).to.be.true
42
43 const files = await readdir(directoryPath)
44 for (const file of files) {
45 expect(file).to.not.contain(filename)
46 }
47 }
48}
49
50export {
51 expectAccountFollows,
52 expectChannelsFollows,
53 checkActorFilesWereRemoved
54}
55
56// ---------------------------------------------------------------------------
57
58function expectActorFollow (options: {
59 server: PeerTubeServer
60 data: (Account | VideoChannel)[]
61 handle: string
62 followers: number
63 following: number
64}) {
65 const { server, data, handle, followers, following } = options
66
67 const actor = data.find(a => a.name + '@' + a.host === handle)
68 const message = `${handle} on ${server.url}`
69
70 expect(actor, message).to.exist
71 expect(actor.followersCount).to.equal(followers, message)
72 expect(actor.followingCount).to.equal(following, message)
73}
diff --git a/shared/extra-utils/users/blocklist-command.ts b/shared/extra-utils/users/blocklist-command.ts
new file mode 100644
index 000000000..14491a1ae
--- /dev/null
+++ b/shared/extra-utils/users/blocklist-command.ts
@@ -0,0 +1,139 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { AccountBlock, HttpStatusCode, ResultList, ServerBlock } from '@shared/models'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6type ListBlocklistOptions = OverrideCommandOptions & {
7 start: number
8 count: number
9 sort: string // default -createdAt
10}
11
12export class BlocklistCommand extends AbstractCommand {
13
14 listMyAccountBlocklist (options: ListBlocklistOptions) {
15 const path = '/api/v1/users/me/blocklist/accounts'
16
17 return this.listBlocklist<AccountBlock>(options, path)
18 }
19
20 listMyServerBlocklist (options: ListBlocklistOptions) {
21 const path = '/api/v1/users/me/blocklist/servers'
22
23 return this.listBlocklist<ServerBlock>(options, path)
24 }
25
26 listServerAccountBlocklist (options: ListBlocklistOptions) {
27 const path = '/api/v1/server/blocklist/accounts'
28
29 return this.listBlocklist<AccountBlock>(options, path)
30 }
31
32 listServerServerBlocklist (options: ListBlocklistOptions) {
33 const path = '/api/v1/server/blocklist/servers'
34
35 return this.listBlocklist<ServerBlock>(options, path)
36 }
37
38 // ---------------------------------------------------------------------------
39
40 addToMyBlocklist (options: OverrideCommandOptions & {
41 account?: string
42 server?: string
43 }) {
44 const { account, server } = options
45
46 const path = account
47 ? '/api/v1/users/me/blocklist/accounts'
48 : '/api/v1/users/me/blocklist/servers'
49
50 return this.postBodyRequest({
51 ...options,
52
53 path,
54 fields: {
55 accountName: account,
56 host: server
57 },
58 implicitToken: true,
59 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
60 })
61 }
62
63 addToServerBlocklist (options: OverrideCommandOptions & {
64 account?: string
65 server?: string
66 }) {
67 const { account, server } = options
68
69 const path = account
70 ? '/api/v1/server/blocklist/accounts'
71 : '/api/v1/server/blocklist/servers'
72
73 return this.postBodyRequest({
74 ...options,
75
76 path,
77 fields: {
78 accountName: account,
79 host: server
80 },
81 implicitToken: true,
82 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
83 })
84 }
85
86 // ---------------------------------------------------------------------------
87
88 removeFromMyBlocklist (options: OverrideCommandOptions & {
89 account?: string
90 server?: string
91 }) {
92 const { account, server } = options
93
94 const path = account
95 ? '/api/v1/users/me/blocklist/accounts/' + account
96 : '/api/v1/users/me/blocklist/servers/' + server
97
98 return this.deleteRequest({
99 ...options,
100
101 path,
102 implicitToken: true,
103 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
104 })
105 }
106
107 removeFromServerBlocklist (options: OverrideCommandOptions & {
108 account?: string
109 server?: string
110 }) {
111 const { account, server } = options
112
113 const path = account
114 ? '/api/v1/server/blocklist/accounts/' + account
115 : '/api/v1/server/blocklist/servers/' + server
116
117 return this.deleteRequest({
118 ...options,
119
120 path,
121 implicitToken: true,
122 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
123 })
124 }
125
126 private listBlocklist <T> (options: ListBlocklistOptions, path: string) {
127 const { start, count, sort = '-createdAt' } = options
128
129 return this.getRequestBody<ResultList<T>>({
130 ...options,
131
132 path,
133 query: { start, count, sort },
134 implicitToken: true,
135 defaultExpectedStatus: HttpStatusCode.OK_200
136 })
137 }
138
139}
diff --git a/shared/extra-utils/users/blocklist.ts b/shared/extra-utils/users/blocklist.ts
deleted file mode 100644
index bdf7ee58a..000000000
--- a/shared/extra-utils/users/blocklist.ts
+++ /dev/null
@@ -1,238 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { makeGetRequest, makeDeleteRequest, makePostBodyRequest } from '../requests/requests'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function getAccountBlocklistByAccount (
7 url: string,
8 token: string,
9 start: number,
10 count: number,
11 sort = '-createdAt',
12 statusCodeExpected = HttpStatusCode.OK_200
13) {
14 const path = '/api/v1/users/me/blocklist/accounts'
15
16 return makeGetRequest({
17 url,
18 token,
19 query: { start, count, sort },
20 path,
21 statusCodeExpected
22 })
23}
24
25function addAccountToAccountBlocklist (
26 url: string,
27 token: string,
28 accountToBlock: string,
29 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
30) {
31 const path = '/api/v1/users/me/blocklist/accounts'
32
33 return makePostBodyRequest({
34 url,
35 path,
36 token,
37 fields: {
38 accountName: accountToBlock
39 },
40 statusCodeExpected
41 })
42}
43
44function removeAccountFromAccountBlocklist (
45 url: string,
46 token: string,
47 accountToUnblock: string,
48 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
49) {
50 const path = '/api/v1/users/me/blocklist/accounts/' + accountToUnblock
51
52 return makeDeleteRequest({
53 url,
54 path,
55 token,
56 statusCodeExpected
57 })
58}
59
60function getServerBlocklistByAccount (
61 url: string,
62 token: string,
63 start: number,
64 count: number,
65 sort = '-createdAt',
66 statusCodeExpected = HttpStatusCode.OK_200
67) {
68 const path = '/api/v1/users/me/blocklist/servers'
69
70 return makeGetRequest({
71 url,
72 token,
73 query: { start, count, sort },
74 path,
75 statusCodeExpected
76 })
77}
78
79function addServerToAccountBlocklist (
80 url: string,
81 token: string,
82 serverToBlock: string,
83 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
84) {
85 const path = '/api/v1/users/me/blocklist/servers'
86
87 return makePostBodyRequest({
88 url,
89 path,
90 token,
91 fields: {
92 host: serverToBlock
93 },
94 statusCodeExpected
95 })
96}
97
98function removeServerFromAccountBlocklist (
99 url: string,
100 token: string,
101 serverToBlock: string,
102 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
103) {
104 const path = '/api/v1/users/me/blocklist/servers/' + serverToBlock
105
106 return makeDeleteRequest({
107 url,
108 path,
109 token,
110 statusCodeExpected
111 })
112}
113
114function getAccountBlocklistByServer (
115 url: string,
116 token: string,
117 start: number,
118 count: number,
119 sort = '-createdAt',
120 statusCodeExpected = HttpStatusCode.OK_200
121) {
122 const path = '/api/v1/server/blocklist/accounts'
123
124 return makeGetRequest({
125 url,
126 token,
127 query: { start, count, sort },
128 path,
129 statusCodeExpected
130 })
131}
132
133function addAccountToServerBlocklist (
134 url: string,
135 token: string,
136 accountToBlock: string,
137 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
138) {
139 const path = '/api/v1/server/blocklist/accounts'
140
141 return makePostBodyRequest({
142 url,
143 path,
144 token,
145 fields: {
146 accountName: accountToBlock
147 },
148 statusCodeExpected
149 })
150}
151
152function removeAccountFromServerBlocklist (
153 url: string,
154 token: string,
155 accountToUnblock: string,
156 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
157) {
158 const path = '/api/v1/server/blocklist/accounts/' + accountToUnblock
159
160 return makeDeleteRequest({
161 url,
162 path,
163 token,
164 statusCodeExpected
165 })
166}
167
168function getServerBlocklistByServer (
169 url: string,
170 token: string,
171 start: number,
172 count: number,
173 sort = '-createdAt',
174 statusCodeExpected = HttpStatusCode.OK_200
175) {
176 const path = '/api/v1/server/blocklist/servers'
177
178 return makeGetRequest({
179 url,
180 token,
181 query: { start, count, sort },
182 path,
183 statusCodeExpected
184 })
185}
186
187function addServerToServerBlocklist (
188 url: string,
189 token: string,
190 serverToBlock: string,
191 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
192) {
193 const path = '/api/v1/server/blocklist/servers'
194
195 return makePostBodyRequest({
196 url,
197 path,
198 token,
199 fields: {
200 host: serverToBlock
201 },
202 statusCodeExpected
203 })
204}
205
206function removeServerFromServerBlocklist (
207 url: string,
208 token: string,
209 serverToBlock: string,
210 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
211) {
212 const path = '/api/v1/server/blocklist/servers/' + serverToBlock
213
214 return makeDeleteRequest({
215 url,
216 path,
217 token,
218 statusCodeExpected
219 })
220}
221
222// ---------------------------------------------------------------------------
223
224export {
225 getAccountBlocklistByAccount,
226 addAccountToAccountBlocklist,
227 removeAccountFromAccountBlocklist,
228 getServerBlocklistByAccount,
229 addServerToAccountBlocklist,
230 removeServerFromAccountBlocklist,
231
232 getAccountBlocklistByServer,
233 addAccountToServerBlocklist,
234 removeAccountFromServerBlocklist,
235 getServerBlocklistByServer,
236 addServerToServerBlocklist,
237 removeServerFromServerBlocklist
238}
diff --git a/shared/extra-utils/users/index.ts b/shared/extra-utils/users/index.ts
new file mode 100644
index 000000000..460a06f70
--- /dev/null
+++ b/shared/extra-utils/users/index.ts
@@ -0,0 +1,9 @@
1export * from './accounts-command'
2export * from './actors'
3export * from './blocklist-command'
4export * from './login'
5export * from './login-command'
6export * from './notifications'
7export * from './notifications-command'
8export * from './subscriptions-command'
9export * from './users-command'
diff --git a/shared/extra-utils/users/login-command.ts b/shared/extra-utils/users/login-command.ts
new file mode 100644
index 000000000..143f72a59
--- /dev/null
+++ b/shared/extra-utils/users/login-command.ts
@@ -0,0 +1,132 @@
1import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models'
2import { unwrapBody } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class LoginCommand extends AbstractCommand {
6
7 login (options: OverrideCommandOptions & {
8 client?: { id?: string, secret?: string }
9 user?: { username: string, password?: string }
10 } = {}) {
11 const { client = this.server.store.client, user = this.server.store.user } = options
12 const path = '/api/v1/users/token'
13
14 const body = {
15 client_id: client.id,
16 client_secret: client.secret,
17 username: user.username,
18 password: user.password ?? 'password',
19 response_type: 'code',
20 grant_type: 'password',
21 scope: 'upload'
22 }
23
24 return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({
25 ...options,
26
27 path,
28 requestType: 'form',
29 fields: body,
30 implicitToken: false,
31 defaultExpectedStatus: HttpStatusCode.OK_200
32 }))
33 }
34
35 getAccessToken (arg1?: { username: string, password?: string }): Promise<string>
36 getAccessToken (arg1: string, password?: string): Promise<string>
37 async getAccessToken (arg1?: { username: string, password?: string } | string, password?: string) {
38 let user: { username: string, password?: string }
39
40 if (!arg1) user = this.server.store.user
41 else if (typeof arg1 === 'object') user = arg1
42 else user = { username: arg1, password }
43
44 try {
45 const body = await this.login({ user })
46
47 return body.access_token
48 } catch (err) {
49 throw new Error(`Cannot authenticate. Please check your username/password. (${err})`)
50 }
51 }
52
53 loginUsingExternalToken (options: OverrideCommandOptions & {
54 username: string
55 externalAuthToken: string
56 }) {
57 const { username, externalAuthToken } = options
58 const path = '/api/v1/users/token'
59
60 const body = {
61 client_id: this.server.store.client.id,
62 client_secret: this.server.store.client.secret,
63 username: username,
64 response_type: 'code',
65 grant_type: 'password',
66 scope: 'upload',
67 externalAuthToken
68 }
69
70 return this.postBodyRequest({
71 ...options,
72
73 path,
74 requestType: 'form',
75 fields: body,
76 implicitToken: false,
77 defaultExpectedStatus: HttpStatusCode.OK_200
78 })
79 }
80
81 logout (options: OverrideCommandOptions & {
82 token: string
83 }) {
84 const path = '/api/v1/users/revoke-token'
85
86 return unwrapBody<{ redirectUrl: string }>(this.postBodyRequest({
87 ...options,
88
89 path,
90 requestType: 'form',
91 implicitToken: false,
92 defaultExpectedStatus: HttpStatusCode.OK_200
93 }))
94 }
95
96 refreshToken (options: OverrideCommandOptions & {
97 refreshToken: string
98 }) {
99 const path = '/api/v1/users/token'
100
101 const body = {
102 client_id: this.server.store.client.id,
103 client_secret: this.server.store.client.secret,
104 refresh_token: options.refreshToken,
105 response_type: 'code',
106 grant_type: 'refresh_token'
107 }
108
109 return this.postBodyRequest({
110 ...options,
111
112 path,
113 requestType: 'form',
114 fields: body,
115 implicitToken: false,
116 defaultExpectedStatus: HttpStatusCode.OK_200
117 })
118 }
119
120 getClient (options: OverrideCommandOptions = {}) {
121 const path = '/api/v1/oauth-clients/local'
122
123 return this.getRequestBody<{ client_id: string, client_secret: string }>({
124 ...options,
125
126 path,
127 host: this.server.host,
128 implicitToken: false,
129 defaultExpectedStatus: HttpStatusCode.OK_200
130 })
131 }
132}
diff --git a/shared/extra-utils/users/login.ts b/shared/extra-utils/users/login.ts
index 39e1a2747..f1df027d3 100644
--- a/shared/extra-utils/users/login.ts
+++ b/shared/extra-utils/users/login.ts
@@ -1,133 +1,19 @@
1import * as request from 'supertest' 1import { PeerTubeServer } from '../server/server'
2 2
3import { ServerInfo } from '../server/servers' 3function setAccessTokensToServers (servers: PeerTubeServer[]) {
4import { getClient } from '../server/clients'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7type Client = { id: string, secret: string }
8type User = { username: string, password: string }
9type Server = { url: string, client: Client, user: User }
10
11function login (url: string, client: Client, user: User, expectedStatus = HttpStatusCode.OK_200) {
12 const path = '/api/v1/users/token'
13
14 const body = {
15 client_id: client.id,
16 client_secret: client.secret,
17 username: user.username,
18 password: user.password,
19 response_type: 'code',
20 grant_type: 'password',
21 scope: 'upload'
22 }
23
24 return request(url)
25 .post(path)
26 .type('form')
27 .send(body)
28 .expect(expectedStatus)
29}
30
31function logout (url: string, token: string, expectedStatus = HttpStatusCode.OK_200) {
32 const path = '/api/v1/users/revoke-token'
33
34 return request(url)
35 .post(path)
36 .set('Authorization', 'Bearer ' + token)
37 .type('form')
38 .expect(expectedStatus)
39}
40
41async function serverLogin (server: Server) {
42 const res = await login(server.url, server.client, server.user, HttpStatusCode.OK_200)
43
44 return res.body.access_token as string
45}
46
47function refreshToken (server: ServerInfo, refreshToken: string, expectedStatus = HttpStatusCode.OK_200) {
48 const path = '/api/v1/users/token'
49
50 const body = {
51 client_id: server.client.id,
52 client_secret: server.client.secret,
53 refresh_token: refreshToken,
54 response_type: 'code',
55 grant_type: 'refresh_token'
56 }
57
58 return request(server.url)
59 .post(path)
60 .type('form')
61 .send(body)
62 .expect(expectedStatus)
63}
64
65async function userLogin (server: Server, user: User, expectedStatus = HttpStatusCode.OK_200) {
66 const res = await login(server.url, server.client, user, expectedStatus)
67
68 return res.body.access_token as string
69}
70
71async function getAccessToken (url: string, username: string, password: string) {
72 const resClient = await getClient(url)
73 const client = {
74 id: resClient.body.client_id,
75 secret: resClient.body.client_secret
76 }
77
78 const user = { username, password }
79
80 try {
81 const res = await login(url, client, user)
82 return res.body.access_token
83 } catch (err) {
84 throw new Error('Cannot authenticate. Please check your username/password.')
85 }
86}
87
88function setAccessTokensToServers (servers: ServerInfo[]) {
89 const tasks: Promise<any>[] = [] 4 const tasks: Promise<any>[] = []
90 5
91 for (const server of servers) { 6 for (const server of servers) {
92 const p = serverLogin(server).then(t => { server.accessToken = t }) 7 const p = server.login.getAccessToken()
8 .then(t => { server.accessToken = t })
93 tasks.push(p) 9 tasks.push(p)
94 } 10 }
95 11
96 return Promise.all(tasks) 12 return Promise.all(tasks)
97} 13}
98 14
99function loginUsingExternalToken (server: Server, username: string, externalAuthToken: string, expectedStatus = HttpStatusCode.OK_200) {
100 const path = '/api/v1/users/token'
101
102 const body = {
103 client_id: server.client.id,
104 client_secret: server.client.secret,
105 username: username,
106 response_type: 'code',
107 grant_type: 'password',
108 scope: 'upload',
109 externalAuthToken
110 }
111
112 return request(server.url)
113 .post(path)
114 .type('form')
115 .send(body)
116 .expect(expectedStatus)
117}
118
119// --------------------------------------------------------------------------- 15// ---------------------------------------------------------------------------
120 16
121export { 17export {
122 login, 18 setAccessTokensToServers
123 logout,
124 serverLogin,
125 refreshToken,
126 userLogin,
127 getAccessToken,
128 setAccessTokensToServers,
129 Server,
130 Client,
131 User,
132 loginUsingExternalToken
133} 19}
diff --git a/shared/extra-utils/users/notifications-command.ts b/shared/extra-utils/users/notifications-command.ts
new file mode 100644
index 000000000..2d79a3747
--- /dev/null
+++ b/shared/extra-utils/users/notifications-command.ts
@@ -0,0 +1,86 @@
1import { HttpStatusCode, ResultList } from '@shared/models'
2import { UserNotification, UserNotificationSetting } from '../../models/users'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class NotificationsCommand extends AbstractCommand {
6
7 updateMySettings (options: OverrideCommandOptions & {
8 settings: UserNotificationSetting
9 }) {
10 const path = '/api/v1/users/me/notification-settings'
11
12 return this.putBodyRequest({
13 ...options,
14
15 path,
16 fields: options.settings,
17 implicitToken: true,
18 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
19 })
20 }
21
22 list (options: OverrideCommandOptions & {
23 start?: number
24 count?: number
25 unread?: boolean
26 sort?: string
27 }) {
28 const { start, count, unread, sort = '-createdAt' } = options
29 const path = '/api/v1/users/me/notifications'
30
31 return this.getRequestBody<ResultList<UserNotification>>({
32 ...options,
33
34 path,
35 query: {
36 start,
37 count,
38 sort,
39 unread
40 },
41 implicitToken: true,
42 defaultExpectedStatus: HttpStatusCode.OK_200
43 })
44 }
45
46 markAsRead (options: OverrideCommandOptions & {
47 ids: number[]
48 }) {
49 const { ids } = options
50 const path = '/api/v1/users/me/notifications/read'
51
52 return this.postBodyRequest({
53 ...options,
54
55 path,
56 fields: { ids },
57 implicitToken: true,
58 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
59 })
60 }
61
62 markAsReadAll (options: OverrideCommandOptions) {
63 const path = '/api/v1/users/me/notifications/read-all'
64
65 return this.postBodyRequest({
66 ...options,
67
68 path,
69 implicitToken: true,
70 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
71 })
72 }
73
74 async getLastest (options: OverrideCommandOptions = {}) {
75 const { total, data } = await this.list({
76 ...options,
77 start: 0,
78 count: 1,
79 sort: '-createdAt'
80 })
81
82 if (total === 0) return undefined
83
84 return data[0]
85 }
86}
diff --git a/shared/extra-utils/users/user-notifications.ts b/shared/extra-utils/users/notifications.ts
index 844f4442d..7db4bfd3f 100644
--- a/shared/extra-utils/users/user-notifications.ts
+++ b/shared/extra-utils/users/notifications.ts
@@ -3,91 +3,15 @@
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { inspect } from 'util' 4import { inspect } from 'util'
5import { AbuseState, PluginType } from '@shared/models' 5import { AbuseState, PluginType } from '@shared/models'
6import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
7import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' 6import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
8import { MockSmtpServer } from '../miscs/email' 7import { MockSmtpServer } from '../mock-servers/mock-email'
9import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests' 8import { PeerTubeServer } from '../server'
10import { doubleFollow } from '../server/follows' 9import { doubleFollow } from '../server/follows'
11import { flushAndRunMultipleServers, ServerInfo } from '../server/servers' 10import { createMultipleServers } from '../server/servers'
12import { getUserNotificationSocket } from '../socket/socket-io' 11import { setAccessTokensToServers } from './login'
13import { setAccessTokensToServers, userLogin } from './login'
14import { createUser, getMyUserInformation } from './users'
15
16function updateMyNotificationSettings (
17 url: string,
18 token: string,
19 settings: UserNotificationSetting,
20 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
21) {
22 const path = '/api/v1/users/me/notification-settings'
23
24 return makePutBodyRequest({
25 url,
26 path,
27 token,
28 fields: settings,
29 statusCodeExpected
30 })
31}
32
33async function getUserNotifications (
34 url: string,
35 token: string,
36 start: number,
37 count: number,
38 unread?: boolean,
39 sort = '-createdAt',
40 statusCodeExpected = HttpStatusCode.OK_200
41) {
42 const path = '/api/v1/users/me/notifications'
43
44 return makeGetRequest({
45 url,
46 path,
47 token,
48 query: {
49 start,
50 count,
51 sort,
52 unread
53 },
54 statusCodeExpected
55 })
56}
57
58function markAsReadNotifications (url: string, token: string, ids: number[], statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
59 const path = '/api/v1/users/me/notifications/read'
60
61 return makePostBodyRequest({
62 url,
63 path,
64 token,
65 fields: { ids },
66 statusCodeExpected
67 })
68}
69
70function markAsReadAllNotifications (url: string, token: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
71 const path = '/api/v1/users/me/notifications/read-all'
72
73 return makePostBodyRequest({
74 url,
75 path,
76 token,
77 statusCodeExpected
78 })
79}
80
81async function getLastNotification (serverUrl: string, accessToken: string) {
82 const res = await getUserNotifications(serverUrl, accessToken, 0, 1, undefined, '-createdAt')
83
84 if (res.body.total === 0) return undefined
85
86 return res.body.data[0] as UserNotification
87}
88 12
89type CheckerBaseParams = { 13type CheckerBaseParams = {
90 server: ServerInfo 14 server: PeerTubeServer
91 emails: any[] 15 emails: any[]
92 socketNotifications: UserNotification[] 16 socketNotifications: UserNotification[]
93 token: string 17 token: string
@@ -96,91 +20,41 @@ type CheckerBaseParams = {
96 20
97type CheckerType = 'presence' | 'absence' 21type CheckerType = 'presence' | 'absence'
98 22
99async function checkNotification ( 23function getAllNotificationsSettings (): UserNotificationSetting {
100 base: CheckerBaseParams, 24 return {
101 notificationChecker: (notification: UserNotification, type: CheckerType) => void, 25 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
102 emailNotificationFinder: (email: object) => boolean, 26 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
103 checkType: CheckerType 27 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
104) { 28 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
105 const check = base.check || { web: true, mail: true } 29 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
106 30 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
107 if (check.web) { 31 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
108 const notification = await getLastNotification(base.server.url, base.token) 32 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
109 33 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
110 if (notification || checkType !== 'absence') { 34 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
111 notificationChecker(notification, checkType) 35 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
112 } 36 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
113 37 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
114 const socketNotification = base.socketNotifications.find(n => { 38 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
115 try { 39 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
116 notificationChecker(n, 'presence') 40 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
117 return true
118 } catch {
119 return false
120 }
121 })
122
123 if (checkType === 'presence') {
124 const obj = inspect(base.socketNotifications, { depth: 5 })
125 expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
126 } else {
127 const obj = inspect(socketNotification, { depth: 5 })
128 expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
129 }
130 }
131
132 if (check.mail) {
133 // Last email
134 const email = base.emails
135 .slice()
136 .reverse()
137 .find(e => emailNotificationFinder(e))
138
139 if (checkType === 'presence') {
140 const emails = base.emails.map(e => e.text)
141 expect(email, 'The email is absent when is should be present. ' + inspect(emails)).to.not.be.undefined
142 } else {
143 expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
144 }
145 }
146}
147
148function checkVideo (video: any, videoName?: string, videoUUID?: string) {
149 if (videoName) {
150 expect(video.name).to.be.a('string')
151 expect(video.name).to.not.be.empty
152 expect(video.name).to.equal(videoName)
153 }
154
155 if (videoUUID) {
156 expect(video.uuid).to.be.a('string')
157 expect(video.uuid).to.not.be.empty
158 expect(video.uuid).to.equal(videoUUID)
159 } 41 }
160
161 expect(video.id).to.be.a('number')
162} 42}
163 43
164function checkActor (actor: any) { 44async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
165 expect(actor.displayName).to.be.a('string') 45 videoName: string
166 expect(actor.displayName).to.not.be.empty 46 shortUUID: string
167 expect(actor.host).to.not.be.undefined 47 checkType: CheckerType
168} 48}) {
169 49 const { videoName, shortUUID } = options
170function checkComment (comment: any, commentId: number, threadId: number) {
171 expect(comment.id).to.equal(commentId)
172 expect(comment.threadId).to.equal(threadId)
173}
174
175async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) {
176 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION 50 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
177 51
178 function notificationChecker (notification: UserNotification, type: CheckerType) { 52 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
179 if (type === 'presence') { 53 if (checkType === 'presence') {
180 expect(notification).to.not.be.undefined 54 expect(notification).to.not.be.undefined
181 expect(notification.type).to.equal(notificationType) 55 expect(notification.type).to.equal(notificationType)
182 56
183 checkVideo(notification.video, videoName, videoUUID) 57 checkVideo(notification.video, videoName, shortUUID)
184 checkActor(notification.video.channel) 58 checkActor(notification.video.channel)
185 } else { 59 } else {
186 expect(notification).to.satisfy((n: UserNotification) => { 60 expect(notification).to.satisfy((n: UserNotification) => {
@@ -191,21 +65,26 @@ async function checkNewVideoFromSubscription (base: CheckerBaseParams, videoName
191 65
192 function emailNotificationFinder (email: object) { 66 function emailNotificationFinder (email: object) {
193 const text = email['text'] 67 const text = email['text']
194 return text.indexOf(videoUUID) !== -1 && text.indexOf('Your subscription') !== -1 68 return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
195 } 69 }
196 70
197 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 71 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
198} 72}
199 73
200async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string, videoUUID: string, type: CheckerType) { 74async function checkVideoIsPublished (options: CheckerBaseParams & {
75 videoName: string
76 shortUUID: string
77 checkType: CheckerType
78}) {
79 const { videoName, shortUUID } = options
201 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED 80 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
202 81
203 function notificationChecker (notification: UserNotification, type: CheckerType) { 82 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
204 if (type === 'presence') { 83 if (checkType === 'presence') {
205 expect(notification).to.not.be.undefined 84 expect(notification).to.not.be.undefined
206 expect(notification.type).to.equal(notificationType) 85 expect(notification.type).to.equal(notificationType)
207 86
208 checkVideo(notification.video, videoName, videoUUID) 87 checkVideo(notification.video, videoName, shortUUID)
209 checkActor(notification.video.channel) 88 checkActor(notification.video.channel)
210 } else { 89 } else {
211 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) 90 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
@@ -214,30 +93,31 @@ async function checkVideoIsPublished (base: CheckerBaseParams, videoName: string
214 93
215 function emailNotificationFinder (email: object) { 94 function emailNotificationFinder (email: object) {
216 const text: string = email['text'] 95 const text: string = email['text']
217 return text.includes(videoUUID) && text.includes('Your video') 96 return text.includes(shortUUID) && text.includes('Your video')
218 } 97 }
219 98
220 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 99 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
221} 100}
222 101
223async function checkMyVideoImportIsFinished ( 102async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
224 base: CheckerBaseParams, 103 videoName: string
225 videoName: string, 104 shortUUID: string
226 videoUUID: string, 105 url: string
227 url: string, 106 success: boolean
228 success: boolean, 107 checkType: CheckerType
229 type: CheckerType 108}) {
230) { 109 const { videoName, shortUUID, url, success } = options
110
231 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR 111 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
232 112
233 function notificationChecker (notification: UserNotification, type: CheckerType) { 113 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
234 if (type === 'presence') { 114 if (checkType === 'presence') {
235 expect(notification).to.not.be.undefined 115 expect(notification).to.not.be.undefined
236 expect(notification.type).to.equal(notificationType) 116 expect(notification.type).to.equal(notificationType)
237 117
238 expect(notification.videoImport.targetUrl).to.equal(url) 118 expect(notification.videoImport.targetUrl).to.equal(url)
239 119
240 if (success) checkVideo(notification.videoImport.video, videoName, videoUUID) 120 if (success) checkVideo(notification.videoImport.video, videoName, shortUUID)
241 } else { 121 } else {
242 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url) 122 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
243 } 123 }
@@ -250,14 +130,18 @@ async function checkMyVideoImportIsFinished (
250 return text.includes(url) && text.includes(toFind) 130 return text.includes(url) && text.includes(toFind)
251 } 131 }
252 132
253 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 133 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
254} 134}
255 135
256async function checkUserRegistered (base: CheckerBaseParams, username: string, type: CheckerType) { 136async function checkUserRegistered (options: CheckerBaseParams & {
137 username: string
138 checkType: CheckerType
139}) {
140 const { username } = options
257 const notificationType = UserNotificationType.NEW_USER_REGISTRATION 141 const notificationType = UserNotificationType.NEW_USER_REGISTRATION
258 142
259 function notificationChecker (notification: UserNotification, type: CheckerType) { 143 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
260 if (type === 'presence') { 144 if (checkType === 'presence') {
261 expect(notification).to.not.be.undefined 145 expect(notification).to.not.be.undefined
262 expect(notification.type).to.equal(notificationType) 146 expect(notification.type).to.equal(notificationType)
263 147
@@ -274,21 +158,21 @@ async function checkUserRegistered (base: CheckerBaseParams, username: string, t
274 return text.includes(' registered.') && text.includes(username) 158 return text.includes(' registered.') && text.includes(username)
275 } 159 }
276 160
277 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 161 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
278} 162}
279 163
280async function checkNewActorFollow ( 164async function checkNewActorFollow (options: CheckerBaseParams & {
281 base: CheckerBaseParams, 165 followType: 'channel' | 'account'
282 followType: 'channel' | 'account', 166 followerName: string
283 followerName: string, 167 followerDisplayName: string
284 followerDisplayName: string, 168 followingDisplayName: string
285 followingDisplayName: string, 169 checkType: CheckerType
286 type: CheckerType 170}) {
287) { 171 const { followType, followerName, followerDisplayName, followingDisplayName } = options
288 const notificationType = UserNotificationType.NEW_FOLLOW 172 const notificationType = UserNotificationType.NEW_FOLLOW
289 173
290 function notificationChecker (notification: UserNotification, type: CheckerType) { 174 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
291 if (type === 'presence') { 175 if (checkType === 'presence') {
292 expect(notification).to.not.be.undefined 176 expect(notification).to.not.be.undefined
293 expect(notification.type).to.equal(notificationType) 177 expect(notification.type).to.equal(notificationType)
294 178
@@ -314,14 +198,18 @@ async function checkNewActorFollow (
314 return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName) 198 return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
315 } 199 }
316 200
317 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 201 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
318} 202}
319 203
320async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost: string, type: CheckerType) { 204async function checkNewInstanceFollower (options: CheckerBaseParams & {
205 followerHost: string
206 checkType: CheckerType
207}) {
208 const { followerHost } = options
321 const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER 209 const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
322 210
323 function notificationChecker (notification: UserNotification, type: CheckerType) { 211 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
324 if (type === 'presence') { 212 if (checkType === 'presence') {
325 expect(notification).to.not.be.undefined 213 expect(notification).to.not.be.undefined
326 expect(notification.type).to.equal(notificationType) 214 expect(notification.type).to.equal(notificationType)
327 215
@@ -343,14 +231,19 @@ async function checkNewInstanceFollower (base: CheckerBaseParams, followerHost:
343 return text.includes('instance has a new follower') && text.includes(followerHost) 231 return text.includes('instance has a new follower') && text.includes(followerHost)
344 } 232 }
345 233
346 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 234 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
347} 235}
348 236
349async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost: string, followingHost: string, type: CheckerType) { 237async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
238 followerHost: string
239 followingHost: string
240 checkType: CheckerType
241}) {
242 const { followerHost, followingHost } = options
350 const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING 243 const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
351 244
352 function notificationChecker (notification: UserNotification, type: CheckerType) { 245 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
353 if (type === 'presence') { 246 if (checkType === 'presence') {
354 expect(notification).to.not.be.undefined 247 expect(notification).to.not.be.undefined
355 expect(notification.type).to.equal(notificationType) 248 expect(notification.type).to.equal(notificationType)
356 249
@@ -374,21 +267,21 @@ async function checkAutoInstanceFollowing (base: CheckerBaseParams, followerHost
374 return text.includes(' automatically followed a new instance') && text.includes(followingHost) 267 return text.includes(' automatically followed a new instance') && text.includes(followingHost)
375 } 268 }
376 269
377 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 270 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
378} 271}
379 272
380async function checkCommentMention ( 273async function checkCommentMention (options: CheckerBaseParams & {
381 base: CheckerBaseParams, 274 shortUUID: string
382 uuid: string, 275 commentId: number
383 commentId: number, 276 threadId: number
384 threadId: number, 277 byAccountDisplayName: string
385 byAccountDisplayName: string, 278 checkType: CheckerType
386 type: CheckerType 279}) {
387) { 280 const { shortUUID, commentId, threadId, byAccountDisplayName } = options
388 const notificationType = UserNotificationType.COMMENT_MENTION 281 const notificationType = UserNotificationType.COMMENT_MENTION
389 282
390 function notificationChecker (notification: UserNotification, type: CheckerType) { 283 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
391 if (type === 'presence') { 284 if (checkType === 'presence') {
392 expect(notification).to.not.be.undefined 285 expect(notification).to.not.be.undefined
393 expect(notification.type).to.equal(notificationType) 286 expect(notification.type).to.equal(notificationType)
394 287
@@ -396,7 +289,7 @@ async function checkCommentMention (
396 checkActor(notification.comment.account) 289 checkActor(notification.comment.account)
397 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName) 290 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
398 291
399 checkVideo(notification.comment.video, undefined, uuid) 292 checkVideo(notification.comment.video, undefined, shortUUID)
400 } else { 293 } else {
401 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId) 294 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
402 } 295 }
@@ -405,25 +298,31 @@ async function checkCommentMention (
405 function emailNotificationFinder (email: object) { 298 function emailNotificationFinder (email: object) {
406 const text: string = email['text'] 299 const text: string = email['text']
407 300
408 return text.includes(' mentioned ') && text.includes(uuid) && text.includes(byAccountDisplayName) 301 return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName)
409 } 302 }
410 303
411 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 304 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
412} 305}
413 306
414let lastEmailCount = 0 307let lastEmailCount = 0
415 308
416async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string, commentId: number, threadId: number, type: CheckerType) { 309async function checkNewCommentOnMyVideo (options: CheckerBaseParams & {
310 shortUUID: string
311 commentId: number
312 threadId: number
313 checkType: CheckerType
314}) {
315 const { server, shortUUID, commentId, threadId, checkType, emails } = options
417 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO 316 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
418 317
419 function notificationChecker (notification: UserNotification, type: CheckerType) { 318 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
420 if (type === 'presence') { 319 if (checkType === 'presence') {
421 expect(notification).to.not.be.undefined 320 expect(notification).to.not.be.undefined
422 expect(notification.type).to.equal(notificationType) 321 expect(notification.type).to.equal(notificationType)
423 322
424 checkComment(notification.comment, commentId, threadId) 323 checkComment(notification.comment, commentId, threadId)
425 checkActor(notification.comment.account) 324 checkActor(notification.comment.account)
426 checkVideo(notification.comment.video, undefined, uuid) 325 checkVideo(notification.comment.video, undefined, shortUUID)
427 } else { 326 } else {
428 expect(notification).to.satisfy((n: UserNotification) => { 327 expect(notification).to.satisfy((n: UserNotification) => {
429 return n === undefined || n.comment === undefined || n.comment.id !== commentId 328 return n === undefined || n.comment === undefined || n.comment.id !== commentId
@@ -431,51 +330,62 @@ async function checkNewCommentOnMyVideo (base: CheckerBaseParams, uuid: string,
431 } 330 }
432 } 331 }
433 332
434 const commentUrl = `http://localhost:${base.server.port}/w/${uuid};threadId=${threadId}` 333 const commentUrl = `http://localhost:${server.port}/w/${shortUUID};threadId=${threadId}`
435 334
436 function emailNotificationFinder (email: object) { 335 function emailNotificationFinder (email: object) {
437 return email['text'].indexOf(commentUrl) !== -1 336 return email['text'].indexOf(commentUrl) !== -1
438 } 337 }
439 338
440 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 339 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
441 340
442 if (type === 'presence') { 341 if (checkType === 'presence') {
443 // We cannot detect email duplicates, so check we received another email 342 // We cannot detect email duplicates, so check we received another email
444 expect(base.emails).to.have.length.above(lastEmailCount) 343 expect(emails).to.have.length.above(lastEmailCount)
445 lastEmailCount = base.emails.length 344 lastEmailCount = emails.length
446 } 345 }
447} 346}
448 347
449async function checkNewVideoAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { 348async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
349 shortUUID: string
350 videoName: string
351 checkType: CheckerType
352}) {
353 const { shortUUID, videoName } = options
450 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS 354 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
451 355
452 function notificationChecker (notification: UserNotification, type: CheckerType) { 356 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
453 if (type === 'presence') { 357 if (checkType === 'presence') {
454 expect(notification).to.not.be.undefined 358 expect(notification).to.not.be.undefined
455 expect(notification.type).to.equal(notificationType) 359 expect(notification.type).to.equal(notificationType)
456 360
457 expect(notification.abuse.id).to.be.a('number') 361 expect(notification.abuse.id).to.be.a('number')
458 checkVideo(notification.abuse.video, videoName, videoUUID) 362 checkVideo(notification.abuse.video, videoName, shortUUID)
459 } else { 363 } else {
460 expect(notification).to.satisfy((n: UserNotification) => { 364 expect(notification).to.satisfy((n: UserNotification) => {
461 return n === undefined || n.abuse === undefined || n.abuse.video.uuid !== videoUUID 365 return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID
462 }) 366 })
463 } 367 }
464 } 368 }
465 369
466 function emailNotificationFinder (email: object) { 370 function emailNotificationFinder (email: object) {
467 const text = email['text'] 371 const text = email['text']
468 return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1 372 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
469 } 373 }
470 374
471 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 375 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
472} 376}
473 377
474async function checkNewAbuseMessage (base: CheckerBaseParams, abuseId: number, message: string, toEmail: string, type: CheckerType) { 378async function checkNewAbuseMessage (options: CheckerBaseParams & {
379 abuseId: number
380 message: string
381 toEmail: string
382 checkType: CheckerType
383}) {
384 const { abuseId, message, toEmail } = options
475 const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE 385 const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
476 386
477 function notificationChecker (notification: UserNotification, type: CheckerType) { 387 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
478 if (type === 'presence') { 388 if (checkType === 'presence') {
479 expect(notification).to.not.be.undefined 389 expect(notification).to.not.be.undefined
480 expect(notification.type).to.equal(notificationType) 390 expect(notification.type).to.equal(notificationType)
481 391
@@ -494,14 +404,19 @@ async function checkNewAbuseMessage (base: CheckerBaseParams, abuseId: number, m
494 return text.indexOf(message) !== -1 && to.length !== 0 404 return text.indexOf(message) !== -1 && to.length !== 0
495 } 405 }
496 406
497 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 407 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
498} 408}
499 409
500async function checkAbuseStateChange (base: CheckerBaseParams, abuseId: number, state: AbuseState, type: CheckerType) { 410async function checkAbuseStateChange (options: CheckerBaseParams & {
411 abuseId: number
412 state: AbuseState
413 checkType: CheckerType
414}) {
415 const { abuseId, state } = options
501 const notificationType = UserNotificationType.ABUSE_STATE_CHANGE 416 const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
502 417
503 function notificationChecker (notification: UserNotification, type: CheckerType) { 418 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
504 if (type === 'presence') { 419 if (checkType === 'presence') {
505 expect(notification).to.not.be.undefined 420 expect(notification).to.not.be.undefined
506 expect(notification.type).to.equal(notificationType) 421 expect(notification.type).to.equal(notificationType)
507 422
@@ -524,39 +439,48 @@ async function checkAbuseStateChange (base: CheckerBaseParams, abuseId: number,
524 return text.indexOf(contains) !== -1 439 return text.indexOf(contains) !== -1
525 } 440 }
526 441
527 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 442 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
528} 443}
529 444
530async function checkNewCommentAbuseForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { 445async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
446 shortUUID: string
447 videoName: string
448 checkType: CheckerType
449}) {
450 const { shortUUID, videoName } = options
531 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS 451 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
532 452
533 function notificationChecker (notification: UserNotification, type: CheckerType) { 453 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
534 if (type === 'presence') { 454 if (checkType === 'presence') {
535 expect(notification).to.not.be.undefined 455 expect(notification).to.not.be.undefined
536 expect(notification.type).to.equal(notificationType) 456 expect(notification.type).to.equal(notificationType)
537 457
538 expect(notification.abuse.id).to.be.a('number') 458 expect(notification.abuse.id).to.be.a('number')
539 checkVideo(notification.abuse.comment.video, videoName, videoUUID) 459 checkVideo(notification.abuse.comment.video, videoName, shortUUID)
540 } else { 460 } else {
541 expect(notification).to.satisfy((n: UserNotification) => { 461 expect(notification).to.satisfy((n: UserNotification) => {
542 return n === undefined || n.abuse === undefined || n.abuse.comment.video.uuid !== videoUUID 462 return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID
543 }) 463 })
544 } 464 }
545 } 465 }
546 466
547 function emailNotificationFinder (email: object) { 467 function emailNotificationFinder (email: object) {
548 const text = email['text'] 468 const text = email['text']
549 return text.indexOf(videoUUID) !== -1 && text.indexOf('abuse') !== -1 469 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
550 } 470 }
551 471
552 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 472 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
553} 473}
554 474
555async function checkNewAccountAbuseForModerators (base: CheckerBaseParams, displayName: string, type: CheckerType) { 475async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
476 displayName: string
477 checkType: CheckerType
478}) {
479 const { displayName } = options
556 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS 480 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
557 481
558 function notificationChecker (notification: UserNotification, type: CheckerType) { 482 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
559 if (type === 'presence') { 483 if (checkType === 'presence') {
560 expect(notification).to.not.be.undefined 484 expect(notification).to.not.be.undefined
561 expect(notification.type).to.equal(notificationType) 485 expect(notification.type).to.equal(notificationType)
562 486
@@ -574,40 +498,45 @@ async function checkNewAccountAbuseForModerators (base: CheckerBaseParams, displ
574 return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1 498 return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
575 } 499 }
576 500
577 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 501 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
578} 502}
579 503
580async function checkVideoAutoBlacklistForModerators (base: CheckerBaseParams, videoUUID: string, videoName: string, type: CheckerType) { 504async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & {
505 shortUUID: string
506 videoName: string
507 checkType: CheckerType
508}) {
509 const { shortUUID, videoName } = options
581 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS 510 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
582 511
583 function notificationChecker (notification: UserNotification, type: CheckerType) { 512 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
584 if (type === 'presence') { 513 if (checkType === 'presence') {
585 expect(notification).to.not.be.undefined 514 expect(notification).to.not.be.undefined
586 expect(notification.type).to.equal(notificationType) 515 expect(notification.type).to.equal(notificationType)
587 516
588 expect(notification.videoBlacklist.video.id).to.be.a('number') 517 expect(notification.videoBlacklist.video.id).to.be.a('number')
589 checkVideo(notification.videoBlacklist.video, videoName, videoUUID) 518 checkVideo(notification.videoBlacklist.video, videoName, shortUUID)
590 } else { 519 } else {
591 expect(notification).to.satisfy((n: UserNotification) => { 520 expect(notification).to.satisfy((n: UserNotification) => {
592 return n === undefined || n.video === undefined || n.video.uuid !== videoUUID 521 return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID
593 }) 522 })
594 } 523 }
595 } 524 }
596 525
597 function emailNotificationFinder (email: object) { 526 function emailNotificationFinder (email: object) {
598 const text = email['text'] 527 const text = email['text']
599 return text.indexOf(videoUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1 528 return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
600 } 529 }
601 530
602 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 531 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
603} 532}
604 533
605async function checkNewBlacklistOnMyVideo ( 534async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
606 base: CheckerBaseParams, 535 shortUUID: string
607 videoUUID: string, 536 videoName: string
608 videoName: string,
609 blacklistType: 'blacklist' | 'unblacklist' 537 blacklistType: 'blacklist' | 'unblacklist'
610) { 538}) {
539 const { videoName, shortUUID, blacklistType } = options
611 const notificationType = blacklistType === 'blacklist' 540 const notificationType = blacklistType === 'blacklist'
612 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO 541 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
613 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO 542 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
@@ -618,22 +547,30 @@ async function checkNewBlacklistOnMyVideo (
618 547
619 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video 548 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
620 549
621 checkVideo(video, videoName, videoUUID) 550 checkVideo(video, videoName, shortUUID)
622 } 551 }
623 552
624 function emailNotificationFinder (email: object) { 553 function emailNotificationFinder (email: object) {
625 const text = email['text'] 554 const text = email['text']
626 return text.indexOf(videoUUID) !== -1 && text.indexOf(' ' + blacklistType) !== -1 555 const blacklistText = blacklistType === 'blacklist'
556 ? 'blacklisted'
557 : 'unblacklisted'
558
559 return text.includes(shortUUID) && text.includes(blacklistText)
627 } 560 }
628 561
629 await checkNotification(base, notificationChecker, emailNotificationFinder, 'presence') 562 await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
630} 563}
631 564
632async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion: string, type: CheckerType) { 565async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
566 latestVersion: string
567 checkType: CheckerType
568}) {
569 const { latestVersion } = options
633 const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION 570 const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
634 571
635 function notificationChecker (notification: UserNotification, type: CheckerType) { 572 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
636 if (type === 'presence') { 573 if (checkType === 'presence') {
637 expect(notification).to.not.be.undefined 574 expect(notification).to.not.be.undefined
638 expect(notification.type).to.equal(notificationType) 575 expect(notification.type).to.equal(notificationType)
639 576
@@ -652,14 +589,19 @@ async function checkNewPeerTubeVersion (base: CheckerBaseParams, latestVersion:
652 return text.includes(latestVersion) 589 return text.includes(latestVersion)
653 } 590 }
654 591
655 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 592 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
656} 593}
657 594
658async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: PluginType, pluginName: string, type: CheckerType) { 595async function checkNewPluginVersion (options: CheckerBaseParams & {
596 pluginType: PluginType
597 pluginName: string
598 checkType: CheckerType
599}) {
600 const { pluginName, pluginType } = options
659 const notificationType = UserNotificationType.NEW_PLUGIN_VERSION 601 const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
660 602
661 function notificationChecker (notification: UserNotification, type: CheckerType) { 603 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
662 if (type === 'presence') { 604 if (checkType === 'presence') {
663 expect(notification).to.not.be.undefined 605 expect(notification).to.not.be.undefined
664 expect(notification.type).to.equal(notificationType) 606 expect(notification.type).to.equal(notificationType)
665 607
@@ -678,28 +620,7 @@ async function checkNewPluginVersion (base: CheckerBaseParams, pluginType: Plugi
678 return text.includes(pluginName) 620 return text.includes(pluginName)
679 } 621 }
680 622
681 await checkNotification(base, notificationChecker, emailNotificationFinder, type) 623 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
682}
683
684function getAllNotificationsSettings (): UserNotificationSetting {
685 return {
686 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
687 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
688 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
689 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
690 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
691 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
692 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
693 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
694 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
695 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
696 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
697 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
698 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
699 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
700 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
701 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
702 }
703} 624}
704 625
705async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) { 626async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
@@ -719,7 +640,7 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
719 limit: 20 640 limit: 20
720 } 641 }
721 } 642 }
722 const servers = await flushAndRunMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) 643 const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
723 644
724 await setAccessTokensToServers(servers) 645 await setAccessTokensToServers(servers)
725 646
@@ -727,42 +648,33 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
727 await doubleFollow(servers[0], servers[1]) 648 await doubleFollow(servers[0], servers[1])
728 } 649 }
729 650
730 const user = { 651 const user = { username: 'user_1', password: 'super password' }
731 username: 'user_1', 652 await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
732 password: 'super password' 653 const userAccessToken = await servers[0].login.getAccessToken(user)
733 }
734 await createUser({
735 url: servers[0].url,
736 accessToken: servers[0].accessToken,
737 username: user.username,
738 password: user.password,
739 videoQuota: 10 * 1000 * 1000
740 })
741 const userAccessToken = await userLogin(servers[0], user)
742 654
743 await updateMyNotificationSettings(servers[0].url, userAccessToken, getAllNotificationsSettings()) 655 await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
744 await updateMyNotificationSettings(servers[0].url, servers[0].accessToken, getAllNotificationsSettings()) 656 await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
745 657
746 if (serversCount > 1) { 658 if (serversCount > 1) {
747 await updateMyNotificationSettings(servers[1].url, servers[1].accessToken, getAllNotificationsSettings()) 659 await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
748 } 660 }
749 661
750 { 662 {
751 const socket = getUserNotificationSocket(servers[0].url, userAccessToken) 663 const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
752 socket.on('new-notification', n => userNotifications.push(n)) 664 socket.on('new-notification', n => userNotifications.push(n))
753 } 665 }
754 { 666 {
755 const socket = getUserNotificationSocket(servers[0].url, servers[0].accessToken) 667 const socket = servers[0].socketIO.getUserNotificationSocket()
756 socket.on('new-notification', n => adminNotifications.push(n)) 668 socket.on('new-notification', n => adminNotifications.push(n))
757 } 669 }
758 670
759 if (serversCount > 1) { 671 if (serversCount > 1) {
760 const socket = getUserNotificationSocket(servers[1].url, servers[1].accessToken) 672 const socket = servers[1].socketIO.getUserNotificationSocket()
761 socket.on('new-notification', n => adminNotificationsServer2.push(n)) 673 socket.on('new-notification', n => adminNotificationsServer2.push(n))
762 } 674 }
763 675
764 const resChannel = await getMyUserInformation(servers[0].url, servers[0].accessToken) 676 const { videoChannels } = await servers[0].users.getMyInfo()
765 const channelId = resChannel.body.videoChannels[0].id 677 const channelId = videoChannels[0].id
766 678
767 return { 679 return {
768 userNotifications, 680 userNotifications,
@@ -778,11 +690,10 @@ async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: an
778// --------------------------------------------------------------------------- 690// ---------------------------------------------------------------------------
779 691
780export { 692export {
693 getAllNotificationsSettings,
694
781 CheckerBaseParams, 695 CheckerBaseParams,
782 CheckerType, 696 CheckerType,
783 getAllNotificationsSettings,
784 checkNotification,
785 markAsReadAllNotifications,
786 checkMyVideoImportIsFinished, 697 checkMyVideoImportIsFinished,
787 checkUserRegistered, 698 checkUserRegistered,
788 checkAutoInstanceFollowing, 699 checkAutoInstanceFollowing,
@@ -792,14 +703,10 @@ export {
792 checkNewCommentOnMyVideo, 703 checkNewCommentOnMyVideo,
793 checkNewBlacklistOnMyVideo, 704 checkNewBlacklistOnMyVideo,
794 checkCommentMention, 705 checkCommentMention,
795 updateMyNotificationSettings,
796 checkNewVideoAbuseForModerators, 706 checkNewVideoAbuseForModerators,
797 checkVideoAutoBlacklistForModerators, 707 checkVideoAutoBlacklistForModerators,
798 checkNewAbuseMessage, 708 checkNewAbuseMessage,
799 checkAbuseStateChange, 709 checkAbuseStateChange,
800 getUserNotifications,
801 markAsReadNotifications,
802 getLastNotification,
803 checkNewInstanceFollower, 710 checkNewInstanceFollower,
804 prepareNotificationsTest, 711 prepareNotificationsTest,
805 checkNewCommentAbuseForModerators, 712 checkNewCommentAbuseForModerators,
@@ -807,3 +714,82 @@ export {
807 checkNewPeerTubeVersion, 714 checkNewPeerTubeVersion,
808 checkNewPluginVersion 715 checkNewPluginVersion
809} 716}
717
718// ---------------------------------------------------------------------------
719
720async function checkNotification (options: CheckerBaseParams & {
721 notificationChecker: (notification: UserNotification, checkType: CheckerType) => void
722 emailNotificationFinder: (email: object) => boolean
723 checkType: CheckerType
724}) {
725 const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options
726
727 const check = options.check || { web: true, mail: true }
728
729 if (check.web) {
730 const notification = await server.notifications.getLastest({ token: token })
731
732 if (notification || checkType !== 'absence') {
733 notificationChecker(notification, checkType)
734 }
735
736 const socketNotification = socketNotifications.find(n => {
737 try {
738 notificationChecker(n, 'presence')
739 return true
740 } catch {
741 return false
742 }
743 })
744
745 if (checkType === 'presence') {
746 const obj = inspect(socketNotifications, { depth: 5 })
747 expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined
748 } else {
749 const obj = inspect(socketNotification, { depth: 5 })
750 expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined
751 }
752 }
753
754 if (check.mail) {
755 // Last email
756 const email = emails
757 .slice()
758 .reverse()
759 .find(e => emailNotificationFinder(e))
760
761 if (checkType === 'presence') {
762 const texts = emails.map(e => e.text)
763 expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined
764 } else {
765 expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined
766 }
767 }
768}
769
770function checkVideo (video: any, videoName?: string, shortUUID?: string) {
771 if (videoName) {
772 expect(video.name).to.be.a('string')
773 expect(video.name).to.not.be.empty
774 expect(video.name).to.equal(videoName)
775 }
776
777 if (shortUUID) {
778 expect(video.shortUUID).to.be.a('string')
779 expect(video.shortUUID).to.not.be.empty
780 expect(video.shortUUID).to.equal(shortUUID)
781 }
782
783 expect(video.id).to.be.a('number')
784}
785
786function checkActor (actor: any) {
787 expect(actor.displayName).to.be.a('string')
788 expect(actor.displayName).to.not.be.empty
789 expect(actor.host).to.not.be.undefined
790}
791
792function checkComment (comment: any, commentId: number, threadId: number) {
793 expect(comment.id).to.equal(commentId)
794 expect(comment.threadId).to.equal(threadId)
795}
diff --git a/shared/extra-utils/users/subscriptions-command.ts b/shared/extra-utils/users/subscriptions-command.ts
new file mode 100644
index 000000000..edc60e612
--- /dev/null
+++ b/shared/extra-utils/users/subscriptions-command.ts
@@ -0,0 +1,99 @@
1import { HttpStatusCode, ResultList, Video, VideoChannel } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class SubscriptionsCommand extends AbstractCommand {
5
6 add (options: OverrideCommandOptions & {
7 targetUri: string
8 }) {
9 const path = '/api/v1/users/me/subscriptions'
10
11 return this.postBodyRequest({
12 ...options,
13
14 path,
15 fields: { uri: options.targetUri },
16 implicitToken: true,
17 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
18 })
19 }
20
21 list (options: OverrideCommandOptions & {
22 sort?: string // default -createdAt
23 search?: string
24 } = {}) {
25 const { sort = '-createdAt', search } = options
26 const path = '/api/v1/users/me/subscriptions'
27
28 return this.getRequestBody<ResultList<VideoChannel>>({
29 ...options,
30
31 path,
32 query: {
33 sort,
34 search
35 },
36 implicitToken: true,
37 defaultExpectedStatus: HttpStatusCode.OK_200
38 })
39 }
40
41 listVideos (options: OverrideCommandOptions & {
42 sort?: string // default -createdAt
43 } = {}) {
44 const { sort = '-createdAt' } = options
45 const path = '/api/v1/users/me/subscriptions/videos'
46
47 return this.getRequestBody<ResultList<Video>>({
48 ...options,
49
50 path,
51 query: { sort },
52 implicitToken: true,
53 defaultExpectedStatus: HttpStatusCode.OK_200
54 })
55 }
56
57 get (options: OverrideCommandOptions & {
58 uri: string
59 }) {
60 const path = '/api/v1/users/me/subscriptions/' + options.uri
61
62 return this.getRequestBody<VideoChannel>({
63 ...options,
64
65 path,
66 implicitToken: true,
67 defaultExpectedStatus: HttpStatusCode.OK_200
68 })
69 }
70
71 remove (options: OverrideCommandOptions & {
72 uri: string
73 }) {
74 const path = '/api/v1/users/me/subscriptions/' + options.uri
75
76 return this.deleteRequest({
77 ...options,
78
79 path,
80 implicitToken: true,
81 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
82 })
83 }
84
85 exist (options: OverrideCommandOptions & {
86 uris: string[]
87 }) {
88 const path = '/api/v1/users/me/subscriptions/exist'
89
90 return this.getRequestBody<{ [id: string ]: boolean }>({
91 ...options,
92
93 path,
94 query: { 'uris[]': options.uris },
95 implicitToken: true,
96 defaultExpectedStatus: HttpStatusCode.OK_200
97 })
98 }
99}
diff --git a/shared/extra-utils/users/user-subscriptions.ts b/shared/extra-utils/users/user-subscriptions.ts
deleted file mode 100644
index edc7a3562..000000000
--- a/shared/extra-utils/users/user-subscriptions.ts
+++ /dev/null
@@ -1,93 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function addUserSubscription (url: string, token: string, targetUri: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
5 const path = '/api/v1/users/me/subscriptions'
6
7 return makePostBodyRequest({
8 url,
9 path,
10 token,
11 statusCodeExpected,
12 fields: { uri: targetUri }
13 })
14}
15
16function listUserSubscriptions (parameters: {
17 url: string
18 token: string
19 sort?: string
20 search?: string
21 statusCodeExpected?: number
22}) {
23 const { url, token, sort = '-createdAt', search, statusCodeExpected = HttpStatusCode.OK_200 } = parameters
24 const path = '/api/v1/users/me/subscriptions'
25
26 return makeGetRequest({
27 url,
28 path,
29 token,
30 statusCodeExpected,
31 query: {
32 sort,
33 search
34 }
35 })
36}
37
38function listUserSubscriptionVideos (url: string, token: string, sort = '-createdAt', statusCodeExpected = HttpStatusCode.OK_200) {
39 const path = '/api/v1/users/me/subscriptions/videos'
40
41 return makeGetRequest({
42 url,
43 path,
44 token,
45 statusCodeExpected,
46 query: { sort }
47 })
48}
49
50function getUserSubscription (url: string, token: string, uri: string, statusCodeExpected = HttpStatusCode.OK_200) {
51 const path = '/api/v1/users/me/subscriptions/' + uri
52
53 return makeGetRequest({
54 url,
55 path,
56 token,
57 statusCodeExpected
58 })
59}
60
61function removeUserSubscription (url: string, token: string, uri: string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
62 const path = '/api/v1/users/me/subscriptions/' + uri
63
64 return makeDeleteRequest({
65 url,
66 path,
67 token,
68 statusCodeExpected
69 })
70}
71
72function areSubscriptionsExist (url: string, token: string, uris: string[], statusCodeExpected = HttpStatusCode.OK_200) {
73 const path = '/api/v1/users/me/subscriptions/exist'
74
75 return makeGetRequest({
76 url,
77 path,
78 query: { 'uris[]': uris },
79 token,
80 statusCodeExpected
81 })
82}
83
84// ---------------------------------------------------------------------------
85
86export {
87 areSubscriptionsExist,
88 addUserSubscription,
89 listUserSubscriptions,
90 getUserSubscription,
91 listUserSubscriptionVideos,
92 removeUserSubscription
93}
diff --git a/shared/extra-utils/users/users-command.ts b/shared/extra-utils/users/users-command.ts
new file mode 100644
index 000000000..ddd20d041
--- /dev/null
+++ b/shared/extra-utils/users/users-command.ts
@@ -0,0 +1,415 @@
1import { omit } from 'lodash'
2import { pick } from '@shared/core-utils'
3import {
4 HttpStatusCode,
5 MyUser,
6 ResultList,
7 User,
8 UserAdminFlag,
9 UserCreateResult,
10 UserRole,
11 UserUpdate,
12 UserUpdateMe,
13 UserVideoQuota,
14 UserVideoRate
15} from '@shared/models'
16import { ScopedToken } from '@shared/models/users/user-scoped-token'
17import { unwrapBody } from '../requests'
18import { AbstractCommand, OverrideCommandOptions } from '../shared'
19
20export class UsersCommand extends AbstractCommand {
21
22 askResetPassword (options: OverrideCommandOptions & {
23 email: string
24 }) {
25 const { email } = options
26 const path = '/api/v1/users/ask-reset-password'
27
28 return this.postBodyRequest({
29 ...options,
30
31 path,
32 fields: { email },
33 implicitToken: false,
34 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
35 })
36 }
37
38 resetPassword (options: OverrideCommandOptions & {
39 userId: number
40 verificationString: string
41 password: string
42 }) {
43 const { userId, verificationString, password } = options
44 const path = '/api/v1/users/' + userId + '/reset-password'
45
46 return this.postBodyRequest({
47 ...options,
48
49 path,
50 fields: { password, verificationString },
51 implicitToken: false,
52 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
53 })
54 }
55
56 // ---------------------------------------------------------------------------
57
58 askSendVerifyEmail (options: OverrideCommandOptions & {
59 email: string
60 }) {
61 const { email } = options
62 const path = '/api/v1/users/ask-send-verify-email'
63
64 return this.postBodyRequest({
65 ...options,
66
67 path,
68 fields: { email },
69 implicitToken: false,
70 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
71 })
72 }
73
74 verifyEmail (options: OverrideCommandOptions & {
75 userId: number
76 verificationString: string
77 isPendingEmail?: boolean // default false
78 }) {
79 const { userId, verificationString, isPendingEmail = false } = options
80 const path = '/api/v1/users/' + userId + '/verify-email'
81
82 return this.postBodyRequest({
83 ...options,
84
85 path,
86 fields: {
87 verificationString,
88 isPendingEmail
89 },
90 implicitToken: false,
91 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
92 })
93 }
94
95 // ---------------------------------------------------------------------------
96
97 banUser (options: OverrideCommandOptions & {
98 userId: number
99 reason?: string
100 }) {
101 const { userId, reason } = options
102 const path = '/api/v1/users' + '/' + userId + '/block'
103
104 return this.postBodyRequest({
105 ...options,
106
107 path,
108 fields: { reason },
109 implicitToken: true,
110 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
111 })
112 }
113
114 unbanUser (options: OverrideCommandOptions & {
115 userId: number
116 }) {
117 const { userId } = options
118 const path = '/api/v1/users' + '/' + userId + '/unblock'
119
120 return this.postBodyRequest({
121 ...options,
122
123 path,
124 implicitToken: true,
125 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
126 })
127 }
128
129 // ---------------------------------------------------------------------------
130
131 getMyScopedTokens (options: OverrideCommandOptions = {}) {
132 const path = '/api/v1/users/scoped-tokens'
133
134 return this.getRequestBody<ScopedToken>({
135 ...options,
136
137 path,
138 implicitToken: true,
139 defaultExpectedStatus: HttpStatusCode.OK_200
140 })
141 }
142
143 renewMyScopedTokens (options: OverrideCommandOptions = {}) {
144 const path = '/api/v1/users/scoped-tokens'
145
146 return this.postBodyRequest({
147 ...options,
148
149 path,
150 implicitToken: true,
151 defaultExpectedStatus: HttpStatusCode.OK_200
152 })
153 }
154
155 // ---------------------------------------------------------------------------
156
157 create (options: OverrideCommandOptions & {
158 username: string
159 password?: string
160 videoQuota?: number
161 videoQuotaDaily?: number
162 role?: UserRole
163 adminFlags?: UserAdminFlag
164 }) {
165 const {
166 username,
167 adminFlags,
168 password = 'password',
169 videoQuota = 42000000,
170 videoQuotaDaily = -1,
171 role = UserRole.USER
172 } = options
173
174 const path = '/api/v1/users'
175
176 return unwrapBody<{ user: UserCreateResult }>(this.postBodyRequest({
177 ...options,
178
179 path,
180 fields: {
181 username,
182 password,
183 role,
184 adminFlags,
185 email: username + '@example.com',
186 videoQuota,
187 videoQuotaDaily
188 },
189 implicitToken: true,
190 defaultExpectedStatus: HttpStatusCode.OK_200
191 })).then(res => res.user)
192 }
193
194 async generate (username: string) {
195 const password = 'password'
196 const user = await this.create({ username, password })
197
198 const token = await this.server.login.getAccessToken({ username, password })
199
200 const me = await this.getMyInfo({ token })
201
202 return {
203 token,
204 userId: user.id,
205 userChannelId: me.videoChannels[0].id
206 }
207 }
208
209 async generateUserAndToken (username: string) {
210 const password = 'password'
211 await this.create({ username, password })
212
213 return this.server.login.getAccessToken({ username, password })
214 }
215
216 register (options: OverrideCommandOptions & {
217 username: string
218 password?: string
219 displayName?: string
220 channel?: {
221 name: string
222 displayName: string
223 }
224 }) {
225 const { username, password = 'password', displayName, channel } = options
226 const path = '/api/v1/users/register'
227
228 return this.postBodyRequest({
229 ...options,
230
231 path,
232 fields: {
233 username,
234 password,
235 email: username + '@example.com',
236 displayName,
237 channel
238 },
239 implicitToken: false,
240 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
241 })
242 }
243
244 // ---------------------------------------------------------------------------
245
246 getMyInfo (options: OverrideCommandOptions = {}) {
247 const path = '/api/v1/users/me'
248
249 return this.getRequestBody<MyUser>({
250 ...options,
251
252 path,
253 implicitToken: true,
254 defaultExpectedStatus: HttpStatusCode.OK_200
255 })
256 }
257
258 getMyQuotaUsed (options: OverrideCommandOptions = {}) {
259 const path = '/api/v1/users/me/video-quota-used'
260
261 return this.getRequestBody<UserVideoQuota>({
262 ...options,
263
264 path,
265 implicitToken: true,
266 defaultExpectedStatus: HttpStatusCode.OK_200
267 })
268 }
269
270 getMyRating (options: OverrideCommandOptions & {
271 videoId: number | string
272 }) {
273 const { videoId } = options
274 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
275
276 return this.getRequestBody<UserVideoRate>({
277 ...options,
278
279 path,
280 implicitToken: true,
281 defaultExpectedStatus: HttpStatusCode.OK_200
282 })
283 }
284
285 deleteMe (options: OverrideCommandOptions = {}) {
286 const path = '/api/v1/users/me'
287
288 return this.deleteRequest({
289 ...options,
290
291 path,
292 implicitToken: true,
293 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
294 })
295 }
296
297 updateMe (options: OverrideCommandOptions & UserUpdateMe) {
298 const path = '/api/v1/users/me'
299
300 const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
301
302 return this.putBodyRequest({
303 ...options,
304
305 path,
306 fields: toSend,
307 implicitToken: true,
308 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
309 })
310 }
311
312 updateMyAvatar (options: OverrideCommandOptions & {
313 fixture: string
314 }) {
315 const { fixture } = options
316 const path = '/api/v1/users/me/avatar/pick'
317
318 return this.updateImageRequest({
319 ...options,
320
321 path,
322 fixture,
323 fieldname: 'avatarfile',
324
325 implicitToken: true,
326 defaultExpectedStatus: HttpStatusCode.OK_200
327 })
328 }
329
330 // ---------------------------------------------------------------------------
331
332 get (options: OverrideCommandOptions & {
333 userId: number
334 withStats?: boolean // default false
335 }) {
336 const { userId, withStats } = options
337 const path = '/api/v1/users/' + userId
338
339 return this.getRequestBody<User>({
340 ...options,
341
342 path,
343 query: { withStats },
344 implicitToken: true,
345 defaultExpectedStatus: HttpStatusCode.OK_200
346 })
347 }
348
349 list (options: OverrideCommandOptions & {
350 start?: number
351 count?: number
352 sort?: string
353 search?: string
354 blocked?: boolean
355 } = {}) {
356 const path = '/api/v1/users'
357
358 return this.getRequestBody<ResultList<User>>({
359 ...options,
360
361 path,
362 query: pick(options, [ 'start', 'count', 'sort', 'search', 'blocked' ]),
363 implicitToken: true,
364 defaultExpectedStatus: HttpStatusCode.OK_200
365 })
366 }
367
368 remove (options: OverrideCommandOptions & {
369 userId: number
370 }) {
371 const { userId } = options
372 const path = '/api/v1/users/' + userId
373
374 return this.deleteRequest({
375 ...options,
376
377 path,
378 implicitToken: true,
379 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
380 })
381 }
382
383 update (options: OverrideCommandOptions & {
384 userId: number
385 email?: string
386 emailVerified?: boolean
387 videoQuota?: number
388 videoQuotaDaily?: number
389 password?: string
390 adminFlags?: UserAdminFlag
391 pluginAuth?: string
392 role?: UserRole
393 }) {
394 const path = '/api/v1/users/' + options.userId
395
396 const toSend: UserUpdate = {}
397 if (options.password !== undefined && options.password !== null) toSend.password = options.password
398 if (options.email !== undefined && options.email !== null) toSend.email = options.email
399 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend.emailVerified = options.emailVerified
400 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend.videoQuota = options.videoQuota
401 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend.videoQuotaDaily = options.videoQuotaDaily
402 if (options.role !== undefined && options.role !== null) toSend.role = options.role
403 if (options.adminFlags !== undefined && options.adminFlags !== null) toSend.adminFlags = options.adminFlags
404 if (options.pluginAuth !== undefined) toSend.pluginAuth = options.pluginAuth
405
406 return this.putBodyRequest({
407 ...options,
408
409 path,
410 fields: toSend,
411 implicitToken: true,
412 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
413 })
414 }
415}
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
deleted file mode 100644
index 0f15962ad..000000000
--- a/shared/extra-utils/users/users.ts
+++ /dev/null
@@ -1,415 +0,0 @@
1import { omit } from 'lodash'
2import * as request from 'supertest'
3import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
4import { UserUpdateMe } from '../../models/users'
5import { UserAdminFlag } from '../../models/users/user-flag.model'
6import { UserRegister } from '../../models/users/user-register.model'
7import { UserRole } from '../../models/users/user-role'
8import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateImageRequest } from '../requests/requests'
9import { ServerInfo } from '../server/servers'
10import { userLogin } from './login'
11
12function createUser (parameters: {
13 url: string
14 accessToken: string
15 username: string
16 password: string
17 videoQuota?: number
18 videoQuotaDaily?: number
19 role?: UserRole
20 adminFlags?: UserAdminFlag
21 specialStatus?: number
22}) {
23 const {
24 url,
25 accessToken,
26 username,
27 adminFlags,
28 password = 'password',
29 videoQuota = 1000000,
30 videoQuotaDaily = -1,
31 role = UserRole.USER,
32 specialStatus = HttpStatusCode.OK_200
33 } = parameters
34
35 const path = '/api/v1/users'
36 const body = {
37 username,
38 password,
39 role,
40 adminFlags,
41 email: username + '@example.com',
42 videoQuota,
43 videoQuotaDaily
44 }
45
46 return request(url)
47 .post(path)
48 .set('Accept', 'application/json')
49 .set('Authorization', 'Bearer ' + accessToken)
50 .send(body)
51 .expect(specialStatus)
52}
53
54async function generateUser (server: ServerInfo, username: string) {
55 const password = 'my super password'
56 const resCreate = await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
57
58 const token = await userLogin(server, { username, password })
59
60 const resMe = await getMyUserInformation(server.url, token)
61
62 return {
63 token,
64 userId: resCreate.body.user.id,
65 userChannelId: resMe.body.videoChannels[0].id
66 }
67}
68
69async function generateUserAccessToken (server: ServerInfo, username: string) {
70 const password = 'my super password'
71 await createUser({ url: server.url, accessToken: server.accessToken, username: username, password: password })
72
73 return userLogin(server, { username, password })
74}
75
76function registerUser (url: string, username: string, password: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
77 const path = '/api/v1/users/register'
78 const body = {
79 username,
80 password,
81 email: username + '@example.com'
82 }
83
84 return request(url)
85 .post(path)
86 .set('Accept', 'application/json')
87 .send(body)
88 .expect(specialStatus)
89}
90
91function registerUserWithChannel (options: {
92 url: string
93 user: { username: string, password: string, displayName?: string }
94 channel: { name: string, displayName: string }
95}) {
96 const path = '/api/v1/users/register'
97 const body: UserRegister = {
98 username: options.user.username,
99 password: options.user.password,
100 email: options.user.username + '@example.com',
101 channel: options.channel
102 }
103
104 if (options.user.displayName) {
105 Object.assign(body, { displayName: options.user.displayName })
106 }
107
108 return makePostBodyRequest({
109 url: options.url,
110 path,
111 fields: body,
112 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
113 })
114}
115
116function getMyUserInformation (url: string, accessToken: string, specialStatus = HttpStatusCode.OK_200) {
117 const path = '/api/v1/users/me'
118
119 return request(url)
120 .get(path)
121 .set('Accept', 'application/json')
122 .set('Authorization', 'Bearer ' + accessToken)
123 .expect(specialStatus)
124 .expect('Content-Type', /json/)
125}
126
127function getUserScopedTokens (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
128 const path = '/api/v1/users/scoped-tokens'
129
130 return makeGetRequest({
131 url,
132 path,
133 token,
134 statusCodeExpected
135 })
136}
137
138function renewUserScopedTokens (url: string, token: string, statusCodeExpected = HttpStatusCode.OK_200) {
139 const path = '/api/v1/users/scoped-tokens'
140
141 return makePostBodyRequest({
142 url,
143 path,
144 token,
145 statusCodeExpected
146 })
147}
148
149function deleteMe (url: string, accessToken: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
150 const path = '/api/v1/users/me'
151
152 return request(url)
153 .delete(path)
154 .set('Accept', 'application/json')
155 .set('Authorization', 'Bearer ' + accessToken)
156 .expect(specialStatus)
157}
158
159function getMyUserVideoQuotaUsed (url: string, accessToken: string, specialStatus = HttpStatusCode.OK_200) {
160 const path = '/api/v1/users/me/video-quota-used'
161
162 return request(url)
163 .get(path)
164 .set('Accept', 'application/json')
165 .set('Authorization', 'Bearer ' + accessToken)
166 .expect(specialStatus)
167 .expect('Content-Type', /json/)
168}
169
170function getUserInformation (url: string, accessToken: string, userId: number, withStats = false) {
171 const path = '/api/v1/users/' + userId
172
173 return request(url)
174 .get(path)
175 .query({ withStats })
176 .set('Accept', 'application/json')
177 .set('Authorization', 'Bearer ' + accessToken)
178 .expect(HttpStatusCode.OK_200)
179 .expect('Content-Type', /json/)
180}
181
182function getMyUserVideoRating (url: string, accessToken: string, videoId: number | string, specialStatus = HttpStatusCode.OK_200) {
183 const path = '/api/v1/users/me/videos/' + videoId + '/rating'
184
185 return request(url)
186 .get(path)
187 .set('Accept', 'application/json')
188 .set('Authorization', 'Bearer ' + accessToken)
189 .expect(specialStatus)
190 .expect('Content-Type', /json/)
191}
192
193function getUsersList (url: string, accessToken: string) {
194 const path = '/api/v1/users'
195
196 return request(url)
197 .get(path)
198 .set('Accept', 'application/json')
199 .set('Authorization', 'Bearer ' + accessToken)
200 .expect(HttpStatusCode.OK_200)
201 .expect('Content-Type', /json/)
202}
203
204function getUsersListPaginationAndSort (
205 url: string,
206 accessToken: string,
207 start: number,
208 count: number,
209 sort: string,
210 search?: string,
211 blocked?: boolean
212) {
213 const path = '/api/v1/users'
214
215 const query = {
216 start,
217 count,
218 sort,
219 search,
220 blocked
221 }
222
223 return request(url)
224 .get(path)
225 .query(query)
226 .set('Accept', 'application/json')
227 .set('Authorization', 'Bearer ' + accessToken)
228 .expect(HttpStatusCode.OK_200)
229 .expect('Content-Type', /json/)
230}
231
232function removeUser (url: string, userId: number | string, accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
233 const path = '/api/v1/users'
234
235 return request(url)
236 .delete(path + '/' + userId)
237 .set('Accept', 'application/json')
238 .set('Authorization', 'Bearer ' + accessToken)
239 .expect(expectedStatus)
240}
241
242function blockUser (
243 url: string,
244 userId: number | string,
245 accessToken: string,
246 expectedStatus = HttpStatusCode.NO_CONTENT_204,
247 reason?: string
248) {
249 const path = '/api/v1/users'
250 let body: any
251 if (reason) body = { reason }
252
253 return request(url)
254 .post(path + '/' + userId + '/block')
255 .send(body)
256 .set('Accept', 'application/json')
257 .set('Authorization', 'Bearer ' + accessToken)
258 .expect(expectedStatus)
259}
260
261function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
262 const path = '/api/v1/users'
263
264 return request(url)
265 .post(path + '/' + userId + '/unblock')
266 .set('Accept', 'application/json')
267 .set('Authorization', 'Bearer ' + accessToken)
268 .expect(expectedStatus)
269}
270
271function updateMyUser (options: { url: string, accessToken: string, statusCodeExpected?: HttpStatusCode } & UserUpdateMe) {
272 const path = '/api/v1/users/me'
273
274 const toSend: UserUpdateMe = omit(options, 'url', 'accessToken')
275
276 return makePutBodyRequest({
277 url: options.url,
278 path,
279 token: options.accessToken,
280 fields: toSend,
281 statusCodeExpected: options.statusCodeExpected || HttpStatusCode.NO_CONTENT_204
282 })
283}
284
285function updateMyAvatar (options: {
286 url: string
287 accessToken: string
288 fixture: string
289}) {
290 const path = '/api/v1/users/me/avatar/pick'
291
292 return updateImageRequest({ ...options, path, fieldname: 'avatarfile' })
293}
294
295function updateUser (options: {
296 url: string
297 userId: number
298 accessToken: string
299 email?: string
300 emailVerified?: boolean
301 videoQuota?: number
302 videoQuotaDaily?: number
303 password?: string
304 adminFlags?: UserAdminFlag
305 pluginAuth?: string
306 role?: UserRole
307}) {
308 const path = '/api/v1/users/' + options.userId
309
310 const toSend = {}
311 if (options.password !== undefined && options.password !== null) toSend['password'] = options.password
312 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
313 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
314 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota
315 if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend['videoQuotaDaily'] = options.videoQuotaDaily
316 if (options.role !== undefined && options.role !== null) toSend['role'] = options.role
317 if (options.adminFlags !== undefined && options.adminFlags !== null) toSend['adminFlags'] = options.adminFlags
318 if (options.pluginAuth !== undefined) toSend['pluginAuth'] = options.pluginAuth
319
320 return makePutBodyRequest({
321 url: options.url,
322 path,
323 token: options.accessToken,
324 fields: toSend,
325 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
326 })
327}
328
329function askResetPassword (url: string, email: string) {
330 const path = '/api/v1/users/ask-reset-password'
331
332 return makePostBodyRequest({
333 url,
334 path,
335 fields: { email },
336 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
337 })
338}
339
340function resetPassword (
341 url: string,
342 userId: number,
343 verificationString: string,
344 password: string,
345 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
346) {
347 const path = '/api/v1/users/' + userId + '/reset-password'
348
349 return makePostBodyRequest({
350 url,
351 path,
352 fields: { password, verificationString },
353 statusCodeExpected
354 })
355}
356
357function askSendVerifyEmail (url: string, email: string) {
358 const path = '/api/v1/users/ask-send-verify-email'
359
360 return makePostBodyRequest({
361 url,
362 path,
363 fields: { email },
364 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
365 })
366}
367
368function verifyEmail (
369 url: string,
370 userId: number,
371 verificationString: string,
372 isPendingEmail = false,
373 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
374) {
375 const path = '/api/v1/users/' + userId + '/verify-email'
376
377 return makePostBodyRequest({
378 url,
379 path,
380 fields: {
381 verificationString,
382 isPendingEmail
383 },
384 statusCodeExpected
385 })
386}
387
388// ---------------------------------------------------------------------------
389
390export {
391 createUser,
392 registerUser,
393 getMyUserInformation,
394 getMyUserVideoRating,
395 deleteMe,
396 registerUserWithChannel,
397 getMyUserVideoQuotaUsed,
398 getUsersList,
399 getUsersListPaginationAndSort,
400 removeUser,
401 updateUser,
402 updateMyUser,
403 getUserInformation,
404 blockUser,
405 unblockUser,
406 askResetPassword,
407 resetPassword,
408 renewUserScopedTokens,
409 updateMyAvatar,
410 generateUser,
411 askSendVerifyEmail,
412 generateUserAccessToken,
413 verifyEmail,
414 getUserScopedTokens
415}
diff --git a/shared/extra-utils/videos/blacklist-command.ts b/shared/extra-utils/videos/blacklist-command.ts
new file mode 100644
index 000000000..3a2ef89ba
--- /dev/null
+++ b/shared/extra-utils/videos/blacklist-command.ts
@@ -0,0 +1,76 @@
1
2import { HttpStatusCode, ResultList } from '@shared/models'
3import { VideoBlacklist, VideoBlacklistType } from '../../models/videos'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class BlacklistCommand extends AbstractCommand {
7
8 add (options: OverrideCommandOptions & {
9 videoId: number | string
10 reason?: string
11 unfederate?: boolean
12 }) {
13 const { videoId, reason, unfederate } = options
14 const path = '/api/v1/videos/' + videoId + '/blacklist'
15
16 return this.postBodyRequest({
17 ...options,
18
19 path,
20 fields: { reason, unfederate },
21 implicitToken: true,
22 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
23 })
24 }
25
26 update (options: OverrideCommandOptions & {
27 videoId: number | string
28 reason?: string
29 }) {
30 const { videoId, reason } = options
31 const path = '/api/v1/videos/' + videoId + '/blacklist'
32
33 return this.putBodyRequest({
34 ...options,
35
36 path,
37 fields: { reason },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
40 })
41 }
42
43 remove (options: OverrideCommandOptions & {
44 videoId: number | string
45 }) {
46 const { videoId } = options
47 const path = '/api/v1/videos/' + videoId + '/blacklist'
48
49 return this.deleteRequest({
50 ...options,
51
52 path,
53 implicitToken: true,
54 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
55 })
56 }
57
58 list (options: OverrideCommandOptions & {
59 sort?: string
60 type?: VideoBlacklistType
61 } = {}) {
62 const { sort, type } = options
63 const path = '/api/v1/videos/blacklist/'
64
65 const query = { sort, type }
66
67 return this.getRequestBody<ResultList<VideoBlacklist>>({
68 ...options,
69
70 path,
71 query,
72 implicitToken: true,
73 defaultExpectedStatus: HttpStatusCode.OK_200
74 })
75 }
76}
diff --git a/shared/extra-utils/videos/captions-command.ts b/shared/extra-utils/videos/captions-command.ts
new file mode 100644
index 000000000..a65ea99e3
--- /dev/null
+++ b/shared/extra-utils/videos/captions-command.ts
@@ -0,0 +1,65 @@
1import { HttpStatusCode, ResultList, VideoCaption } from '@shared/models'
2import { buildAbsoluteFixturePath } from '../miscs'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class CaptionsCommand extends AbstractCommand {
6
7 add (options: OverrideCommandOptions & {
8 videoId: string | number
9 language: string
10 fixture: string
11 mimeType?: string
12 }) {
13 const { videoId, language, fixture, mimeType } = options
14
15 const path = '/api/v1/videos/' + videoId + '/captions/' + language
16
17 const captionfile = buildAbsoluteFixturePath(fixture)
18 const captionfileAttach = mimeType
19 ? [ captionfile, { contentType: mimeType } ]
20 : captionfile
21
22 return this.putUploadRequest({
23 ...options,
24
25 path,
26 fields: {},
27 attaches: {
28 captionfile: captionfileAttach
29 },
30 implicitToken: true,
31 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
32 })
33 }
34
35 list (options: OverrideCommandOptions & {
36 videoId: string | number
37 }) {
38 const { videoId } = options
39 const path = '/api/v1/videos/' + videoId + '/captions'
40
41 return this.getRequestBody<ResultList<VideoCaption>>({
42 ...options,
43
44 path,
45 implicitToken: false,
46 defaultExpectedStatus: HttpStatusCode.OK_200
47 })
48 }
49
50 delete (options: OverrideCommandOptions & {
51 videoId: string | number
52 language: string
53 }) {
54 const { videoId, language } = options
55 const path = '/api/v1/videos/' + videoId + '/captions/' + language
56
57 return this.deleteRequest({
58 ...options,
59
60 path,
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
63 })
64 }
65}
diff --git a/shared/extra-utils/videos/captions.ts b/shared/extra-utils/videos/captions.ts
new file mode 100644
index 000000000..ff8a43366
--- /dev/null
+++ b/shared/extra-utils/videos/captions.ts
@@ -0,0 +1,17 @@
1import { expect } from 'chai'
2import * as request from 'supertest'
3import { HttpStatusCode } from '@shared/models'
4
5async function testCaptionFile (url: string, captionPath: string, containsString: string) {
6 const res = await request(url)
7 .get(captionPath)
8 .expect(HttpStatusCode.OK_200)
9
10 expect(res.text).to.contain(containsString)
11}
12
13// ---------------------------------------------------------------------------
14
15export {
16 testCaptionFile
17}
diff --git a/shared/extra-utils/videos/change-ownership-command.ts b/shared/extra-utils/videos/change-ownership-command.ts
new file mode 100644
index 000000000..ad4c726ef
--- /dev/null
+++ b/shared/extra-utils/videos/change-ownership-command.ts
@@ -0,0 +1,68 @@
1
2import { HttpStatusCode, ResultList, VideoChangeOwnership } from '@shared/models'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class ChangeOwnershipCommand extends AbstractCommand {
6
7 create (options: OverrideCommandOptions & {
8 videoId: number | string
9 username: string
10 }) {
11 const { videoId, username } = options
12 const path = '/api/v1/videos/' + videoId + '/give-ownership'
13
14 return this.postBodyRequest({
15 ...options,
16
17 path,
18 fields: { username },
19 implicitToken: true,
20 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
21 })
22 }
23
24 list (options: OverrideCommandOptions = {}) {
25 const path = '/api/v1/videos/ownership'
26
27 return this.getRequestBody<ResultList<VideoChangeOwnership>>({
28 ...options,
29
30 path,
31 query: { sort: '-createdAt' },
32 implicitToken: true,
33 defaultExpectedStatus: HttpStatusCode.OK_200
34 })
35 }
36
37 accept (options: OverrideCommandOptions & {
38 ownershipId: number
39 channelId: number
40 }) {
41 const { ownershipId, channelId } = options
42 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
43
44 return this.postBodyRequest({
45 ...options,
46
47 path,
48 fields: { channelId },
49 implicitToken: true,
50 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
51 })
52 }
53
54 refuse (options: OverrideCommandOptions & {
55 ownershipId: number
56 }) {
57 const { ownershipId } = options
58 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
59
60 return this.postBodyRequest({
61 ...options,
62
63 path,
64 implicitToken: true,
65 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
66 })
67 }
68}
diff --git a/shared/extra-utils/videos/channels-command.ts b/shared/extra-utils/videos/channels-command.ts
new file mode 100644
index 000000000..255e1d62d
--- /dev/null
+++ b/shared/extra-utils/videos/channels-command.ts
@@ -0,0 +1,156 @@
1import { pick } from '@shared/core-utils'
2import { HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models'
3import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
5import { unwrapBody } from '../requests'
6import { AbstractCommand, OverrideCommandOptions } from '../shared'
7
8export class ChannelsCommand extends AbstractCommand {
9
10 list (options: OverrideCommandOptions & {
11 start?: number
12 count?: number
13 sort?: string
14 withStats?: boolean
15 } = {}) {
16 const path = '/api/v1/video-channels'
17
18 return this.getRequestBody<ResultList<VideoChannel>>({
19 ...options,
20
21 path,
22 query: pick(options, [ 'start', 'count', 'sort', 'withStats' ]),
23 implicitToken: false,
24 defaultExpectedStatus: HttpStatusCode.OK_200
25 })
26 }
27
28 listByAccount (options: OverrideCommandOptions & {
29 accountName: string
30 start?: number
31 count?: number
32 sort?: string
33 withStats?: boolean
34 search?: string
35 }) {
36 const { accountName, sort = 'createdAt' } = options
37 const path = '/api/v1/accounts/' + accountName + '/video-channels'
38
39 return this.getRequestBody<ResultList<VideoChannel>>({
40 ...options,
41
42 path,
43 query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
44 implicitToken: false,
45 defaultExpectedStatus: HttpStatusCode.OK_200
46 })
47 }
48
49 async create (options: OverrideCommandOptions & {
50 attributes: VideoChannelCreate
51 }) {
52 const path = '/api/v1/video-channels/'
53
54 // Default attributes
55 const defaultAttributes = {
56 displayName: 'my super video channel',
57 description: 'my super channel description',
58 support: 'my super channel support'
59 }
60 const attributes = { ...defaultAttributes, ...options.attributes }
61
62 const body = await unwrapBody<{ videoChannel: VideoChannelCreateResult }>(this.postBodyRequest({
63 ...options,
64
65 path,
66 fields: attributes,
67 implicitToken: true,
68 defaultExpectedStatus: HttpStatusCode.OK_200
69 }))
70
71 return body.videoChannel
72 }
73
74 update (options: OverrideCommandOptions & {
75 channelName: string
76 attributes: VideoChannelUpdate
77 }) {
78 const { channelName, attributes } = options
79 const path = '/api/v1/video-channels/' + channelName
80
81 return this.putBodyRequest({
82 ...options,
83
84 path,
85 fields: attributes,
86 implicitToken: true,
87 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
88 })
89 }
90
91 delete (options: OverrideCommandOptions & {
92 channelName: string
93 }) {
94 const path = '/api/v1/video-channels/' + options.channelName
95
96 return this.deleteRequest({
97 ...options,
98
99 path,
100 implicitToken: true,
101 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
102 })
103 }
104
105 get (options: OverrideCommandOptions & {
106 channelName: string
107 }) {
108 const path = '/api/v1/video-channels/' + options.channelName
109
110 return this.getRequestBody<VideoChannel>({
111 ...options,
112
113 path,
114 implicitToken: false,
115 defaultExpectedStatus: HttpStatusCode.OK_200
116 })
117 }
118
119 updateImage (options: OverrideCommandOptions & {
120 fixture: string
121 channelName: string | number
122 type: 'avatar' | 'banner'
123 }) {
124 const { channelName, fixture, type } = options
125
126 const path = `/api/v1/video-channels/${channelName}/${type}/pick`
127
128 return this.updateImageRequest({
129 ...options,
130
131 path,
132 fixture,
133 fieldname: type + 'file',
134
135 implicitToken: true,
136 defaultExpectedStatus: HttpStatusCode.OK_200
137 })
138 }
139
140 deleteImage (options: OverrideCommandOptions & {
141 channelName: string | number
142 type: 'avatar' | 'banner'
143 }) {
144 const { channelName, type } = options
145
146 const path = `/api/v1/video-channels/${channelName}/${type}`
147
148 return this.deleteRequest({
149 ...options,
150
151 path,
152 implicitToken: true,
153 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
154 })
155 }
156}
diff --git a/shared/extra-utils/videos/channels.ts b/shared/extra-utils/videos/channels.ts
new file mode 100644
index 000000000..756c47453
--- /dev/null
+++ b/shared/extra-utils/videos/channels.ts
@@ -0,0 +1,18 @@
1import { PeerTubeServer } from '../server/server'
2
3function setDefaultVideoChannel (servers: PeerTubeServer[]) {
4 const tasks: Promise<any>[] = []
5
6 for (const server of servers) {
7 const p = server.users.getMyInfo()
8 .then(user => { server.store.channel = user.videoChannels[0] })
9
10 tasks.push(p)
11 }
12
13 return Promise.all(tasks)
14}
15
16export {
17 setDefaultVideoChannel
18}
diff --git a/shared/extra-utils/videos/comments-command.ts b/shared/extra-utils/videos/comments-command.ts
new file mode 100644
index 000000000..f0d163a07
--- /dev/null
+++ b/shared/extra-utils/videos/comments-command.ts
@@ -0,0 +1,152 @@
1import { pick } from 'lodash'
2import { HttpStatusCode, ResultList, VideoComment, VideoCommentThreads, VideoCommentThreadTree } from '@shared/models'
3import { unwrapBody } from '../requests'
4import { AbstractCommand, OverrideCommandOptions } from '../shared'
5
6export class CommentsCommand extends AbstractCommand {
7
8 private lastVideoId: number | string
9 private lastThreadId: number
10 private lastReplyId: number
11
12 listForAdmin (options: OverrideCommandOptions & {
13 start?: number
14 count?: number
15 sort?: string
16 isLocal?: boolean
17 search?: string
18 searchAccount?: string
19 searchVideo?: string
20 } = {}) {
21 const { sort = '-createdAt' } = options
22 const path = '/api/v1/videos/comments'
23
24 const query = { sort, ...pick(options, [ 'start', 'count', 'isLocal', 'search', 'searchAccount', 'searchVideo' ]) }
25
26 return this.getRequestBody<ResultList<VideoComment>>({
27 ...options,
28
29 path,
30 query,
31 implicitToken: true,
32 defaultExpectedStatus: HttpStatusCode.OK_200
33 })
34 }
35
36 listThreads (options: OverrideCommandOptions & {
37 videoId: number | string
38 start?: number
39 count?: number
40 sort?: string
41 }) {
42 const { start, count, sort, videoId } = options
43 const path = '/api/v1/videos/' + videoId + '/comment-threads'
44
45 return this.getRequestBody<VideoCommentThreads>({
46 ...options,
47
48 path,
49 query: { start, count, sort },
50 implicitToken: false,
51 defaultExpectedStatus: HttpStatusCode.OK_200
52 })
53 }
54
55 getThread (options: OverrideCommandOptions & {
56 videoId: number | string
57 threadId: number
58 }) {
59 const { videoId, threadId } = options
60 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
61
62 return this.getRequestBody<VideoCommentThreadTree>({
63 ...options,
64
65 path,
66 implicitToken: false,
67 defaultExpectedStatus: HttpStatusCode.OK_200
68 })
69 }
70
71 async createThread (options: OverrideCommandOptions & {
72 videoId: number | string
73 text: string
74 }) {
75 const { videoId, text } = options
76 const path = '/api/v1/videos/' + videoId + '/comment-threads'
77
78 const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
79 ...options,
80
81 path,
82 fields: { text },
83 implicitToken: true,
84 defaultExpectedStatus: HttpStatusCode.OK_200
85 }))
86
87 this.lastThreadId = body.comment?.id
88 this.lastVideoId = videoId
89
90 return body.comment
91 }
92
93 async addReply (options: OverrideCommandOptions & {
94 videoId: number | string
95 toCommentId: number
96 text: string
97 }) {
98 const { videoId, toCommentId, text } = options
99 const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
100
101 const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
102 ...options,
103
104 path,
105 fields: { text },
106 implicitToken: true,
107 defaultExpectedStatus: HttpStatusCode.OK_200
108 }))
109
110 this.lastReplyId = body.comment?.id
111
112 return body.comment
113 }
114
115 async addReplyToLastReply (options: OverrideCommandOptions & {
116 text: string
117 }) {
118 return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
119 }
120
121 async addReplyToLastThread (options: OverrideCommandOptions & {
122 text: string
123 }) {
124 return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
125 }
126
127 async findCommentId (options: OverrideCommandOptions & {
128 videoId: number | string
129 text: string
130 }) {
131 const { videoId, text } = options
132 const { data } = await this.listThreads({ videoId, count: 25, sort: '-createdAt' })
133
134 return data.find(c => c.text === text).id
135 }
136
137 delete (options: OverrideCommandOptions & {
138 videoId: number | string
139 commentId: number
140 }) {
141 const { videoId, commentId } = options
142 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
143
144 return this.deleteRequest({
145 ...options,
146
147 path,
148 implicitToken: true,
149 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
150 })
151 }
152}
diff --git a/shared/extra-utils/videos/history-command.ts b/shared/extra-utils/videos/history-command.ts
new file mode 100644
index 000000000..13b7150c1
--- /dev/null
+++ b/shared/extra-utils/videos/history-command.ts
@@ -0,0 +1,58 @@
1import { HttpStatusCode, ResultList, Video } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class HistoryCommand extends AbstractCommand {
5
6 wathVideo (options: OverrideCommandOptions & {
7 videoId: number | string
8 currentTime: number
9 }) {
10 const { videoId, currentTime } = options
11
12 const path = '/api/v1/videos/' + videoId + '/watching'
13 const fields = { currentTime }
14
15 return this.putBodyRequest({
16 ...options,
17
18 path,
19 fields,
20 implicitToken: true,
21 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
22 })
23 }
24
25 list (options: OverrideCommandOptions & {
26 search?: string
27 } = {}) {
28 const { search } = options
29 const path = '/api/v1/users/me/history/videos'
30
31 return this.getRequestBody<ResultList<Video>>({
32 ...options,
33
34 path,
35 query: {
36 search
37 },
38 implicitToken: true,
39 defaultExpectedStatus: HttpStatusCode.OK_200
40 })
41 }
42
43 remove (options: OverrideCommandOptions & {
44 beforeDate?: string
45 } = {}) {
46 const { beforeDate } = options
47 const path = '/api/v1/users/me/history/videos/remove'
48
49 return this.postBodyRequest({
50 ...options,
51
52 path,
53 fields: { beforeDate },
54 implicitToken: true,
55 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
56 })
57 }
58}
diff --git a/shared/extra-utils/videos/imports-command.ts b/shared/extra-utils/videos/imports-command.ts
new file mode 100644
index 000000000..e4944694d
--- /dev/null
+++ b/shared/extra-utils/videos/imports-command.ts
@@ -0,0 +1,47 @@
1
2import { HttpStatusCode, ResultList } from '@shared/models'
3import { VideoImport, VideoImportCreate } from '../../models/videos'
4import { unwrapBody } from '../requests'
5import { AbstractCommand, OverrideCommandOptions } from '../shared'
6
7export class ImportsCommand extends AbstractCommand {
8
9 importVideo (options: OverrideCommandOptions & {
10 attributes: VideoImportCreate & { torrentfile?: string }
11 }) {
12 const { attributes } = options
13 const path = '/api/v1/videos/imports'
14
15 let attaches: any = {}
16 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
17
18 return unwrapBody<VideoImport>(this.postUploadRequest({
19 ...options,
20
21 path,
22 attaches,
23 fields: options.attributes,
24 implicitToken: true,
25 defaultExpectedStatus: HttpStatusCode.OK_200
26 }))
27 }
28
29 getMyVideoImports (options: OverrideCommandOptions & {
30 sort?: string
31 } = {}) {
32 const { sort } = options
33 const path = '/api/v1/users/me/videos/imports'
34
35 const query = {}
36 if (sort) query['sort'] = sort
37
38 return this.getRequestBody<ResultList<VideoImport>>({
39 ...options,
40
41 path,
42 query: { sort },
43 implicitToken: true,
44 defaultExpectedStatus: HttpStatusCode.OK_200
45 })
46 }
47}
diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts
new file mode 100644
index 000000000..26e663f46
--- /dev/null
+++ b/shared/extra-utils/videos/index.ts
@@ -0,0 +1,19 @@
1export * from './blacklist-command'
2export * from './captions-command'
3export * from './captions'
4export * from './change-ownership-command'
5export * from './channels'
6export * from './channels-command'
7export * from './comments-command'
8export * from './history-command'
9export * from './imports-command'
10export * from './live-command'
11export * from './live'
12export * from './playlists-command'
13export * from './playlists'
14export * from './services-command'
15export * from './streaming-playlists-command'
16export * from './streaming-playlists'
17export * from './comments-command'
18export * from './videos-command'
19export * from './videos'
diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts
new file mode 100644
index 000000000..bf9486a05
--- /dev/null
+++ b/shared/extra-utils/videos/live-command.ts
@@ -0,0 +1,154 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { readdir } from 'fs-extra'
4import { omit } from 'lodash'
5import { join } from 'path'
6import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models'
7import { wait } from '../miscs'
8import { unwrapBody } from '../requests'
9import { AbstractCommand, OverrideCommandOptions } from '../shared'
10import { sendRTMPStream, testFfmpegStreamError } from './live'
11
12export class LiveCommand extends AbstractCommand {
13
14 get (options: OverrideCommandOptions & {
15 videoId: number | string
16 }) {
17 const path = '/api/v1/videos/live'
18
19 return this.getRequestBody<LiveVideo>({
20 ...options,
21
22 path: path + '/' + options.videoId,
23 implicitToken: true,
24 defaultExpectedStatus: HttpStatusCode.OK_200
25 })
26 }
27
28 update (options: OverrideCommandOptions & {
29 videoId: number | string
30 fields: LiveVideoUpdate
31 }) {
32 const { videoId, fields } = options
33 const path = '/api/v1/videos/live'
34
35 return this.putBodyRequest({
36 ...options,
37
38 path: path + '/' + videoId,
39 fields,
40 implicitToken: true,
41 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
42 })
43 }
44
45 async create (options: OverrideCommandOptions & {
46 fields: LiveVideoCreate
47 }) {
48 const { fields } = options
49 const path = '/api/v1/videos/live'
50
51 const attaches: any = {}
52 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
53 if (fields.previewfile) attaches.previewfile = fields.previewfile
54
55 const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
56 ...options,
57
58 path,
59 attaches,
60 fields: omit(fields, 'thumbnailfile', 'previewfile'),
61 implicitToken: true,
62 defaultExpectedStatus: HttpStatusCode.OK_200
63 }))
64
65 return body.video
66 }
67
68 async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
69 videoId: number | string
70 fixtureName?: string
71 }) {
72 const { videoId, fixtureName } = options
73 const videoLive = await this.get({ videoId })
74
75 return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
76 }
77
78 async runAndTestStreamError (options: OverrideCommandOptions & {
79 videoId: number | string
80 shouldHaveError: boolean
81 }) {
82 const command = await this.sendRTMPStreamInVideo(options)
83
84 return testFfmpegStreamError(command, options.shouldHaveError)
85 }
86
87 waitUntilPublished (options: OverrideCommandOptions & {
88 videoId: number | string
89 }) {
90 const { videoId } = options
91 return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
92 }
93
94 waitUntilWaiting (options: OverrideCommandOptions & {
95 videoId: number | string
96 }) {
97 const { videoId } = options
98 return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
99 }
100
101 waitUntilEnded (options: OverrideCommandOptions & {
102 videoId: number | string
103 }) {
104 const { videoId } = options
105 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
106 }
107
108 waitUntilSegmentGeneration (options: OverrideCommandOptions & {
109 videoUUID: string
110 resolution: number
111 segment: number
112 }) {
113 const { resolution, segment, videoUUID } = options
114 const segmentName = `${resolution}-00000${segment}.ts`
115
116 return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false)
117 }
118
119 async waitUntilSaved (options: OverrideCommandOptions & {
120 videoId: number | string
121 }) {
122 let video: VideoDetails
123
124 do {
125 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
126
127 await wait(500)
128 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
129 }
130
131 async countPlaylists (options: OverrideCommandOptions & {
132 videoUUID: string
133 }) {
134 const basePath = this.server.servers.buildDirectory('streaming-playlists')
135 const hlsPath = join(basePath, 'hls', options.videoUUID)
136
137 const files = await readdir(hlsPath)
138
139 return files.filter(f => f.endsWith('.m3u8')).length
140 }
141
142 private async waitUntilState (options: OverrideCommandOptions & {
143 videoId: number | string
144 state: VideoState
145 }) {
146 let video: VideoDetails
147
148 do {
149 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
150
151 await wait(500)
152 } while (video.state.id !== options.state)
153 }
154}
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts
index c0384769b..94f5f5b59 100644
--- a/shared/extra-utils/videos/live.ts
+++ b/shared/extra-utils/videos/live.ts
@@ -3,69 +3,9 @@
3import { expect } from 'chai' 3import { expect } from 'chai'
4import * as ffmpeg from 'fluent-ffmpeg' 4import * as ffmpeg from 'fluent-ffmpeg'
5import { pathExists, readdir } from 'fs-extra' 5import { pathExists, readdir } from 'fs-extra'
6import { omit } from 'lodash'
7import { join } from 'path' 6import { join } from 'path'
8import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoDetails, VideoState } from '@shared/models' 7import { buildAbsoluteFixturePath, wait } from '../miscs'
9import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes' 8import { PeerTubeServer } from '../server/server'
10import { buildAbsoluteFixturePath, buildServerDirectory, wait } from '../miscs/miscs'
11import { makeGetRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
12import { ServerInfo, waitUntilLog } from '../server/servers'
13import { getVideoWithToken } from './videos'
14
15function getLive (url: string, token: string, videoId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
16 const path = '/api/v1/videos/live'
17
18 return makeGetRequest({
19 url,
20 token,
21 path: path + '/' + videoId,
22 statusCodeExpected
23 })
24}
25
26function updateLive (
27 url: string,
28 token: string,
29 videoId: number | string,
30 fields: LiveVideoUpdate,
31 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
32) {
33 const path = '/api/v1/videos/live'
34
35 return makePutBodyRequest({
36 url,
37 token,
38 path: path + '/' + videoId,
39 fields,
40 statusCodeExpected
41 })
42}
43
44function createLive (url: string, token: string, fields: LiveVideoCreate, statusCodeExpected = HttpStatusCode.OK_200) {
45 const path = '/api/v1/videos/live'
46
47 const attaches: any = {}
48 if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
49 if (fields.previewfile) attaches.previewfile = fields.previewfile
50
51 const updatedFields = omit(fields, 'thumbnailfile', 'previewfile')
52
53 return makeUploadRequest({
54 url,
55 path,
56 token,
57 attaches,
58 fields: updatedFields,
59 statusCodeExpected
60 })
61}
62
63async function sendRTMPStreamInVideo (url: string, token: string, videoId: number | string, fixtureName?: string) {
64 const res = await getLive(url, token, videoId)
65 const videoLive = res.body as LiveVideo
66
67 return sendRTMPStream(videoLive.rtmpUrl, videoLive.streamKey, fixtureName)
68}
69 9
70function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') { 10function sendRTMPStream (rtmpBaseUrl: string, streamKey: string, fixtureName = 'video_short.mp4') {
71 const fixture = buildAbsoluteFixturePath(fixtureName) 11 const fixture = buildAbsoluteFixturePath(fixtureName)
@@ -109,12 +49,6 @@ function waitFfmpegUntilError (command: ffmpeg.FfmpegCommand, successAfterMS = 1
109 }) 49 })
110} 50}
111 51
112async function runAndTestFfmpegStreamError (url: string, token: string, videoId: number | string, shouldHaveError: boolean) {
113 const command = await sendRTMPStreamInVideo(url, token, videoId)
114
115 return testFfmpegStreamError(command, shouldHaveError)
116}
117
118async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) { 52async function testFfmpegStreamError (command: ffmpeg.FfmpegCommand, shouldHaveError: boolean) {
119 let error: Error 53 let error: Error
120 54
@@ -136,53 +70,14 @@ async function stopFfmpeg (command: ffmpeg.FfmpegCommand) {
136 await wait(500) 70 await wait(500)
137} 71}
138 72
139function waitUntilLivePublished (url: string, token: string, videoId: number | string) { 73async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
140 return waitUntilLiveState(url, token, videoId, VideoState.PUBLISHED)
141}
142
143function waitUntilLiveWaiting (url: string, token: string, videoId: number | string) {
144 return waitUntilLiveState(url, token, videoId, VideoState.WAITING_FOR_LIVE)
145}
146
147function waitUntilLiveEnded (url: string, token: string, videoId: number | string) {
148 return waitUntilLiveState(url, token, videoId, VideoState.LIVE_ENDED)
149}
150
151function waitUntilLiveSegmentGeneration (server: ServerInfo, videoUUID: string, resolutionNum: number, segmentNum: number) {
152 const segmentName = `${resolutionNum}-00000${segmentNum}.ts`
153 return waitUntilLog(server, `${videoUUID}/${segmentName}`, 2, false)
154}
155
156async function waitUntilLiveState (url: string, token: string, videoId: number | string, state: VideoState) {
157 let video: VideoDetails
158
159 do {
160 const res = await getVideoWithToken(url, token, videoId)
161 video = res.body
162
163 await wait(500)
164 } while (video.state.id !== state)
165}
166
167async function waitUntilLiveSaved (url: string, token: string, videoId: number | string) {
168 let video: VideoDetails
169
170 do {
171 const res = await getVideoWithToken(url, token, videoId)
172 video = res.body
173
174 await wait(500)
175 } while (video.isLive === true && video.state.id !== VideoState.PUBLISHED)
176}
177
178async function waitUntilLivePublishedOnAllServers (servers: ServerInfo[], videoId: string) {
179 for (const server of servers) { 74 for (const server of servers) {
180 await waitUntilLivePublished(server.url, server.accessToken, videoId) 75 await server.live.waitUntilPublished({ videoId })
181 } 76 }
182} 77}
183 78
184async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resolutions: number[] = []) { 79async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) {
185 const basePath = buildServerDirectory(server, 'streaming-playlists') 80 const basePath = server.servers.buildDirectory('streaming-playlists')
186 const hlsPath = join(basePath, 'hls', videoUUID) 81 const hlsPath = join(basePath, 'hls', videoUUID)
187 82
188 if (resolutions.length === 0) { 83 if (resolutions.length === 0) {
@@ -198,41 +93,25 @@ async function checkLiveCleanup (server: ServerInfo, videoUUID: string, resoluti
198 expect(files).to.have.lengthOf(resolutions.length * 2 + 2) 93 expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
199 94
200 for (const resolution of resolutions) { 95 for (const resolution of resolutions) {
201 expect(files).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`) 96 const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`))
202 expect(files).to.contain(`${resolution}.m3u8`) 97 expect(fragmentedFile).to.exist
203 }
204
205 expect(files).to.contain('master.m3u8')
206 expect(files).to.contain('segments-sha256.json')
207}
208 98
209async function getPlaylistsCount (server: ServerInfo, videoUUID: string) { 99 const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`))
210 const basePath = buildServerDirectory(server, 'streaming-playlists') 100 expect(playlistFile).to.exist
211 const hlsPath = join(basePath, 'hls', videoUUID) 101 }
212 102
213 const files = await readdir(hlsPath) 103 const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8'))
104 expect(masterPlaylistFile).to.exist
214 105
215 return files.filter(f => f.endsWith('.m3u8')).length 106 const shaFile = files.find(f => f.endsWith('-segments-sha256.json'))
107 expect(shaFile).to.exist
216} 108}
217 109
218// ---------------------------------------------------------------------------
219
220export { 110export {
221 getLive, 111 sendRTMPStream,
222 getPlaylistsCount,
223 waitUntilLiveSaved,
224 waitUntilLivePublished,
225 updateLive,
226 createLive,
227 runAndTestFfmpegStreamError,
228 checkLiveCleanup,
229 waitUntilLiveSegmentGeneration,
230 stopFfmpeg,
231 waitUntilLiveWaiting,
232 sendRTMPStreamInVideo,
233 waitUntilLiveEnded,
234 waitFfmpegUntilError, 112 waitFfmpegUntilError,
113 testFfmpegStreamError,
114 stopFfmpeg,
235 waitUntilLivePublishedOnAllServers, 115 waitUntilLivePublishedOnAllServers,
236 sendRTMPStream, 116 checkLiveCleanupAfterSave
237 testFfmpegStreamError
238} 117}
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
new file mode 100644
index 000000000..ce23900d3
--- /dev/null
+++ b/shared/extra-utils/videos/playlists-command.ts
@@ -0,0 +1,280 @@
1import { omit } from 'lodash'
2import { pick } from '@shared/core-utils'
3import {
4 BooleanBothQuery,
5 HttpStatusCode,
6 ResultList,
7 VideoExistInPlaylist,
8 VideoPlaylist,
9 VideoPlaylistCreate,
10 VideoPlaylistCreateResult,
11 VideoPlaylistElement,
12 VideoPlaylistElementCreate,
13 VideoPlaylistElementCreateResult,
14 VideoPlaylistElementUpdate,
15 VideoPlaylistReorder,
16 VideoPlaylistType,
17 VideoPlaylistUpdate
18} from '@shared/models'
19import { unwrapBody } from '../requests'
20import { AbstractCommand, OverrideCommandOptions } from '../shared'
21
22export class PlaylistsCommand extends AbstractCommand {
23
24 list (options: OverrideCommandOptions & {
25 start?: number
26 count?: number
27 sort?: string
28 }) {
29 const path = '/api/v1/video-playlists'
30 const query = pick(options, [ 'start', 'count', 'sort' ])
31
32 return this.getRequestBody<ResultList<VideoPlaylist>>({
33 ...options,
34
35 path,
36 query,
37 implicitToken: false,
38 defaultExpectedStatus: HttpStatusCode.OK_200
39 })
40 }
41
42 listByChannel (options: OverrideCommandOptions & {
43 handle: string
44 start?: number
45 count?: number
46 sort?: string
47 }) {
48 const path = '/api/v1/video-channels/' + options.handle + '/video-playlists'
49 const query = pick(options, [ 'start', 'count', 'sort' ])
50
51 return this.getRequestBody<ResultList<VideoPlaylist>>({
52 ...options,
53
54 path,
55 query,
56 implicitToken: false,
57 defaultExpectedStatus: HttpStatusCode.OK_200
58 })
59 }
60
61 listByAccount (options: OverrideCommandOptions & {
62 handle: string
63 start?: number
64 count?: number
65 sort?: string
66 search?: string
67 playlistType?: VideoPlaylistType
68 }) {
69 const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
70 const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ])
71
72 return this.getRequestBody<ResultList<VideoPlaylist>>({
73 ...options,
74
75 path,
76 query,
77 implicitToken: false,
78 defaultExpectedStatus: HttpStatusCode.OK_200
79 })
80 }
81
82 get (options: OverrideCommandOptions & {
83 playlistId: number | string
84 }) {
85 const { playlistId } = options
86 const path = '/api/v1/video-playlists/' + playlistId
87
88 return this.getRequestBody<VideoPlaylist>({
89 ...options,
90
91 path,
92 implicitToken: false,
93 defaultExpectedStatus: HttpStatusCode.OK_200
94 })
95 }
96
97 listVideos (options: OverrideCommandOptions & {
98 playlistId: number | string
99 start?: number
100 count?: number
101 query?: { nsfw?: BooleanBothQuery }
102 }) {
103 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
104 const query = options.query ?? {}
105
106 return this.getRequestBody<ResultList<VideoPlaylistElement>>({
107 ...options,
108
109 path,
110 query: {
111 ...query,
112 start: options.start,
113 count: options.count
114 },
115 implicitToken: true,
116 defaultExpectedStatus: HttpStatusCode.OK_200
117 })
118 }
119
120 delete (options: OverrideCommandOptions & {
121 playlistId: number | string
122 }) {
123 const path = '/api/v1/video-playlists/' + options.playlistId
124
125 return this.deleteRequest({
126 ...options,
127
128 path,
129 implicitToken: true,
130 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
131 })
132 }
133
134 async create (options: OverrideCommandOptions & {
135 attributes: VideoPlaylistCreate
136 }) {
137 const path = '/api/v1/video-playlists'
138
139 const fields = omit(options.attributes, 'thumbnailfile')
140
141 const attaches = options.attributes.thumbnailfile
142 ? { thumbnailfile: options.attributes.thumbnailfile }
143 : {}
144
145 const body = await unwrapBody<{ videoPlaylist: VideoPlaylistCreateResult }>(this.postUploadRequest({
146 ...options,
147
148 path,
149 fields,
150 attaches,
151 implicitToken: true,
152 defaultExpectedStatus: HttpStatusCode.OK_200
153 }))
154
155 return body.videoPlaylist
156 }
157
158 update (options: OverrideCommandOptions & {
159 attributes: VideoPlaylistUpdate
160 playlistId: number | string
161 }) {
162 const path = '/api/v1/video-playlists/' + options.playlistId
163
164 const fields = omit(options.attributes, 'thumbnailfile')
165
166 const attaches = options.attributes.thumbnailfile
167 ? { thumbnailfile: options.attributes.thumbnailfile }
168 : {}
169
170 return this.putUploadRequest({
171 ...options,
172
173 path,
174 fields,
175 attaches,
176 implicitToken: true,
177 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
178 })
179 }
180
181 async addElement (options: OverrideCommandOptions & {
182 playlistId: number | string
183 attributes: VideoPlaylistElementCreate | { videoId: string }
184 }) {
185 const attributes = {
186 ...options.attributes,
187
188 videoId: await this.server.videos.getId({ ...options, uuid: options.attributes.videoId })
189 }
190
191 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
192
193 const body = await unwrapBody<{ videoPlaylistElement: VideoPlaylistElementCreateResult }>(this.postBodyRequest({
194 ...options,
195
196 path,
197 fields: attributes,
198 implicitToken: true,
199 defaultExpectedStatus: HttpStatusCode.OK_200
200 }))
201
202 return body.videoPlaylistElement
203 }
204
205 updateElement (options: OverrideCommandOptions & {
206 playlistId: number | string
207 elementId: number | string
208 attributes: VideoPlaylistElementUpdate
209 }) {
210 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
211
212 return this.putBodyRequest({
213 ...options,
214
215 path,
216 fields: options.attributes,
217 implicitToken: true,
218 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
219 })
220 }
221
222 removeElement (options: OverrideCommandOptions & {
223 playlistId: number | string
224 elementId: number
225 }) {
226 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
227
228 return this.deleteRequest({
229 ...options,
230
231 path,
232 implicitToken: true,
233 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
234 })
235 }
236
237 reorderElements (options: OverrideCommandOptions & {
238 playlistId: number | string
239 attributes: VideoPlaylistReorder
240 }) {
241 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
242
243 return this.postBodyRequest({
244 ...options,
245
246 path,
247 fields: options.attributes,
248 implicitToken: true,
249 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
250 })
251 }
252
253 getPrivacies (options: OverrideCommandOptions = {}) {
254 const path = '/api/v1/video-playlists/privacies'
255
256 return this.getRequestBody<{ [ id: number ]: string }>({
257 ...options,
258
259 path,
260 implicitToken: false,
261 defaultExpectedStatus: HttpStatusCode.OK_200
262 })
263 }
264
265 videosExist (options: OverrideCommandOptions & {
266 videoIds: number[]
267 }) {
268 const { videoIds } = options
269 const path = '/api/v1/users/me/video-playlists/videos-exist'
270
271 return this.getRequestBody<VideoExistInPlaylist>({
272 ...options,
273
274 path,
275 query: { videoIds },
276 implicitToken: true,
277 defaultExpectedStatus: HttpStatusCode.OK_200
278 })
279 }
280}
diff --git a/shared/extra-utils/videos/playlists.ts b/shared/extra-utils/videos/playlists.ts
new file mode 100644
index 000000000..3dde52bb9
--- /dev/null
+++ b/shared/extra-utils/videos/playlists.ts
@@ -0,0 +1,25 @@
1import { expect } from 'chai'
2import { readdir } from 'fs-extra'
3import { join } from 'path'
4import { root } from '../miscs'
5
6async function checkPlaylistFilesWereRemoved (
7 playlistUUID: string,
8 internalServerNumber: number,
9 directories = [ 'thumbnails' ]
10) {
11 const testDirectory = 'test' + internalServerNumber
12
13 for (const directory of directories) {
14 const directoryPath = join(root(), testDirectory, directory)
15
16 const files = await readdir(directoryPath)
17 for (const file of files) {
18 expect(file).to.not.contain(playlistUUID)
19 }
20 }
21}
22
23export {
24 checkPlaylistFilesWereRemoved
25}
diff --git a/shared/extra-utils/videos/services-command.ts b/shared/extra-utils/videos/services-command.ts
new file mode 100644
index 000000000..06760df42
--- /dev/null
+++ b/shared/extra-utils/videos/services-command.ts
@@ -0,0 +1,29 @@
1import { HttpStatusCode } from '@shared/models'
2import { AbstractCommand, OverrideCommandOptions } from '../shared'
3
4export class ServicesCommand extends AbstractCommand {
5
6 getOEmbed (options: OverrideCommandOptions & {
7 oembedUrl: string
8 format?: string
9 maxHeight?: number
10 maxWidth?: number
11 }) {
12 const path = '/services/oembed'
13 const query = {
14 url: options.oembedUrl,
15 format: options.format,
16 maxheight: options.maxHeight,
17 maxwidth: options.maxWidth
18 }
19
20 return this.getRequest({
21 ...options,
22
23 path,
24 query,
25 implicitToken: false,
26 defaultExpectedStatus: HttpStatusCode.OK_200
27 })
28 }
29}
diff --git a/shared/extra-utils/videos/services.ts b/shared/extra-utils/videos/services.ts
deleted file mode 100644
index e13a788bd..000000000
--- a/shared/extra-utils/videos/services.ts
+++ /dev/null
@@ -1,24 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
5 const path = '/services/oembed'
6 const query = {
7 url: oembedUrl,
8 format,
9 maxheight: maxHeight,
10 maxwidth: maxWidth
11 }
12
13 return request(url)
14 .get(path)
15 .query(query)
16 .set('Accept', 'application/json')
17 .expect(HttpStatusCode.OK_200)
18}
19
20// ---------------------------------------------------------------------------
21
22export {
23 getOEmbed
24}
diff --git a/shared/extra-utils/videos/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts
new file mode 100644
index 000000000..9662685da
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists-command.ts
@@ -0,0 +1,44 @@
1import { HttpStatusCode } from '@shared/models'
2import { unwrapBody, unwrapText } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class StreamingPlaylistsCommand extends AbstractCommand {
6
7 get (options: OverrideCommandOptions & {
8 url: string
9 }) {
10 return unwrapText(this.getRawRequest({
11 ...options,
12
13 url: options.url,
14 implicitToken: false,
15 defaultExpectedStatus: HttpStatusCode.OK_200
16 }))
17 }
18
19 getSegment (options: OverrideCommandOptions & {
20 url: string
21 range?: string
22 }) {
23 return unwrapBody<Buffer>(this.getRawRequest({
24 ...options,
25
26 url: options.url,
27 range: options.range,
28 implicitToken: false,
29 defaultExpectedStatus: HttpStatusCode.OK_200
30 }))
31 }
32
33 getSegmentSha256 (options: OverrideCommandOptions & {
34 url: string
35 }) {
36 return unwrapBody<{ [ id: string ]: string }>(this.getRawRequest({
37 ...options,
38
39 url: options.url,
40 implicitToken: false,
41 defaultExpectedStatus: HttpStatusCode.OK_200
42 }))
43 }
44}
diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts
new file mode 100644
index 000000000..a224b8f5f
--- /dev/null
+++ b/shared/extra-utils/videos/streaming-playlists.ts
@@ -0,0 +1,78 @@
1import { expect } from 'chai'
2import { basename } from 'path'
3import { sha256 } from '@server/helpers/core-utils'
4import { removeFragmentedMP4Ext } from '@shared/core-utils'
5import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models'
6import { PeerTubeServer } from '../server'
7
8async function checkSegmentHash (options: {
9 server: PeerTubeServer
10 baseUrlPlaylist: string
11 baseUrlSegment: string
12 videoUUID: string
13 resolution: number
14 hlsPlaylist: VideoStreamingPlaylist
15}) {
16 const { server, baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist } = options
17 const command = server.streamingPlaylists
18
19 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
20 const videoName = basename(file.fileUrl)
21
22 const playlist = await command.get({ url: `${baseUrlPlaylist}/${videoUUID}/${removeFragmentedMP4Ext(videoName)}.m3u8` })
23
24 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
25
26 const length = parseInt(matches[1], 10)
27 const offset = parseInt(matches[2], 10)
28 const range = `${offset}-${offset + length - 1}`
29
30 const segmentBody = await command.getSegment({
31 url: `${baseUrlSegment}/${videoUUID}/${videoName}`,
32 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
33 range: `bytes=${range}`
34 })
35
36 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
37 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
38}
39
40async function checkLiveSegmentHash (options: {
41 server: PeerTubeServer
42 baseUrlSegment: string
43 videoUUID: string
44 segmentName: string
45 hlsPlaylist: VideoStreamingPlaylist
46}) {
47 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
48 const command = server.streamingPlaylists
49
50 const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
51 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
52
53 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
54}
55
56async function checkResolutionsInMasterPlaylist (options: {
57 server: PeerTubeServer
58 playlistUrl: string
59 resolutions: number[]
60}) {
61 const { server, playlistUrl, resolutions } = options
62
63 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl })
64
65 for (const resolution of resolutions) {
66 const reg = new RegExp(
67 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
68 )
69
70 expect(masterPlaylist).to.match(reg)
71 }
72}
73
74export {
75 checkSegmentHash,
76 checkLiveSegmentHash,
77 checkResolutionsInMasterPlaylist
78}
diff --git a/shared/extra-utils/videos/video-blacklist.ts b/shared/extra-utils/videos/video-blacklist.ts
deleted file mode 100644
index aa1548537..000000000
--- a/shared/extra-utils/videos/video-blacklist.ts
+++ /dev/null
@@ -1,79 +0,0 @@
1import * as request from 'supertest'
2import { VideoBlacklistType } from '../../models/videos'
3import { makeGetRequest } from '..'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function addVideoToBlacklist (
7 url: string,
8 token: string,
9 videoId: number | string,
10 reason?: string,
11 unfederate?: boolean,
12 specialStatus = HttpStatusCode.NO_CONTENT_204
13) {
14 const path = '/api/v1/videos/' + videoId + '/blacklist'
15
16 return request(url)
17 .post(path)
18 .send({ reason, unfederate })
19 .set('Accept', 'application/json')
20 .set('Authorization', 'Bearer ' + token)
21 .expect(specialStatus)
22}
23
24function updateVideoBlacklist (
25 url: string,
26 token: string,
27 videoId: number,
28 reason?: string,
29 specialStatus = HttpStatusCode.NO_CONTENT_204
30) {
31 const path = '/api/v1/videos/' + videoId + '/blacklist'
32
33 return request(url)
34 .put(path)
35 .send({ reason })
36 .set('Accept', 'application/json')
37 .set('Authorization', 'Bearer ' + token)
38 .expect(specialStatus)
39}
40
41function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
42 const path = '/api/v1/videos/' + videoId + '/blacklist'
43
44 return request(url)
45 .delete(path)
46 .set('Accept', 'application/json')
47 .set('Authorization', 'Bearer ' + token)
48 .expect(specialStatus)
49}
50
51function getBlacklistedVideosList (parameters: {
52 url: string
53 token: string
54 sort?: string
55 type?: VideoBlacklistType
56 specialStatus?: HttpStatusCode
57}) {
58 const { url, token, sort, type, specialStatus = HttpStatusCode.OK_200 } = parameters
59 const path = '/api/v1/videos/blacklist/'
60
61 const query = { sort, type }
62
63 return makeGetRequest({
64 url,
65 path,
66 query,
67 token,
68 statusCodeExpected: specialStatus
69 })
70}
71
72// ---------------------------------------------------------------------------
73
74export {
75 addVideoToBlacklist,
76 removeVideoFromBlacklist,
77 getBlacklistedVideosList,
78 updateVideoBlacklist
79}
diff --git a/shared/extra-utils/videos/video-captions.ts b/shared/extra-utils/videos/video-captions.ts
deleted file mode 100644
index 62eec7b90..000000000
--- a/shared/extra-utils/videos/video-captions.ts
+++ /dev/null
@@ -1,72 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makeUploadRequest } from '../requests/requests'
2import * as request from 'supertest'
3import * as chai from 'chai'
4import { buildAbsoluteFixturePath } from '../miscs/miscs'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7const expect = chai.expect
8
9function createVideoCaption (args: {
10 url: string
11 accessToken: string
12 videoId: string | number
13 language: string
14 fixture: string
15 mimeType?: string
16 statusCodeExpected?: number
17}) {
18 const path = '/api/v1/videos/' + args.videoId + '/captions/' + args.language
19
20 const captionfile = buildAbsoluteFixturePath(args.fixture)
21 const captionfileAttach = args.mimeType ? [ captionfile, { contentType: args.mimeType } ] : captionfile
22
23 return makeUploadRequest({
24 method: 'PUT',
25 url: args.url,
26 path,
27 token: args.accessToken,
28 fields: {},
29 attaches: {
30 captionfile: captionfileAttach
31 },
32 statusCodeExpected: args.statusCodeExpected || HttpStatusCode.NO_CONTENT_204
33 })
34}
35
36function listVideoCaptions (url: string, videoId: string | number) {
37 const path = '/api/v1/videos/' + videoId + '/captions'
38
39 return makeGetRequest({
40 url,
41 path,
42 statusCodeExpected: HttpStatusCode.OK_200
43 })
44}
45
46function deleteVideoCaption (url: string, token: string, videoId: string | number, language: string) {
47 const path = '/api/v1/videos/' + videoId + '/captions/' + language
48
49 return makeDeleteRequest({
50 url,
51 token,
52 path,
53 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
54 })
55}
56
57async function testCaptionFile (url: string, captionPath: string, containsString: string) {
58 const res = await request(url)
59 .get(captionPath)
60 .expect(HttpStatusCode.OK_200)
61
62 expect(res.text).to.contain(containsString)
63}
64
65// ---------------------------------------------------------------------------
66
67export {
68 createVideoCaption,
69 listVideoCaptions,
70 testCaptionFile,
71 deleteVideoCaption
72}
diff --git a/shared/extra-utils/videos/video-change-ownership.ts b/shared/extra-utils/videos/video-change-ownership.ts
deleted file mode 100644
index ef82a7636..000000000
--- a/shared/extra-utils/videos/video-change-ownership.ts
+++ /dev/null
@@ -1,72 +0,0 @@
1import * as request from 'supertest'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function changeVideoOwnership (
5 url: string,
6 token: string,
7 videoId: number | string,
8 username,
9 expectedStatus = HttpStatusCode.NO_CONTENT_204
10) {
11 const path = '/api/v1/videos/' + videoId + '/give-ownership'
12
13 return request(url)
14 .post(path)
15 .set('Accept', 'application/json')
16 .set('Authorization', 'Bearer ' + token)
17 .send({ username })
18 .expect(expectedStatus)
19}
20
21function getVideoChangeOwnershipList (url: string, token: string) {
22 const path = '/api/v1/videos/ownership'
23
24 return request(url)
25 .get(path)
26 .query({ sort: '-createdAt' })
27 .set('Accept', 'application/json')
28 .set('Authorization', 'Bearer ' + token)
29 .expect(HttpStatusCode.OK_200)
30 .expect('Content-Type', /json/)
31}
32
33function acceptChangeOwnership (
34 url: string,
35 token: string,
36 ownershipId: string,
37 channelId: number,
38 expectedStatus = HttpStatusCode.NO_CONTENT_204
39) {
40 const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
41
42 return request(url)
43 .post(path)
44 .set('Accept', 'application/json')
45 .set('Authorization', 'Bearer ' + token)
46 .send({ channelId })
47 .expect(expectedStatus)
48}
49
50function refuseChangeOwnership (
51 url: string,
52 token: string,
53 ownershipId: string,
54 expectedStatus = HttpStatusCode.NO_CONTENT_204
55) {
56 const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
57
58 return request(url)
59 .post(path)
60 .set('Accept', 'application/json')
61 .set('Authorization', 'Bearer ' + token)
62 .expect(expectedStatus)
63}
64
65// ---------------------------------------------------------------------------
66
67export {
68 changeVideoOwnership,
69 getVideoChangeOwnershipList,
70 acceptChangeOwnership,
71 refuseChangeOwnership
72}
diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts
deleted file mode 100644
index 0aab93e52..000000000
--- a/shared/extra-utils/videos/video-channels.ts
+++ /dev/null
@@ -1,192 +0,0 @@
1/* eslint-disable @typescript-eslint/no-floating-promises */
2
3import * as request from 'supertest'
4import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model'
5import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model'
6import { makeDeleteRequest, makeGetRequest, updateImageRequest } from '../requests/requests'
7import { ServerInfo } from '../server/servers'
8import { MyUser, User } from '../../models/users/user.model'
9import { getMyUserInformation } from '../users/users'
10import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
11
12function getVideoChannelsList (url: string, start: number, count: number, sort?: string, withStats?: boolean) {
13 const path = '/api/v1/video-channels'
14
15 const req = request(url)
16 .get(path)
17 .query({ start: start })
18 .query({ count: count })
19
20 if (sort) req.query({ sort })
21 if (withStats) req.query({ withStats })
22
23 return req.set('Accept', 'application/json')
24 .expect(HttpStatusCode.OK_200)
25 .expect('Content-Type', /json/)
26}
27
28function getAccountVideoChannelsList (parameters: {
29 url: string
30 accountName: string
31 start?: number
32 count?: number
33 sort?: string
34 specialStatus?: HttpStatusCode
35 withStats?: boolean
36 search?: string
37}) {
38 const {
39 url,
40 accountName,
41 start,
42 count,
43 sort = 'createdAt',
44 specialStatus = HttpStatusCode.OK_200,
45 withStats = false,
46 search
47 } = parameters
48
49 const path = '/api/v1/accounts/' + accountName + '/video-channels'
50
51 return makeGetRequest({
52 url,
53 path,
54 query: {
55 start,
56 count,
57 sort,
58 withStats,
59 search
60 },
61 statusCodeExpected: specialStatus
62 })
63}
64
65function addVideoChannel (
66 url: string,
67 token: string,
68 videoChannelAttributesArg: VideoChannelCreate,
69 expectedStatus = HttpStatusCode.OK_200
70) {
71 const path = '/api/v1/video-channels/'
72
73 // Default attributes
74 let attributes = {
75 displayName: 'my super video channel',
76 description: 'my super channel description',
77 support: 'my super channel support'
78 }
79 attributes = Object.assign(attributes, videoChannelAttributesArg)
80
81 return request(url)
82 .post(path)
83 .send(attributes)
84 .set('Accept', 'application/json')
85 .set('Authorization', 'Bearer ' + token)
86 .expect(expectedStatus)
87}
88
89function updateVideoChannel (
90 url: string,
91 token: string,
92 channelName: string,
93 attributes: VideoChannelUpdate,
94 expectedStatus = HttpStatusCode.NO_CONTENT_204
95) {
96 const body: any = {}
97 const path = '/api/v1/video-channels/' + channelName
98
99 if (attributes.displayName) body.displayName = attributes.displayName
100 if (attributes.description) body.description = attributes.description
101 if (attributes.support) body.support = attributes.support
102 if (attributes.bulkVideosSupportUpdate) body.bulkVideosSupportUpdate = attributes.bulkVideosSupportUpdate
103
104 return request(url)
105 .put(path)
106 .send(body)
107 .set('Accept', 'application/json')
108 .set('Authorization', 'Bearer ' + token)
109 .expect(expectedStatus)
110}
111
112function deleteVideoChannel (url: string, token: string, channelName: string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
113 const path = '/api/v1/video-channels/' + channelName
114
115 return request(url)
116 .delete(path)
117 .set('Accept', 'application/json')
118 .set('Authorization', 'Bearer ' + token)
119 .expect(expectedStatus)
120}
121
122function getVideoChannel (url: string, channelName: string) {
123 const path = '/api/v1/video-channels/' + channelName
124
125 return request(url)
126 .get(path)
127 .set('Accept', 'application/json')
128 .expect(HttpStatusCode.OK_200)
129 .expect('Content-Type', /json/)
130}
131
132function updateVideoChannelImage (options: {
133 url: string
134 accessToken: string
135 fixture: string
136 videoChannelName: string | number
137 type: 'avatar' | 'banner'
138}) {
139 const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}/pick`
140
141 return updateImageRequest({ ...options, path, fieldname: options.type + 'file' })
142}
143
144function deleteVideoChannelImage (options: {
145 url: string
146 accessToken: string
147 videoChannelName: string | number
148 type: 'avatar' | 'banner'
149}) {
150 const path = `/api/v1/video-channels/${options.videoChannelName}/${options.type}`
151
152 return makeDeleteRequest({
153 url: options.url,
154 token: options.accessToken,
155 path,
156 statusCodeExpected: 204
157 })
158}
159
160function setDefaultVideoChannel (servers: ServerInfo[]) {
161 const tasks: Promise<any>[] = []
162
163 for (const server of servers) {
164 const p = getMyUserInformation(server.url, server.accessToken)
165 .then(res => { server.videoChannel = (res.body as User).videoChannels[0] })
166
167 tasks.push(p)
168 }
169
170 return Promise.all(tasks)
171}
172
173async function getDefaultVideoChannel (url: string, token: string) {
174 const res = await getMyUserInformation(url, token)
175
176 return (res.body as MyUser).videoChannels[0].id
177}
178
179// ---------------------------------------------------------------------------
180
181export {
182 updateVideoChannelImage,
183 getVideoChannelsList,
184 getAccountVideoChannelsList,
185 addVideoChannel,
186 updateVideoChannel,
187 deleteVideoChannel,
188 getVideoChannel,
189 setDefaultVideoChannel,
190 deleteVideoChannelImage,
191 getDefaultVideoChannel
192}
diff --git a/shared/extra-utils/videos/video-comments.ts b/shared/extra-utils/videos/video-comments.ts
deleted file mode 100644
index 71b9f875a..000000000
--- a/shared/extra-utils/videos/video-comments.ts
+++ /dev/null
@@ -1,138 +0,0 @@
1/* eslint-disable @typescript-eslint/no-floating-promises */
2
3import * as request from 'supertest'
4import { makeDeleteRequest, makeGetRequest } from '../requests/requests'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7function getAdminVideoComments (options: {
8 url: string
9 token: string
10 start: number
11 count: number
12 sort?: string
13 isLocal?: boolean
14 search?: string
15 searchAccount?: string
16 searchVideo?: string
17}) {
18 const { url, token, start, count, sort, isLocal, search, searchAccount, searchVideo } = options
19 const path = '/api/v1/videos/comments'
20
21 const query = {
22 start,
23 count,
24 sort: sort || '-createdAt'
25 }
26
27 if (isLocal !== undefined) Object.assign(query, { isLocal })
28 if (search !== undefined) Object.assign(query, { search })
29 if (searchAccount !== undefined) Object.assign(query, { searchAccount })
30 if (searchVideo !== undefined) Object.assign(query, { searchVideo })
31
32 return makeGetRequest({
33 url,
34 path,
35 token,
36 query,
37 statusCodeExpected: HttpStatusCode.OK_200
38 })
39}
40
41function getVideoCommentThreads (url: string, videoId: number | string, start: number, count: number, sort?: string, token?: string) {
42 const path = '/api/v1/videos/' + videoId + '/comment-threads'
43
44 const req = request(url)
45 .get(path)
46 .query({ start: start })
47 .query({ count: count })
48
49 if (sort) req.query({ sort })
50 if (token) req.set('Authorization', 'Bearer ' + token)
51
52 return req.set('Accept', 'application/json')
53 .expect(HttpStatusCode.OK_200)
54 .expect('Content-Type', /json/)
55}
56
57function getVideoThreadComments (url: string, videoId: number | string, threadId: number, token?: string) {
58 const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
59
60 const req = request(url)
61 .get(path)
62 .set('Accept', 'application/json')
63
64 if (token) req.set('Authorization', 'Bearer ' + token)
65
66 return req.expect(HttpStatusCode.OK_200)
67 .expect('Content-Type', /json/)
68}
69
70function addVideoCommentThread (
71 url: string,
72 token: string,
73 videoId: number | string,
74 text: string,
75 expectedStatus = HttpStatusCode.OK_200
76) {
77 const path = '/api/v1/videos/' + videoId + '/comment-threads'
78
79 return request(url)
80 .post(path)
81 .send({ text })
82 .set('Accept', 'application/json')
83 .set('Authorization', 'Bearer ' + token)
84 .expect(expectedStatus)
85}
86
87function addVideoCommentReply (
88 url: string,
89 token: string,
90 videoId: number | string,
91 inReplyToCommentId: number,
92 text: string,
93 expectedStatus = HttpStatusCode.OK_200
94) {
95 const path = '/api/v1/videos/' + videoId + '/comments/' + inReplyToCommentId
96
97 return request(url)
98 .post(path)
99 .send({ text })
100 .set('Accept', 'application/json')
101 .set('Authorization', 'Bearer ' + token)
102 .expect(expectedStatus)
103}
104
105async function findCommentId (url: string, videoId: number | string, text: string) {
106 const res = await getVideoCommentThreads(url, videoId, 0, 25, '-createdAt')
107
108 return res.body.data.find(c => c.text === text).id as number
109}
110
111function deleteVideoComment (
112 url: string,
113 token: string,
114 videoId: number | string,
115 commentId: number,
116 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
117) {
118 const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
119
120 return makeDeleteRequest({
121 url,
122 path,
123 token,
124 statusCodeExpected
125 })
126}
127
128// ---------------------------------------------------------------------------
129
130export {
131 getVideoCommentThreads,
132 getAdminVideoComments,
133 getVideoThreadComments,
134 addVideoCommentThread,
135 addVideoCommentReply,
136 findCommentId,
137 deleteVideoComment
138}
diff --git a/shared/extra-utils/videos/video-history.ts b/shared/extra-utils/videos/video-history.ts
deleted file mode 100644
index b989e14dc..000000000
--- a/shared/extra-utils/videos/video-history.ts
+++ /dev/null
@@ -1,49 +0,0 @@
1import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
2import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
3
4function userWatchVideo (
5 url: string,
6 token: string,
7 videoId: number | string,
8 currentTime: number,
9 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
10) {
11 const path = '/api/v1/videos/' + videoId + '/watching'
12 const fields = { currentTime }
13
14 return makePutBodyRequest({ url, path, token, fields, statusCodeExpected })
15}
16
17function listMyVideosHistory (url: string, token: string, search?: string) {
18 const path = '/api/v1/users/me/history/videos'
19
20 return makeGetRequest({
21 url,
22 path,
23 token,
24 query: {
25 search
26 },
27 statusCodeExpected: HttpStatusCode.OK_200
28 })
29}
30
31function removeMyVideosHistory (url: string, token: string, beforeDate?: string) {
32 const path = '/api/v1/users/me/history/videos/remove'
33
34 return makePostBodyRequest({
35 url,
36 path,
37 token,
38 fields: beforeDate ? { beforeDate } : {},
39 statusCodeExpected: HttpStatusCode.NO_CONTENT_204
40 })
41}
42
43// ---------------------------------------------------------------------------
44
45export {
46 userWatchVideo,
47 listMyVideosHistory,
48 removeMyVideosHistory
49}
diff --git a/shared/extra-utils/videos/video-imports.ts b/shared/extra-utils/videos/video-imports.ts
deleted file mode 100644
index 81c0163cb..000000000
--- a/shared/extra-utils/videos/video-imports.ts
+++ /dev/null
@@ -1,90 +0,0 @@
1
2import { VideoImportCreate } from '../../models/videos'
3import { makeGetRequest, makeUploadRequest } from '../requests/requests'
4import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
5
6function getYoutubeVideoUrl () {
7 return 'https://www.youtube.com/watch?v=msX3jv1XdvM'
8}
9
10function getYoutubeHDRVideoUrl () {
11 /**
12 * The video is used to check format-selection correctness wrt. HDR,
13 * which brings its own set of oddities outside of a MediaSource.
14 * FIXME: refactor once HDR is supported at playback
15 *
16 * The video needs to have the following format_ids:
17 * (which you can check by using `youtube-dl <url> -F`):
18 * - 303 (1080p webm vp9)
19 * - 299 (1080p mp4 avc1)
20 * - 335 (1080p webm vp9.2 HDR)
21 *
22 * 15 jan. 2021: TEST VIDEO NOT CURRENTLY PROVIDING
23 * - 400 (1080p mp4 av01)
24 * - 315 (2160p webm vp9 HDR)
25 * - 337 (2160p webm vp9.2 HDR)
26 * - 401 (2160p mp4 av01 HDR)
27 */
28 return 'https://www.youtube.com/watch?v=qR5vOXbZsI4'
29}
30
31function getMagnetURI () {
32 // eslint-disable-next-line max-len
33 return 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4'
34}
35
36function getBadVideoUrl () {
37 return 'https://download.cpy.re/peertube/bad_video.mp4'
38}
39
40function getGoodVideoUrl () {
41 return 'https://download.cpy.re/peertube/good_video.mp4'
42}
43
44function importVideo (
45 url: string,
46 token: string,
47 attributes: VideoImportCreate & { torrentfile?: string },
48 statusCodeExpected = HttpStatusCode.OK_200
49) {
50 const path = '/api/v1/videos/imports'
51
52 let attaches: any = {}
53 if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
54
55 return makeUploadRequest({
56 url,
57 path,
58 token,
59 attaches,
60 fields: attributes,
61 statusCodeExpected
62 })
63}
64
65function getMyVideoImports (url: string, token: string, sort?: string) {
66 const path = '/api/v1/users/me/videos/imports'
67
68 const query = {}
69 if (sort) query['sort'] = sort
70
71 return makeGetRequest({
72 url,
73 query,
74 path,
75 token,
76 statusCodeExpected: HttpStatusCode.OK_200
77 })
78}
79
80// ---------------------------------------------------------------------------
81
82export {
83 getBadVideoUrl,
84 getYoutubeVideoUrl,
85 getYoutubeHDRVideoUrl,
86 importVideo,
87 getMagnetURI,
88 getMyVideoImports,
89 getGoodVideoUrl
90}
diff --git a/shared/extra-utils/videos/video-playlists.ts b/shared/extra-utils/videos/video-playlists.ts
deleted file mode 100644
index c6f799e5d..000000000
--- a/shared/extra-utils/videos/video-playlists.ts
+++ /dev/null
@@ -1,320 +0,0 @@
1import { makeDeleteRequest, makeGetRequest, makePostBodyRequest, makePutBodyRequest, makeUploadRequest } from '../requests/requests'
2import { VideoPlaylistCreate } from '../../models/videos/playlist/video-playlist-create.model'
3import { omit } from 'lodash'
4import { VideoPlaylistUpdate } from '../../models/videos/playlist/video-playlist-update.model'
5import { VideoPlaylistElementCreate } from '../../models/videos/playlist/video-playlist-element-create.model'
6import { VideoPlaylistElementUpdate } from '../../models/videos/playlist/video-playlist-element-update.model'
7import { videoUUIDToId } from './videos'
8import { join } from 'path'
9import { root } from '..'
10import { readdir } from 'fs-extra'
11import { expect } from 'chai'
12import { VideoPlaylistType } from '../../models/videos/playlist/video-playlist-type.model'
13import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
14
15function getVideoPlaylistsList (url: string, start: number, count: number, sort?: string) {
16 const path = '/api/v1/video-playlists'
17
18 const query = {
19 start,
20 count,
21 sort
22 }
23
24 return makeGetRequest({
25 url,
26 path,
27 query,
28 statusCodeExpected: HttpStatusCode.OK_200
29 })
30}
31
32function getVideoChannelPlaylistsList (url: string, videoChannelName: string, start: number, count: number, sort?: string) {
33 const path = '/api/v1/video-channels/' + videoChannelName + '/video-playlists'
34
35 const query = {
36 start,
37 count,
38 sort
39 }
40
41 return makeGetRequest({
42 url,
43 path,
44 query,
45 statusCodeExpected: HttpStatusCode.OK_200
46 })
47}
48
49function getAccountPlaylistsList (url: string, accountName: string, start: number, count: number, sort?: string, search?: string) {
50 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
51
52 const query = {
53 start,
54 count,
55 sort,
56 search
57 }
58
59 return makeGetRequest({
60 url,
61 path,
62 query,
63 statusCodeExpected: HttpStatusCode.OK_200
64 })
65}
66
67function getAccountPlaylistsListWithToken (
68 url: string,
69 token: string,
70 accountName: string,
71 start: number,
72 count: number,
73 playlistType?: VideoPlaylistType,
74 sort?: string
75) {
76 const path = '/api/v1/accounts/' + accountName + '/video-playlists'
77
78 const query = {
79 start,
80 count,
81 playlistType,
82 sort
83 }
84
85 return makeGetRequest({
86 url,
87 token,
88 path,
89 query,
90 statusCodeExpected: HttpStatusCode.OK_200
91 })
92}
93
94function getVideoPlaylist (url: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
95 const path = '/api/v1/video-playlists/' + playlistId
96
97 return makeGetRequest({
98 url,
99 path,
100 statusCodeExpected
101 })
102}
103
104function getVideoPlaylistWithToken (url: string, token: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.OK_200) {
105 const path = '/api/v1/video-playlists/' + playlistId
106
107 return makeGetRequest({
108 url,
109 token,
110 path,
111 statusCodeExpected
112 })
113}
114
115function deleteVideoPlaylist (url: string, token: string, playlistId: number | string, statusCodeExpected = HttpStatusCode.NO_CONTENT_204) {
116 const path = '/api/v1/video-playlists/' + playlistId
117
118 return makeDeleteRequest({
119 url,
120 path,
121 token,
122 statusCodeExpected
123 })
124}
125
126function createVideoPlaylist (options: {
127 url: string
128 token: string
129 playlistAttrs: VideoPlaylistCreate
130 expectedStatus?: number
131}) {
132 const path = '/api/v1/video-playlists'
133
134 const fields = omit(options.playlistAttrs, 'thumbnailfile')
135
136 const attaches = options.playlistAttrs.thumbnailfile
137 ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
138 : {}
139
140 return makeUploadRequest({
141 method: 'POST',
142 url: options.url,
143 path,
144 token: options.token,
145 fields,
146 attaches,
147 statusCodeExpected: options.expectedStatus || HttpStatusCode.OK_200
148 })
149}
150
151function updateVideoPlaylist (options: {
152 url: string
153 token: string
154 playlistAttrs: VideoPlaylistUpdate
155 playlistId: number | string
156 expectedStatus?: number
157}) {
158 const path = '/api/v1/video-playlists/' + options.playlistId
159
160 const fields = omit(options.playlistAttrs, 'thumbnailfile')
161
162 const attaches = options.playlistAttrs.thumbnailfile
163 ? { thumbnailfile: options.playlistAttrs.thumbnailfile }
164 : {}
165
166 return makeUploadRequest({
167 method: 'PUT',
168 url: options.url,
169 path,
170 token: options.token,
171 fields,
172 attaches,
173 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
174 })
175}
176
177async function addVideoInPlaylist (options: {
178 url: string
179 token: string
180 playlistId: number | string
181 elementAttrs: VideoPlaylistElementCreate | { videoId: string }
182 expectedStatus?: number
183}) {
184 options.elementAttrs.videoId = await videoUUIDToId(options.url, options.elementAttrs.videoId)
185
186 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
187
188 return makePostBodyRequest({
189 url: options.url,
190 path,
191 token: options.token,
192 fields: options.elementAttrs,
193 statusCodeExpected: options.expectedStatus || HttpStatusCode.OK_200
194 })
195}
196
197function updateVideoPlaylistElement (options: {
198 url: string
199 token: string
200 playlistId: number | string
201 playlistElementId: number | string
202 elementAttrs: VideoPlaylistElementUpdate
203 expectedStatus?: number
204}) {
205 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
206
207 return makePutBodyRequest({
208 url: options.url,
209 path,
210 token: options.token,
211 fields: options.elementAttrs,
212 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
213 })
214}
215
216function removeVideoFromPlaylist (options: {
217 url: string
218 token: string
219 playlistId: number | string
220 playlistElementId: number
221 expectedStatus?: number
222}) {
223 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
224
225 return makeDeleteRequest({
226 url: options.url,
227 path,
228 token: options.token,
229 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
230 })
231}
232
233function reorderVideosPlaylist (options: {
234 url: string
235 token: string
236 playlistId: number | string
237 elementAttrs: {
238 startPosition: number
239 insertAfterPosition: number
240 reorderLength?: number
241 }
242 expectedStatus?: number
243}) {
244 const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
245
246 return makePostBodyRequest({
247 url: options.url,
248 path,
249 token: options.token,
250 fields: options.elementAttrs,
251 statusCodeExpected: options.expectedStatus || HttpStatusCode.NO_CONTENT_204
252 })
253}
254
255async function checkPlaylistFilesWereRemoved (
256 playlistUUID: string,
257 internalServerNumber: number,
258 directories = [ 'thumbnails' ]
259) {
260 const testDirectory = 'test' + internalServerNumber
261
262 for (const directory of directories) {
263 const directoryPath = join(root(), testDirectory, directory)
264
265 const files = await readdir(directoryPath)
266 for (const file of files) {
267 expect(file).to.not.contain(playlistUUID)
268 }
269 }
270}
271
272function getVideoPlaylistPrivacies (url: string) {
273 const path = '/api/v1/video-playlists/privacies'
274
275 return makeGetRequest({
276 url,
277 path,
278 statusCodeExpected: HttpStatusCode.OK_200
279 })
280}
281
282function doVideosExistInMyPlaylist (url: string, token: string, videoIds: number[]) {
283 const path = '/api/v1/users/me/video-playlists/videos-exist'
284
285 return makeGetRequest({
286 url,
287 token,
288 path,
289 query: { videoIds },
290 statusCodeExpected: HttpStatusCode.OK_200
291 })
292}
293
294// ---------------------------------------------------------------------------
295
296export {
297 getVideoPlaylistPrivacies,
298
299 getVideoPlaylistsList,
300 getVideoChannelPlaylistsList,
301 getAccountPlaylistsList,
302 getAccountPlaylistsListWithToken,
303
304 getVideoPlaylist,
305 getVideoPlaylistWithToken,
306
307 createVideoPlaylist,
308 updateVideoPlaylist,
309 deleteVideoPlaylist,
310
311 addVideoInPlaylist,
312 updateVideoPlaylistElement,
313 removeVideoFromPlaylist,
314
315 reorderVideosPlaylist,
316
317 checkPlaylistFilesWereRemoved,
318
319 doVideosExistInMyPlaylist
320}
diff --git a/shared/extra-utils/videos/video-streaming-playlists.ts b/shared/extra-utils/videos/video-streaming-playlists.ts
deleted file mode 100644
index 99c2e1880..000000000
--- a/shared/extra-utils/videos/video-streaming-playlists.ts
+++ /dev/null
@@ -1,82 +0,0 @@
1import { makeRawRequest } from '../requests/requests'
2import { sha256 } from '../../../server/helpers/core-utils'
3import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
4import { expect } from 'chai'
5import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
6
7function getPlaylist (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
8 return makeRawRequest(url, statusCodeExpected)
9}
10
11function getSegment (url: string, statusCodeExpected = HttpStatusCode.OK_200, range?: string) {
12 return makeRawRequest(url, statusCodeExpected, range)
13}
14
15function getSegmentSha256 (url: string, statusCodeExpected = HttpStatusCode.OK_200) {
16 return makeRawRequest(url, statusCodeExpected)
17}
18
19async function checkSegmentHash (
20 baseUrlPlaylist: string,
21 baseUrlSegment: string,
22 videoUUID: string,
23 resolution: number,
24 hlsPlaylist: VideoStreamingPlaylist
25) {
26 const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
27 const playlist = res.text
28
29 const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
30
31 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
32
33 const length = parseInt(matches[1], 10)
34 const offset = parseInt(matches[2], 10)
35 const range = `${offset}-${offset + length - 1}`
36
37 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, HttpStatusCode.PARTIAL_CONTENT_206, `bytes=${range}`)
38
39 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
40
41 const sha256Server = resSha.body[videoName][range]
42 expect(sha256(res2.body)).to.equal(sha256Server)
43}
44
45async function checkLiveSegmentHash (
46 baseUrlSegment: string,
47 videoUUID: string,
48 segmentName: string,
49 hlsPlaylist: VideoStreamingPlaylist
50) {
51 const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${segmentName}`)
52
53 const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
54
55 const sha256Server = resSha.body[segmentName]
56 expect(sha256(res2.body)).to.equal(sha256Server)
57}
58
59async function checkResolutionsInMasterPlaylist (playlistUrl: string, resolutions: number[]) {
60 const res = await getPlaylist(playlistUrl)
61
62 const masterPlaylist = res.text
63
64 for (const resolution of resolutions) {
65 const reg = new RegExp(
66 '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"'
67 )
68
69 expect(masterPlaylist).to.match(reg)
70 }
71}
72
73// ---------------------------------------------------------------------------
74
75export {
76 getPlaylist,
77 getSegment,
78 checkResolutionsInMasterPlaylist,
79 getSegmentSha256,
80 checkLiveSegmentHash,
81 checkSegmentHash
82}
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
new file mode 100644
index 000000000..33725bfdc
--- /dev/null
+++ b/shared/extra-utils/videos/videos-command.ts
@@ -0,0 +1,599 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { createReadStream, stat } from 'fs-extra'
5import got, { Response as GotResponse } from 'got'
6import { omit } from 'lodash'
7import validator from 'validator'
8import { buildUUID } from '@server/helpers/uuid'
9import { loadLanguages } from '@server/initializers/constants'
10import { pick } from '@shared/core-utils'
11import {
12 HttpStatusCode,
13 ResultList,
14 UserVideoRateType,
15 Video,
16 VideoCreate,
17 VideoCreateResult,
18 VideoDetails,
19 VideoFileMetadata,
20 VideoPrivacy,
21 VideosCommonQuery,
22 VideosWithSearchCommonQuery
23} from '@shared/models'
24import { buildAbsoluteFixturePath, wait } from '../miscs'
25import { unwrapBody } from '../requests'
26import { PeerTubeServer, waitJobs } from '../server'
27import { AbstractCommand, OverrideCommandOptions } from '../shared'
28
29export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
30 fixture?: string
31 thumbnailfile?: string
32 previewfile?: string
33}
34
35export class VideosCommand extends AbstractCommand {
36
37 constructor (server: PeerTubeServer) {
38 super(server)
39
40 loadLanguages()
41 }
42
43 getCategories (options: OverrideCommandOptions = {}) {
44 const path = '/api/v1/videos/categories'
45
46 return this.getRequestBody<{ [id: number]: string }>({
47 ...options,
48 path,
49
50 implicitToken: false,
51 defaultExpectedStatus: HttpStatusCode.OK_200
52 })
53 }
54
55 getLicences (options: OverrideCommandOptions = {}) {
56 const path = '/api/v1/videos/licences'
57
58 return this.getRequestBody<{ [id: number]: string }>({
59 ...options,
60 path,
61
62 implicitToken: false,
63 defaultExpectedStatus: HttpStatusCode.OK_200
64 })
65 }
66
67 getLanguages (options: OverrideCommandOptions = {}) {
68 const path = '/api/v1/videos/languages'
69
70 return this.getRequestBody<{ [id: string]: string }>({
71 ...options,
72 path,
73
74 implicitToken: false,
75 defaultExpectedStatus: HttpStatusCode.OK_200
76 })
77 }
78
79 getPrivacies (options: OverrideCommandOptions = {}) {
80 const path = '/api/v1/videos/privacies'
81
82 return this.getRequestBody<{ [id in VideoPrivacy]: string }>({
83 ...options,
84 path,
85
86 implicitToken: false,
87 defaultExpectedStatus: HttpStatusCode.OK_200
88 })
89 }
90
91 // ---------------------------------------------------------------------------
92
93 getDescription (options: OverrideCommandOptions & {
94 descriptionPath: string
95 }) {
96 return this.getRequestBody<{ description: string }>({
97 ...options,
98 path: options.descriptionPath,
99
100 implicitToken: false,
101 defaultExpectedStatus: HttpStatusCode.OK_200
102 })
103 }
104
105 getFileMetadata (options: OverrideCommandOptions & {
106 url: string
107 }) {
108 return unwrapBody<VideoFileMetadata>(this.getRawRequest({
109 ...options,
110
111 url: options.url,
112 implicitToken: false,
113 defaultExpectedStatus: HttpStatusCode.OK_200
114 }))
115 }
116
117 // ---------------------------------------------------------------------------
118
119 view (options: OverrideCommandOptions & {
120 id: number | string
121 xForwardedFor?: string
122 }) {
123 const { id, xForwardedFor } = options
124 const path = '/api/v1/videos/' + id + '/views'
125
126 return this.postBodyRequest({
127 ...options,
128
129 path,
130 xForwardedFor,
131 implicitToken: false,
132 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
133 })
134 }
135
136 rate (options: OverrideCommandOptions & {
137 id: number | string
138 rating: UserVideoRateType
139 }) {
140 const { id, rating } = options
141 const path = '/api/v1/videos/' + id + '/rate'
142
143 return this.putBodyRequest({
144 ...options,
145
146 path,
147 fields: { rating },
148 implicitToken: true,
149 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
150 })
151 }
152
153 // ---------------------------------------------------------------------------
154
155 get (options: OverrideCommandOptions & {
156 id: number | string
157 }) {
158 const path = '/api/v1/videos/' + options.id
159
160 return this.getRequestBody<VideoDetails>({
161 ...options,
162
163 path,
164 implicitToken: false,
165 defaultExpectedStatus: HttpStatusCode.OK_200
166 })
167 }
168
169 getWithToken (options: OverrideCommandOptions & {
170 id: number | string
171 }) {
172 return this.get({
173 ...options,
174
175 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
176 })
177 }
178
179 async getId (options: OverrideCommandOptions & {
180 uuid: number | string
181 }) {
182 const { uuid } = options
183
184 if (validator.isUUID('' + uuid) === false) return uuid as number
185
186 const { id } = await this.get({ ...options, id: uuid })
187
188 return id
189 }
190
191 // ---------------------------------------------------------------------------
192
193 listMyVideos (options: OverrideCommandOptions & {
194 start?: number
195 count?: number
196 sort?: string
197 search?: string
198 isLive?: boolean
199 } = {}) {
200 const path = '/api/v1/users/me/videos'
201
202 return this.getRequestBody<ResultList<Video>>({
203 ...options,
204
205 path,
206 query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive' ]),
207 implicitToken: true,
208 defaultExpectedStatus: HttpStatusCode.OK_200
209 })
210 }
211
212 // ---------------------------------------------------------------------------
213
214 list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
215 const path = '/api/v1/videos'
216
217 const query = this.buildListQuery(options)
218
219 return this.getRequestBody<ResultList<Video>>({
220 ...options,
221
222 path,
223 query: { sort: 'name', ...query },
224 implicitToken: false,
225 defaultExpectedStatus: HttpStatusCode.OK_200
226 })
227 }
228
229 listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
230 return this.list({
231 ...options,
232
233 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
234 })
235 }
236
237 listByAccount (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
238 handle: string
239 }) {
240 const { handle, search } = options
241 const path = '/api/v1/accounts/' + handle + '/videos'
242
243 return this.getRequestBody<ResultList<Video>>({
244 ...options,
245
246 path,
247 query: { search, ...this.buildListQuery(options) },
248 implicitToken: true,
249 defaultExpectedStatus: HttpStatusCode.OK_200
250 })
251 }
252
253 listByChannel (options: OverrideCommandOptions & VideosWithSearchCommonQuery & {
254 handle: string
255 }) {
256 const { handle } = options
257 const path = '/api/v1/video-channels/' + handle + '/videos'
258
259 return this.getRequestBody<ResultList<Video>>({
260 ...options,
261
262 path,
263 query: this.buildListQuery(options),
264 implicitToken: true,
265 defaultExpectedStatus: HttpStatusCode.OK_200
266 })
267 }
268
269 // ---------------------------------------------------------------------------
270
271 async find (options: OverrideCommandOptions & {
272 name: string
273 }) {
274 const { data } = await this.list(options)
275
276 return data.find(v => v.name === options.name)
277 }
278
279 // ---------------------------------------------------------------------------
280
281 update (options: OverrideCommandOptions & {
282 id: number | string
283 attributes?: VideoEdit
284 }) {
285 const { id, attributes = {} } = options
286 const path = '/api/v1/videos/' + id
287
288 // Upload request
289 if (attributes.thumbnailfile || attributes.previewfile) {
290 const attaches: any = {}
291 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
292 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
293
294 return this.putUploadRequest({
295 ...options,
296
297 path,
298 fields: options.attributes,
299 attaches: {
300 thumbnailfile: attributes.thumbnailfile,
301 previewfile: attributes.previewfile
302 },
303 implicitToken: true,
304 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
305 })
306 }
307
308 return this.putBodyRequest({
309 ...options,
310
311 path,
312 fields: options.attributes,
313 implicitToken: true,
314 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
315 })
316 }
317
318 remove (options: OverrideCommandOptions & {
319 id: number | string
320 }) {
321 const path = '/api/v1/videos/' + options.id
322
323 return unwrapBody(this.deleteRequest({
324 ...options,
325
326 path,
327 implicitToken: true,
328 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
329 }))
330 }
331
332 async removeAll () {
333 const { data } = await this.list()
334
335 for (const v of data) {
336 await this.remove({ id: v.id })
337 }
338 }
339
340 // ---------------------------------------------------------------------------
341
342 async upload (options: OverrideCommandOptions & {
343 attributes?: VideoEdit
344 mode?: 'legacy' | 'resumable' // default legacy
345 } = {}) {
346 const { mode = 'legacy' } = options
347 let defaultChannelId = 1
348
349 try {
350 const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
351 defaultChannelId = videoChannels[0].id
352 } catch (e) { /* empty */ }
353
354 // Override default attributes
355 const attributes = {
356 name: 'my super video',
357 category: 5,
358 licence: 4,
359 language: 'zh',
360 channelId: defaultChannelId,
361 nsfw: true,
362 waitTranscoding: false,
363 description: 'my super description',
364 support: 'my super support text',
365 tags: [ 'tag' ],
366 privacy: VideoPrivacy.PUBLIC,
367 commentsEnabled: true,
368 downloadEnabled: true,
369 fixture: 'video_short.webm',
370
371 ...options.attributes
372 }
373
374 const created = mode === 'legacy'
375 ? await this.buildLegacyUpload({ ...options, attributes })
376 : await this.buildResumeUpload({ ...options, attributes })
377
378 // Wait torrent generation
379 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
380 if (expectedStatus === HttpStatusCode.OK_200) {
381 let video: VideoDetails
382
383 do {
384 video = await this.getWithToken({ ...options, id: created.uuid })
385
386 await wait(50)
387 } while (!video.files[0].torrentUrl)
388 }
389
390 return created
391 }
392
393 async buildLegacyUpload (options: OverrideCommandOptions & {
394 attributes: VideoEdit
395 }): Promise<VideoCreateResult> {
396 const path = '/api/v1/videos/upload'
397
398 return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
399 ...options,
400
401 path,
402 fields: this.buildUploadFields(options.attributes),
403 attaches: this.buildUploadAttaches(options.attributes),
404 implicitToken: true,
405 defaultExpectedStatus: HttpStatusCode.OK_200
406 })).then(body => body.video || body as any)
407 }
408
409 async buildResumeUpload (options: OverrideCommandOptions & {
410 attributes: VideoEdit
411 }): Promise<VideoCreateResult> {
412 const { attributes, expectedStatus } = options
413
414 let size = 0
415 let videoFilePath: string
416 let mimetype = 'video/mp4'
417
418 if (attributes.fixture) {
419 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
420 size = (await stat(videoFilePath)).size
421
422 if (videoFilePath.endsWith('.mkv')) {
423 mimetype = 'video/x-matroska'
424 } else if (videoFilePath.endsWith('.webm')) {
425 mimetype = 'video/webm'
426 }
427 }
428
429 // Do not check status automatically, we'll check it manually
430 const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
431 const initStatus = initializeSessionRes.status
432
433 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
434 const locationHeader = initializeSessionRes.header['location']
435 expect(locationHeader).to.not.be.undefined
436
437 const pathUploadId = locationHeader.split('?')[1]
438
439 const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
440
441 return result.body?.video || result.body as any
442 }
443
444 const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
445 ? HttpStatusCode.CREATED_201
446 : expectedStatus
447
448 expect(initStatus).to.equal(expectedInitStatus)
449
450 return initializeSessionRes.body.video || initializeSessionRes.body
451 }
452
453 async prepareResumableUpload (options: OverrideCommandOptions & {
454 attributes: VideoEdit
455 size: number
456 mimetype: string
457 }) {
458 const { attributes, size, mimetype } = options
459
460 const path = '/api/v1/videos/upload-resumable'
461
462 return this.postUploadRequest({
463 ...options,
464
465 path,
466 headers: {
467 'X-Upload-Content-Type': mimetype,
468 'X-Upload-Content-Length': size.toString()
469 },
470 fields: { filename: attributes.fixture, ...this.buildUploadFields(options.attributes) },
471 // Fixture will be sent later
472 attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
473 implicitToken: true,
474
475 defaultExpectedStatus: null
476 })
477 }
478
479 sendResumableChunks (options: OverrideCommandOptions & {
480 pathUploadId: string
481 videoFilePath: string
482 size: number
483 contentLength?: number
484 contentRangeBuilder?: (start: number, chunk: any) => string
485 }) {
486 const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
487
488 const path = '/api/v1/videos/upload-resumable'
489 let start = 0
490
491 const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
492 const url = this.server.url
493
494 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
495 return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
496 readable.on('data', async function onData (chunk) {
497 readable.pause()
498
499 const headers = {
500 'Authorization': 'Bearer ' + token,
501 'Content-Type': 'application/octet-stream',
502 'Content-Range': contentRangeBuilder
503 ? contentRangeBuilder(start, chunk)
504 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
505 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
506 }
507
508 const res = await got<{ video: VideoCreateResult }>({
509 url,
510 method: 'put',
511 headers,
512 path: path + '?' + pathUploadId,
513 body: chunk,
514 responseType: 'json',
515 throwHttpErrors: false
516 })
517
518 start += chunk.length
519
520 if (res.statusCode === expectedStatus) {
521 return resolve(res)
522 }
523
524 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
525 readable.off('data', onData)
526 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
527 }
528
529 readable.resume()
530 })
531 })
532 }
533
534 quickUpload (options: OverrideCommandOptions & {
535 name: string
536 nsfw?: boolean
537 privacy?: VideoPrivacy
538 fixture?: string
539 }) {
540 const attributes: VideoEdit = { name: options.name }
541 if (options.nsfw) attributes.nsfw = options.nsfw
542 if (options.privacy) attributes.privacy = options.privacy
543 if (options.fixture) attributes.fixture = options.fixture
544
545 return this.upload({ ...options, attributes })
546 }
547
548 async randomUpload (options: OverrideCommandOptions & {
549 wait?: boolean // default true
550 additionalParams?: VideoEdit & { prefixName?: string }
551 } = {}) {
552 const { wait = true, additionalParams } = options
553 const prefixName = additionalParams?.prefixName || ''
554 const name = prefixName + buildUUID()
555
556 const attributes = { name, ...additionalParams }
557
558 const result = await this.upload({ ...options, attributes })
559
560 if (wait) await waitJobs([ this.server ])
561
562 return { ...result, name }
563 }
564
565 // ---------------------------------------------------------------------------
566
567 private buildListQuery (options: VideosCommonQuery) {
568 return pick(options, [
569 'start',
570 'count',
571 'sort',
572 'nsfw',
573 'isLive',
574 'categoryOneOf',
575 'licenceOneOf',
576 'languageOneOf',
577 'tagsOneOf',
578 'tagsAllOf',
579 'filter',
580 'skipCount'
581 ])
582 }
583
584 private buildUploadFields (attributes: VideoEdit) {
585 return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
586 }
587
588 private buildUploadAttaches (attributes: VideoEdit) {
589 const attaches: { [ name: string ]: string } = {}
590
591 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
592 if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
593 }
594
595 if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
596
597 return attaches
598 }
599}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
index 469ea4d63..a1d2ba0fc 100644
--- a/shared/extra-utils/videos/videos.ts
+++ b/shared/extra-utils/videos/videos.ts
@@ -1,646 +1,92 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2 2
3import { expect } from 'chai' 3import { expect } from 'chai'
4import { createReadStream, pathExists, readdir, readFile, stat } from 'fs-extra' 4import { pathExists, readdir } from 'fs-extra'
5import got, { Response as GotResponse } from 'got/dist/source' 5import { basename, join } from 'path'
6import * as parseTorrent from 'parse-torrent'
7import { join } from 'path'
8import * as request from 'supertest'
9import validator from 'validator'
10import { getLowercaseExtension } from '@server/helpers/core-utils' 6import { getLowercaseExtension } from '@server/helpers/core-utils'
11import { buildUUID } from '@server/helpers/uuid' 7import { uuidRegex } from '@shared/core-utils'
12import { HttpStatusCode } from '@shared/core-utils' 8import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models'
13import { VideosCommonQuery } from '@shared/models' 9import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
14import { loadLanguages, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants' 10import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
15import { VideoDetails, VideoPrivacy } from '../../models/videos' 11import { makeRawRequest } from '../requests/requests'
16import { 12import { waitJobs } from '../server'
17 buildAbsoluteFixturePath, 13import { PeerTubeServer } from '../server/server'
18 buildServerDirectory, 14import { VideoEdit } from './videos-command'
19 dateIsValid, 15
20 immutableAssign, 16async function checkVideoFilesWereRemoved (options: {
21 testImage, 17 server: PeerTubeServer
22 wait, 18 video: VideoDetails
23 webtorrentAdd 19 captions?: VideoCaption[]
24} from '../miscs/miscs' 20 onlyVideoFiles?: boolean // default false
25import { makeGetRequest, makePutBodyRequest, makeRawRequest, makeUploadRequest } from '../requests/requests' 21}) {
26import { waitJobs } from '../server/jobs' 22 const { video, server, captions = [], onlyVideoFiles = false } = options
27import { ServerInfo } from '../server/servers'
28import { getMyUserInformation } from '../users/users'
29
30loadLanguages()
31
32type VideoAttributes = {
33 name?: string
34 category?: number
35 licence?: number
36 language?: string
37 nsfw?: boolean
38 commentsEnabled?: boolean
39 downloadEnabled?: boolean
40 waitTranscoding?: boolean
41 description?: string
42 originallyPublishedAt?: string
43 tags?: string[]
44 channelId?: number
45 privacy?: VideoPrivacy
46 fixture?: string
47 support?: string
48 thumbnailfile?: string
49 previewfile?: string
50 scheduleUpdate?: {
51 updateAt: string
52 privacy?: VideoPrivacy
53 }
54}
55
56function getVideoCategories (url: string) {
57 const path = '/api/v1/videos/categories'
58
59 return makeGetRequest({
60 url,
61 path,
62 statusCodeExpected: HttpStatusCode.OK_200
63 })
64}
65
66function getVideoLicences (url: string) {
67 const path = '/api/v1/videos/licences'
68
69 return makeGetRequest({
70 url,
71 path,
72 statusCodeExpected: HttpStatusCode.OK_200
73 })
74}
75
76function getVideoLanguages (url: string) {
77 const path = '/api/v1/videos/languages'
78
79 return makeGetRequest({
80 url,
81 path,
82 statusCodeExpected: HttpStatusCode.OK_200
83 })
84}
85
86function getVideoPrivacies (url: string) {
87 const path = '/api/v1/videos/privacies'
88
89 return makeGetRequest({
90 url,
91 path,
92 statusCodeExpected: HttpStatusCode.OK_200
93 })
94}
95
96function getVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
97 const path = '/api/v1/videos/' + id
98
99 return request(url)
100 .get(path)
101 .set('Accept', 'application/json')
102 .expect(expectedStatus)
103}
104 23
105async function getVideoIdFromUUID (url: string, uuid: string) { 24 const webtorrentFiles = video.files || []
106 const res = await getVideo(url, uuid) 25 const hlsFiles = video.streamingPlaylists[0]?.files || []
107 26
108 return res.body.id 27 const thumbnailName = basename(video.thumbnailPath)
109} 28 const previewName = basename(video.previewPath)
110 29
111function getVideoFileMetadataUrl (url: string) { 30 const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl))
112 return request(url)
113 .get('/')
114 .set('Accept', 'application/json')
115 .expect(HttpStatusCode.OK_200)
116 .expect('Content-Type', /json/)
117}
118 31
119function viewVideo (url: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204, xForwardedFor?: string) { 32 const captionNames = captions.map(c => basename(c.captionPath))
120 const path = '/api/v1/videos/' + id + '/views'
121 33
122 const req = request(url) 34 const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl))
123 .post(path) 35 const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl))
124 .set('Accept', 'application/json')
125 36
126 if (xForwardedFor) { 37 let directories: { [ directory: string ]: string[] } = {
127 req.set('X-Forwarded-For', xForwardedFor) 38 videos: webtorrentFilenames,
39 redundancy: webtorrentFilenames,
40 [join('playlists', 'hls')]: hlsFilenames,
41 [join('redundancy', 'hls')]: hlsFilenames
128 } 42 }
129 43
130 return req.expect(expectedStatus) 44 if (onlyVideoFiles !== true) {
131} 45 directories = {
132 46 ...directories,
133function getVideoWithToken (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.OK_200) {
134 const path = '/api/v1/videos/' + id
135
136 return request(url)
137 .get(path)
138 .set('Authorization', 'Bearer ' + token)
139 .set('Accept', 'application/json')
140 .expect(expectedStatus)
141}
142
143function getVideoDescription (url: string, descriptionPath: string) {
144 return request(url)
145 .get(descriptionPath)
146 .set('Accept', 'application/json')
147 .expect(HttpStatusCode.OK_200)
148 .expect('Content-Type', /json/)
149}
150
151function getVideosList (url: string) {
152 const path = '/api/v1/videos'
153
154 return request(url)
155 .get(path)
156 .query({ sort: 'name' })
157 .set('Accept', 'application/json')
158 .expect(HttpStatusCode.OK_200)
159 .expect('Content-Type', /json/)
160}
161
162function getVideosListWithToken (url: string, token: string, query: { nsfw?: boolean } = {}) {
163 const path = '/api/v1/videos'
164
165 return request(url)
166 .get(path)
167 .set('Authorization', 'Bearer ' + token)
168 .query(immutableAssign(query, { sort: 'name' }))
169 .set('Accept', 'application/json')
170 .expect(HttpStatusCode.OK_200)
171 .expect('Content-Type', /json/)
172}
173
174function getLocalVideos (url: string) {
175 const path = '/api/v1/videos'
176
177 return request(url)
178 .get(path)
179 .query({ sort: 'name', filter: 'local' })
180 .set('Accept', 'application/json')
181 .expect(HttpStatusCode.OK_200)
182 .expect('Content-Type', /json/)
183}
184
185function getMyVideos (url: string, accessToken: string, start: number, count: number, sort?: string, search?: string) {
186 const path = '/api/v1/users/me/videos'
187
188 const req = request(url)
189 .get(path)
190 .query({ start: start })
191 .query({ count: count })
192 .query({ search: search })
193
194 if (sort) req.query({ sort })
195
196 return req.set('Accept', 'application/json')
197 .set('Authorization', 'Bearer ' + accessToken)
198 .expect(HttpStatusCode.OK_200)
199 .expect('Content-Type', /json/)
200}
201
202function getMyVideosWithFilter (url: string, accessToken: string, query: { isLive?: boolean }) {
203 const path = '/api/v1/users/me/videos'
204
205 return makeGetRequest({
206 url,
207 path,
208 token: accessToken,
209 query,
210 statusCodeExpected: HttpStatusCode.OK_200
211 })
212}
213
214function getAccountVideos (
215 url: string,
216 accessToken: string,
217 accountName: string,
218 start: number,
219 count: number,
220 sort?: string,
221 query: {
222 nsfw?: boolean
223 search?: string
224 } = {}
225) {
226 const path = '/api/v1/accounts/' + accountName + '/videos'
227
228 return makeGetRequest({
229 url,
230 path,
231 query: immutableAssign(query, {
232 start,
233 count,
234 sort
235 }),
236 token: accessToken,
237 statusCodeExpected: HttpStatusCode.OK_200
238 })
239}
240
241function getVideoChannelVideos (
242 url: string,
243 accessToken: string,
244 videoChannelName: string,
245 start: number,
246 count: number,
247 sort?: string,
248 query: { nsfw?: boolean } = {}
249) {
250 const path = '/api/v1/video-channels/' + videoChannelName + '/videos'
251
252 return makeGetRequest({
253 url,
254 path,
255 query: immutableAssign(query, {
256 start,
257 count,
258 sort
259 }),
260 token: accessToken,
261 statusCodeExpected: HttpStatusCode.OK_200
262 })
263}
264
265function getPlaylistVideos (
266 url: string,
267 accessToken: string,
268 playlistId: number | string,
269 start: number,
270 count: number,
271 query: { nsfw?: boolean } = {}
272) {
273 const path = '/api/v1/video-playlists/' + playlistId + '/videos'
274
275 return makeGetRequest({
276 url,
277 path,
278 query: immutableAssign(query, {
279 start,
280 count
281 }),
282 token: accessToken,
283 statusCodeExpected: HttpStatusCode.OK_200
284 })
285}
286
287function getVideosListPagination (url: string, start: number, count: number, sort?: string, skipCount?: boolean) {
288 const path = '/api/v1/videos'
289
290 const req = request(url)
291 .get(path)
292 .query({ start: start })
293 .query({ count: count })
294
295 if (sort) req.query({ sort })
296 if (skipCount) req.query({ skipCount })
297
298 return req.set('Accept', 'application/json')
299 .expect(HttpStatusCode.OK_200)
300 .expect('Content-Type', /json/)
301}
302
303function getVideosListSort (url: string, sort: string) {
304 const path = '/api/v1/videos'
305
306 return request(url)
307 .get(path)
308 .query({ sort: sort })
309 .set('Accept', 'application/json')
310 .expect(HttpStatusCode.OK_200)
311 .expect('Content-Type', /json/)
312}
313
314function getVideosWithFilters (url: string, query: VideosCommonQuery) {
315 const path = '/api/v1/videos'
316
317 return request(url)
318 .get(path)
319 .query(query)
320 .set('Accept', 'application/json')
321 .expect(HttpStatusCode.OK_200)
322 .expect('Content-Type', /json/)
323}
324
325function removeVideo (url: string, token: string, id: number | string, expectedStatus = HttpStatusCode.NO_CONTENT_204) {
326 const path = '/api/v1/videos'
327
328 return request(url)
329 .delete(path + '/' + id)
330 .set('Accept', 'application/json')
331 .set('Authorization', 'Bearer ' + token)
332 .expect(expectedStatus)
333}
334 47
335async function removeAllVideos (server: ServerInfo) { 48 thumbnails: [ thumbnailName ],
336 const resVideos = await getVideosList(server.url) 49 previews: [ previewName ],
337 50 torrents: torrentNames,
338 for (const v of resVideos.body.data) { 51 captions: captionNames
339 await removeVideo(server.url, server.accessToken, v.id) 52 }
340 } 53 }
341}
342 54
343async function checkVideoFilesWereRemoved ( 55 for (const directory of Object.keys(directories)) {
344 videoUUID: string, 56 const directoryPath = server.servers.buildDirectory(directory)
345 serverNumber: number,
346 directories = [
347 'redundancy',
348 'videos',
349 'thumbnails',
350 'torrents',
351 'previews',
352 'captions',
353 join('playlists', 'hls'),
354 join('redundancy', 'hls')
355 ]
356) {
357 for (const directory of directories) {
358 const directoryPath = buildServerDirectory({ internalServerNumber: serverNumber }, directory)
359 57
360 const directoryExists = await pathExists(directoryPath) 58 const directoryExists = await pathExists(directoryPath)
361 if (directoryExists === false) continue 59 if (directoryExists === false) continue
362 60
363 const files = await readdir(directoryPath) 61 const existingFiles = await readdir(directoryPath)
364 for (const file of files) { 62 for (const existingFile of existingFiles) {
365 expect(file, `File ${file} should not exist in ${directoryPath}`).to.not.contain(videoUUID) 63 for (const shouldNotExist of directories[directory]) {
64 expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist)
65 }
366 } 66 }
367 } 67 }
368} 68}
369 69
370async function uploadVideo ( 70async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) {
371 url: string, 71 for (const server of servers) {
372 accessToken: string, 72 server.store.videoDetails = await server.videos.get({ id: uuid })
373 videoAttributesArg: VideoAttributes,
374 specialStatus = HttpStatusCode.OK_200,
375 mode: 'legacy' | 'resumable' = 'legacy'
376) {
377 let defaultChannelId = '1'
378
379 try {
380 const res = await getMyUserInformation(url, accessToken)
381 defaultChannelId = res.body.videoChannels[0].id
382 } catch (e) { /* empty */ }
383
384 // Override default attributes
385 const attributes = Object.assign({
386 name: 'my super video',
387 category: 5,
388 licence: 4,
389 language: 'zh',
390 channelId: defaultChannelId,
391 nsfw: true,
392 waitTranscoding: false,
393 description: 'my super description',
394 support: 'my super support text',
395 tags: [ 'tag' ],
396 privacy: VideoPrivacy.PUBLIC,
397 commentsEnabled: true,
398 downloadEnabled: true,
399 fixture: 'video_short.webm'
400 }, videoAttributesArg)
401
402 const res = mode === 'legacy'
403 ? await buildLegacyUpload(url, accessToken, attributes, specialStatus)
404 : await buildResumeUpload(url, accessToken, attributes, specialStatus)
405
406 // Wait torrent generation
407 if (specialStatus === HttpStatusCode.OK_200) {
408 let video: VideoDetails
409 do {
410 const resVideo = await getVideoWithToken(url, accessToken, res.body.video.uuid)
411 video = resVideo.body
412
413 await wait(50)
414 } while (!video.files[0].torrentUrl)
415 } 73 }
416
417 return res
418} 74}
419 75
420function checkUploadVideoParam ( 76function checkUploadVideoParam (
421 url: string, 77 server: PeerTubeServer,
422 token: string, 78 token: string,
423 attributes: Partial<VideoAttributes>, 79 attributes: Partial<VideoEdit>,
424 specialStatus = HttpStatusCode.OK_200, 80 expectedStatus = HttpStatusCode.OK_200,
425 mode: 'legacy' | 'resumable' = 'legacy' 81 mode: 'legacy' | 'resumable' = 'legacy'
426) { 82) {
427 return mode === 'legacy' 83 return mode === 'legacy'
428 ? buildLegacyUpload(url, token, attributes, specialStatus) 84 ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus })
429 : buildResumeUpload(url, token, attributes, specialStatus) 85 : server.videos.buildResumeUpload({ token, attributes, expectedStatus })
430}
431
432async function buildLegacyUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
433 const path = '/api/v1/videos/upload'
434 const req = request(url)
435 .post(path)
436 .set('Accept', 'application/json')
437 .set('Authorization', 'Bearer ' + token)
438
439 buildUploadReq(req, attributes)
440
441 if (attributes.fixture !== undefined) {
442 req.attach('videofile', buildAbsoluteFixturePath(attributes.fixture))
443 }
444
445 return req.expect(specialStatus)
446}
447
448async function buildResumeUpload (url: string, token: string, attributes: VideoAttributes, specialStatus = HttpStatusCode.OK_200) {
449 let size = 0
450 let videoFilePath: string
451 let mimetype = 'video/mp4'
452
453 if (attributes.fixture) {
454 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
455 size = (await stat(videoFilePath)).size
456
457 if (videoFilePath.endsWith('.mkv')) {
458 mimetype = 'video/x-matroska'
459 } else if (videoFilePath.endsWith('.webm')) {
460 mimetype = 'video/webm'
461 }
462 }
463
464 const initializeSessionRes = await prepareResumableUpload({ url, token, attributes, size, mimetype })
465 const initStatus = initializeSessionRes.status
466
467 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
468 const locationHeader = initializeSessionRes.header['location']
469 expect(locationHeader).to.not.be.undefined
470
471 const pathUploadId = locationHeader.split('?')[1]
472
473 return sendResumableChunks({ url, token, pathUploadId, videoFilePath, size, specialStatus })
474 }
475
476 const expectedInitStatus = specialStatus === HttpStatusCode.OK_200
477 ? HttpStatusCode.CREATED_201
478 : specialStatus
479
480 expect(initStatus).to.equal(expectedInitStatus)
481
482 return initializeSessionRes
483}
484
485async function prepareResumableUpload (options: {
486 url: string
487 token: string
488 attributes: VideoAttributes
489 size: number
490 mimetype: string
491}) {
492 const { url, token, attributes, size, mimetype } = options
493
494 const path = '/api/v1/videos/upload-resumable'
495
496 const req = request(url)
497 .post(path)
498 .set('Authorization', 'Bearer ' + token)
499 .set('X-Upload-Content-Type', mimetype)
500 .set('X-Upload-Content-Length', size.toString())
501
502 buildUploadReq(req, attributes)
503
504 if (attributes.fixture) {
505 req.field('filename', attributes.fixture)
506 }
507
508 return req
509}
510
511function sendResumableChunks (options: {
512 url: string
513 token: string
514 pathUploadId: string
515 videoFilePath: string
516 size: number
517 specialStatus?: HttpStatusCode
518 contentLength?: number
519 contentRangeBuilder?: (start: number, chunk: any) => string
520}) {
521 const { url, token, pathUploadId, videoFilePath, size, specialStatus, contentLength, contentRangeBuilder } = options
522
523 const expectedStatus = specialStatus || HttpStatusCode.OK_200
524
525 const path = '/api/v1/videos/upload-resumable'
526 let start = 0
527
528 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
529 return new Promise<GotResponse>((resolve, reject) => {
530 readable.on('data', async function onData (chunk) {
531 readable.pause()
532
533 const headers = {
534 'Authorization': 'Bearer ' + token,
535 'Content-Type': 'application/octet-stream',
536 'Content-Range': contentRangeBuilder
537 ? contentRangeBuilder(start, chunk)
538 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
539 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
540 }
541
542 const res = await got({
543 url,
544 method: 'put',
545 headers,
546 path: path + '?' + pathUploadId,
547 body: chunk,
548 responseType: 'json',
549 throwHttpErrors: false
550 })
551
552 start += chunk.length
553
554 if (res.statusCode === expectedStatus) {
555 return resolve(res)
556 }
557
558 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
559 readable.off('data', onData)
560 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
561 }
562
563 readable.resume()
564 })
565 })
566}
567
568function updateVideo (
569 url: string,
570 accessToken: string,
571 id: number | string,
572 attributes: VideoAttributes,
573 statusCodeExpected = HttpStatusCode.NO_CONTENT_204
574) {
575 const path = '/api/v1/videos/' + id
576 const body = {}
577
578 if (attributes.name) body['name'] = attributes.name
579 if (attributes.category) body['category'] = attributes.category
580 if (attributes.licence) body['licence'] = attributes.licence
581 if (attributes.language) body['language'] = attributes.language
582 if (attributes.nsfw !== undefined) body['nsfw'] = JSON.stringify(attributes.nsfw)
583 if (attributes.commentsEnabled !== undefined) body['commentsEnabled'] = JSON.stringify(attributes.commentsEnabled)
584 if (attributes.downloadEnabled !== undefined) body['downloadEnabled'] = JSON.stringify(attributes.downloadEnabled)
585 if (attributes.originallyPublishedAt !== undefined) body['originallyPublishedAt'] = attributes.originallyPublishedAt
586 if (attributes.description) body['description'] = attributes.description
587 if (attributes.tags) body['tags'] = attributes.tags
588 if (attributes.privacy) body['privacy'] = attributes.privacy
589 if (attributes.channelId) body['channelId'] = attributes.channelId
590 if (attributes.scheduleUpdate) body['scheduleUpdate'] = attributes.scheduleUpdate
591
592 // Upload request
593 if (attributes.thumbnailfile || attributes.previewfile) {
594 const attaches: any = {}
595 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
596 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
597
598 return makeUploadRequest({
599 url,
600 method: 'PUT',
601 path,
602 token: accessToken,
603 fields: body,
604 attaches,
605 statusCodeExpected
606 })
607 }
608
609 return makePutBodyRequest({
610 url,
611 path,
612 fields: body,
613 token: accessToken,
614 statusCodeExpected
615 })
616}
617
618function rateVideo (url: string, accessToken: string, id: number | string, rating: string, specialStatus = HttpStatusCode.NO_CONTENT_204) {
619 const path = '/api/v1/videos/' + id + '/rate'
620
621 return request(url)
622 .put(path)
623 .set('Accept', 'application/json')
624 .set('Authorization', 'Bearer ' + accessToken)
625 .send({ rating })
626 .expect(specialStatus)
627}
628
629function parseTorrentVideo (server: ServerInfo, videoUUID: string, resolution: number) {
630 return new Promise<any>((res, rej) => {
631 const torrentName = videoUUID + '-' + resolution + '.torrent'
632 const torrentPath = buildServerDirectory(server, join('torrents', torrentName))
633
634 readFile(torrentPath, (err, data) => {
635 if (err) return rej(err)
636
637 return res(parseTorrent(data))
638 })
639 })
640} 86}
641 87
642async function completeVideoCheck ( 88async function completeVideoCheck (
643 url: string, 89 server: PeerTubeServer,
644 video: any, 90 video: any,
645 attributes: { 91 attributes: {
646 name: string 92 name: string
@@ -682,7 +128,7 @@ async function completeVideoCheck (
682 if (!attributes.likes) attributes.likes = 0 128 if (!attributes.likes) attributes.likes = 0
683 if (!attributes.dislikes) attributes.dislikes = 0 129 if (!attributes.dislikes) attributes.dislikes = 0
684 130
685 const host = new URL(url).host 131 const host = new URL(server.url).host
686 const originHost = attributes.account.host 132 const originHost = attributes.account.host
687 133
688 expect(video.name).to.equal(attributes.name) 134 expect(video.name).to.equal(attributes.name)
@@ -719,8 +165,7 @@ async function completeVideoCheck (
719 expect(video.originallyPublishedAt).to.be.null 165 expect(video.originallyPublishedAt).to.be.null
720 } 166 }
721 167
722 const res = await getVideo(url, video.uuid) 168 const videoDetails = await server.videos.get({ id: video.uuid })
723 const videoDetails: VideoDetails = res.body
724 169
725 expect(videoDetails.files).to.have.lengthOf(attributes.files.length) 170 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
726 expect(videoDetails.tags).to.deep.equal(attributes.tags) 171 expect(videoDetails.tags).to.deep.equal(attributes.tags)
@@ -745,18 +190,16 @@ async function completeVideoCheck (
745 190
746 expect(file.magnetUri).to.have.lengthOf.above(2) 191 expect(file.magnetUri).to.have.lengthOf.above(2)
747 192
748 expect(file.torrentDownloadUrl).to.equal(`http://${host}/download/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`) 193 expect(file.torrentDownloadUrl).to.match(new RegExp(`http://${host}/download/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
749 expect(file.torrentUrl).to.equal(`http://${host}/lazy-static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`) 194 expect(file.torrentUrl).to.match(new RegExp(`http://${host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
750 195
751 expect(file.fileUrl).to.equal(`http://${originHost}/static/webseed/${videoDetails.uuid}-${file.resolution.id}${extension}`) 196 expect(file.fileUrl).to.match(new RegExp(`http://${originHost}/static/webseed/${uuidRegex}-${file.resolution.id}${extension}`))
752 expect(file.fileDownloadUrl).to.equal(`http://${originHost}/download/videos/${videoDetails.uuid}-${file.resolution.id}${extension}`) 197 expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`))
753 198
754 await Promise.all([ 199 await Promise.all([
755 makeRawRequest(file.torrentUrl, 200), 200 makeRawRequest(file.torrentUrl, 200),
756 makeRawRequest(file.torrentDownloadUrl, 200), 201 makeRawRequest(file.torrentDownloadUrl, 200),
757 makeRawRequest(file.metadataUrl, 200), 202 makeRawRequest(file.metadataUrl, 200)
758 // Backward compatibility
759 makeRawRequest(`http://${originHost}/static/torrents/${videoDetails.uuid}-${file.resolution.id}.torrent`, 200)
760 ]) 203 ])
761 204
762 expect(file.resolution.id).to.equal(attributeFile.resolution) 205 expect(file.resolution.id).to.equal(attributeFile.resolution)
@@ -776,149 +219,34 @@ async function completeVideoCheck (
776 } 219 }
777 220
778 expect(videoDetails.thumbnailPath).to.exist 221 expect(videoDetails.thumbnailPath).to.exist
779 await testImage(url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath) 222 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
780 223
781 if (attributes.previewfile) { 224 if (attributes.previewfile) {
782 expect(videoDetails.previewPath).to.exist 225 expect(videoDetails.previewPath).to.exist
783 await testImage(url, attributes.previewfile, videoDetails.previewPath) 226 await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
784 } 227 }
785} 228}
786 229
787async function videoUUIDToId (url: string, id: number | string) {
788 if (validator.isUUID('' + id) === false) return id
789
790 const res = await getVideo(url, id)
791 return res.body.id
792}
793
794async function uploadVideoAndGetId (options: {
795 server: ServerInfo
796 videoName: string
797 nsfw?: boolean
798 privacy?: VideoPrivacy
799 token?: string
800 fixture?: string
801}) {
802 const videoAttrs: any = { name: options.videoName }
803 if (options.nsfw) videoAttrs.nsfw = options.nsfw
804 if (options.privacy) videoAttrs.privacy = options.privacy
805 if (options.fixture) videoAttrs.fixture = options.fixture
806
807 const res = await uploadVideo(options.server.url, options.token || options.server.accessToken, videoAttrs)
808
809 return res.body.video as { id: number, uuid: string, shortUUID: string }
810}
811
812async function getLocalIdByUUID (url: string, uuid: string) {
813 const res = await getVideo(url, uuid)
814
815 return res.body.id
816}
817
818// serverNumber starts from 1 230// serverNumber starts from 1
819async function uploadRandomVideoOnServers (servers: ServerInfo[], serverNumber: number, additionalParams: any = {}) { 231async function uploadRandomVideoOnServers (
232 servers: PeerTubeServer[],
233 serverNumber: number,
234 additionalParams?: VideoEdit & { prefixName?: string }
235) {
820 const server = servers.find(s => s.serverNumber === serverNumber) 236 const server = servers.find(s => s.serverNumber === serverNumber)
821 const res = await uploadRandomVideo(server, false, additionalParams) 237 const res = await server.videos.randomUpload({ wait: false, additionalParams })
822 238
823 await waitJobs(servers) 239 await waitJobs(servers)
824 240
825 return res 241 return res
826} 242}
827 243
828async function uploadRandomVideo (server: ServerInfo, wait = true, additionalParams: any = {}) {
829 const prefixName = additionalParams.prefixName || ''
830 const name = prefixName + buildUUID()
831
832 const data = Object.assign({ name }, additionalParams)
833 const res = await uploadVideo(server.url, server.accessToken, data)
834
835 if (wait) await waitJobs([ server ])
836
837 return { uuid: res.body.video.uuid, name }
838}
839
840// --------------------------------------------------------------------------- 244// ---------------------------------------------------------------------------
841 245
842export { 246export {
843 getVideoDescription,
844 getVideoCategories,
845 uploadRandomVideo,
846 getVideoLicences,
847 videoUUIDToId,
848 getVideoPrivacies,
849 getVideoLanguages,
850 getMyVideos,
851 getAccountVideos,
852 getVideoChannelVideos,
853 getVideo,
854 getVideoFileMetadataUrl,
855 getVideoWithToken,
856 getVideosList,
857 removeAllVideos,
858 checkUploadVideoParam, 247 checkUploadVideoParam,
859 getVideosListPagination,
860 getVideosListSort,
861 removeVideo,
862 getVideosListWithToken,
863 uploadVideo,
864 sendResumableChunks,
865 getVideosWithFilters,
866 uploadRandomVideoOnServers,
867 updateVideo,
868 rateVideo,
869 viewVideo,
870 parseTorrentVideo,
871 getLocalVideos,
872 completeVideoCheck, 248 completeVideoCheck,
249 uploadRandomVideoOnServers,
873 checkVideoFilesWereRemoved, 250 checkVideoFilesWereRemoved,
874 getPlaylistVideos, 251 saveVideoInServers
875 getMyVideosWithFilter,
876 uploadVideoAndGetId,
877 getLocalIdByUUID,
878 getVideoIdFromUUID,
879 prepareResumableUpload
880}
881
882// ---------------------------------------------------------------------------
883
884function buildUploadReq (req: request.Test, attributes: VideoAttributes) {
885
886 for (const key of [ 'name', 'support', 'channelId', 'description', 'originallyPublishedAt' ]) {
887 if (attributes[key] !== undefined) {
888 req.field(key, attributes[key])
889 }
890 }
891
892 for (const key of [ 'nsfw', 'commentsEnabled', 'downloadEnabled', 'waitTranscoding' ]) {
893 if (attributes[key] !== undefined) {
894 req.field(key, JSON.stringify(attributes[key]))
895 }
896 }
897
898 for (const key of [ 'language', 'privacy', 'category', 'licence' ]) {
899 if (attributes[key] !== undefined) {
900 req.field(key, attributes[key].toString())
901 }
902 }
903
904 const tags = attributes.tags || []
905 for (let i = 0; i < tags.length; i++) {
906 req.field('tags[' + i + ']', attributes.tags[i])
907 }
908
909 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
910 if (attributes[key] !== undefined) {
911 req.attach(key, buildAbsoluteFixturePath(attributes[key]))
912 }
913 }
914
915 if (attributes.scheduleUpdate) {
916 if (attributes.scheduleUpdate.updateAt) {
917 req.field('scheduleUpdate[updateAt]', attributes.scheduleUpdate.updateAt)
918 }
919
920 if (attributes.scheduleUpdate.privacy) {
921 req.field('scheduleUpdate[privacy]', attributes.scheduleUpdate.privacy)
922 }
923 }
924} 252}
diff --git a/shared/core-utils/miscs/http-error-codes.ts b/shared/models/http/http-error-codes.ts
index b2fbdfc5a..b2fbdfc5a 100644
--- a/shared/core-utils/miscs/http-error-codes.ts
+++ b/shared/models/http/http-error-codes.ts
diff --git a/shared/core-utils/miscs/http-methods.ts b/shared/models/http/http-methods.ts
index 1cfa458b9..1cfa458b9 100644
--- a/shared/core-utils/miscs/http-methods.ts
+++ b/shared/models/http/http-methods.ts
diff --git a/shared/models/http/index.ts b/shared/models/http/index.ts
new file mode 100644
index 000000000..ec991afe0
--- /dev/null
+++ b/shared/models/http/index.ts
@@ -0,0 +1,2 @@
1export * from './http-error-codes'
2export * from './http-methods'
diff --git a/shared/models/index.ts b/shared/models/index.ts
index 5c2bc480e..78723d830 100644
--- a/shared/models/index.ts
+++ b/shared/models/index.ts
@@ -4,6 +4,7 @@ export * from './bulk'
4export * from './common' 4export * from './common'
5export * from './custom-markup' 5export * from './custom-markup'
6export * from './feeds' 6export * from './feeds'
7export * from './http'
7export * from './joinpeertube' 8export * from './joinpeertube'
8export * from './moderation' 9export * from './moderation'
9export * from './overviews' 10export * from './overviews'
diff --git a/shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts b/shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts
index 4703c0a8b..35247c1e3 100644
--- a/shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-playlist-privacy-manager.model.ts
@@ -1,8 +1,12 @@
1import { VideoPlaylistPrivacy } from '../../../videos/playlist/video-playlist-privacy.model' 1import { VideoPlaylistPrivacy } from '../../../videos/playlist/video-playlist-privacy.model'
2import { ConstantManager } from '../plugin-constant-manager.model'
2 3
3export interface PluginPlaylistPrivacyManager { 4export interface PluginPlaylistPrivacyManager extends ConstantManager<VideoPlaylistPrivacy> {
4 // PUBLIC = 1, 5 /**
5 // UNLISTED = 2, 6 * PUBLIC = 1,
6 // PRIVATE = 3 7 * UNLISTED = 2,
8 * PRIVATE = 3
9 * @deprecated use `deleteConstant` instead
10 */
7 deletePlaylistPrivacy: (privacyKey: VideoPlaylistPrivacy) => boolean 11 deletePlaylistPrivacy: (privacyKey: VideoPlaylistPrivacy) => boolean
8} 12}
diff --git a/shared/models/plugins/server/managers/plugin-video-category-manager.model.ts b/shared/models/plugins/server/managers/plugin-video-category-manager.model.ts
index 201bfa979..cf3d828fe 100644
--- a/shared/models/plugins/server/managers/plugin-video-category-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-video-category-manager.model.ts
@@ -1,5 +1,13 @@
1export interface PluginVideoCategoryManager { 1import { ConstantManager } from '../plugin-constant-manager.model'
2
3export interface PluginVideoCategoryManager extends ConstantManager<number> {
4 /**
5 * @deprecated use `addConstant` instead
6 */
2 addCategory: (categoryKey: number, categoryLabel: string) => boolean 7 addCategory: (categoryKey: number, categoryLabel: string) => boolean
3 8
9 /**
10 * @deprecated use `deleteConstant` instead
11 */
4 deleteCategory: (categoryKey: number) => boolean 12 deleteCategory: (categoryKey: number) => boolean
5} 13}
diff --git a/shared/models/plugins/server/managers/plugin-video-language-manager.model.ts b/shared/models/plugins/server/managers/plugin-video-language-manager.model.ts
index 3fd577a79..69fc8e503 100644
--- a/shared/models/plugins/server/managers/plugin-video-language-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-video-language-manager.model.ts
@@ -1,5 +1,13 @@
1export interface PluginVideoLanguageManager { 1import { ConstantManager } from '../plugin-constant-manager.model'
2
3export interface PluginVideoLanguageManager extends ConstantManager<string> {
4 /**
5 * @deprecated use `addConstant` instead
6 */
2 addLanguage: (languageKey: string, languageLabel: string) => boolean 7 addLanguage: (languageKey: string, languageLabel: string) => boolean
3 8
9 /**
10 * @deprecated use `deleteConstant` instead
11 */
4 deleteLanguage: (languageKey: string) => boolean 12 deleteLanguage: (languageKey: string) => boolean
5} 13}
diff --git a/shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts b/shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts
index 82a634d3a..6efeadd7d 100644
--- a/shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-video-licence-manager.model.ts
@@ -1,5 +1,13 @@
1export interface PluginVideoLicenceManager { 1import { ConstantManager } from '../plugin-constant-manager.model'
2
3export interface PluginVideoLicenceManager extends ConstantManager<number> {
4 /**
5 * @deprecated use `addLicence` instead
6 */
2 addLicence: (licenceKey: number, licenceLabel: string) => boolean 7 addLicence: (licenceKey: number, licenceLabel: string) => boolean
3 8
9 /**
10 * @deprecated use `deleteLicence` instead
11 */
4 deleteLicence: (licenceKey: number) => boolean 12 deleteLicence: (licenceKey: number) => boolean
5} 13}
diff --git a/shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts b/shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts
index 7717115e3..a237037db 100644
--- a/shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts
+++ b/shared/models/plugins/server/managers/plugin-video-privacy-manager.model.ts
@@ -1,9 +1,13 @@
1import { VideoPrivacy } from '../../../videos/video-privacy.enum' 1import { VideoPrivacy } from '../../../videos/video-privacy.enum'
2import { ConstantManager } from '../plugin-constant-manager.model'
2 3
3export interface PluginVideoPrivacyManager { 4export interface PluginVideoPrivacyManager extends ConstantManager<VideoPrivacy> {
4 // PUBLIC = 1 5 /**
5 // UNLISTED = 2 6 * PUBLIC = 1,
6 // PRIVATE = 3 7 * UNLISTED = 2,
7 // INTERNAL = 4 8 * PRIVATE = 3
9 * INTERNAL = 4
10 * @deprecated use `deleteConstant` instead
11 */
8 deletePrivacy: (privacyKey: VideoPrivacy) => boolean 12 deletePrivacy: (privacyKey: VideoPrivacy) => boolean
9} 13}
diff --git a/shared/models/plugins/server/plugin-constant-manager.model.ts b/shared/models/plugins/server/plugin-constant-manager.model.ts
new file mode 100644
index 000000000..4de3ce38f
--- /dev/null
+++ b/shared/models/plugins/server/plugin-constant-manager.model.ts
@@ -0,0 +1,7 @@
1export interface ConstantManager <K extends string | number> {
2 addConstant: (key: K, label: string) => boolean
3 deleteConstant: (key: K) => boolean
4 getConstantValue: (key: K) => string
5 getConstants: () => Record<K, string>
6 resetConstants: () => void
7}
diff --git a/shared/models/plugins/server/server-hook.model.ts b/shared/models/plugins/server/server-hook.model.ts
index 5f29534b5..562c6eb12 100644
--- a/shared/models/plugins/server/server-hook.model.ts
+++ b/shared/models/plugins/server/server-hook.model.ts
@@ -18,6 +18,10 @@ export const serverFilterHookObject = {
18 'filter:api.user.me.videos.list.params': true, 18 'filter:api.user.me.videos.list.params': true,
19 'filter:api.user.me.videos.list.result': true, 19 'filter:api.user.me.videos.list.result': true,
20 20
21 // Filter params/result used to list overview videos for the REST API
22 'filter:api.overviews.videos.list.params': true,
23 'filter:api.overviews.videos.list.result': true,
24
21 // Filter params/results to search videos/channels in the DB or on the remote index 25 // Filter params/results to search videos/channels in the DB or on the remote index
22 'filter:api.search.videos.local.list.params': true, 26 'filter:api.search.videos.local.list.params': true,
23 'filter:api.search.videos.local.list.result': true, 27 'filter:api.search.videos.local.list.result': true,
diff --git a/shared/models/search/video-channels-search-query.model.ts b/shared/models/search/video-channels-search-query.model.ts
index 8f93c4bd5..b68a1e80b 100644
--- a/shared/models/search/video-channels-search-query.model.ts
+++ b/shared/models/search/video-channels-search-query.model.ts
@@ -1,9 +1,18 @@
1import { SearchTargetQuery } from './search-target-query.model' 1import { SearchTargetQuery } from './search-target-query.model'
2 2
3export interface VideoChannelsSearchQuery extends SearchTargetQuery { 3export interface VideoChannelsSearchQuery extends SearchTargetQuery {
4 search: string 4 search?: string
5 5
6 start?: number 6 start?: number
7 count?: number 7 count?: number
8 sort?: string 8 sort?: string
9
10 host?: string
11 handles?: string[]
12}
13
14export interface VideoChannelsSearchQueryAfterSanitize extends VideoChannelsSearchQuery {
15 start: number
16 count: number
17 sort: string
9} 18}
diff --git a/shared/models/search/video-playlists-search-query.model.ts b/shared/models/search/video-playlists-search-query.model.ts
index 31f05218e..d9027eb5b 100644
--- a/shared/models/search/video-playlists-search-query.model.ts
+++ b/shared/models/search/video-playlists-search-query.model.ts
@@ -1,9 +1,20 @@
1import { SearchTargetQuery } from './search-target-query.model' 1import { SearchTargetQuery } from './search-target-query.model'
2 2
3export interface VideoPlaylistsSearchQuery extends SearchTargetQuery { 3export interface VideoPlaylistsSearchQuery extends SearchTargetQuery {
4 search: string 4 search?: string
5 5
6 start?: number 6 start?: number
7 count?: number 7 count?: number
8 sort?: string 8 sort?: string
9
10 host?: string
11
12 // UUIDs or short UUIDs
13 uuids?: string[]
14}
15
16export interface VideoPlaylistsSearchQueryAfterSanitize extends VideoPlaylistsSearchQuery {
17 start: number
18 count: number
19 sort: string
9} 20}
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts
index bd02489ea..2f2e9a934 100644
--- a/shared/models/search/videos-common-query.model.ts
+++ b/shared/models/search/videos-common-query.model.ts
@@ -21,6 +21,14 @@ export interface VideosCommonQuery {
21 tagsAllOf?: string[] 21 tagsAllOf?: string[]
22 22
23 filter?: VideoFilter 23 filter?: VideoFilter
24
25 skipCount?: boolean
26}
27
28export interface VideosCommonQueryAfterSanitize extends VideosCommonQuery {
29 start: number
30 count: number
31 sort: string
24} 32}
25 33
26export interface VideosWithSearchCommonQuery extends VideosCommonQuery { 34export interface VideosWithSearchCommonQuery extends VideosCommonQuery {
diff --git a/shared/models/search/videos-search-query.model.ts b/shared/models/search/videos-search-query.model.ts
index 406f6cab2..a5436879d 100644
--- a/shared/models/search/videos-search-query.model.ts
+++ b/shared/models/search/videos-search-query.model.ts
@@ -4,6 +4,8 @@ import { VideosCommonQuery } from './videos-common-query.model'
4export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery { 4export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery {
5 search?: string 5 search?: string
6 6
7 host?: string
8
7 startDate?: string // ISO 8601 9 startDate?: string // ISO 8601
8 endDate?: string // ISO 8601 10 endDate?: string // ISO 8601
9 11
@@ -12,4 +14,13 @@ export interface VideosSearchQuery extends SearchTargetQuery, VideosCommonQuery
12 14
13 durationMin?: number // seconds 15 durationMin?: number // seconds
14 durationMax?: number // seconds 16 durationMax?: number // seconds
17
18 // UUIDs or short UUIDs
19 uuids?: string[]
20}
21
22export interface VideosSearchQueryAfterSanitize extends VideosSearchQuery {
23 start: number
24 count: number
25 sort: string
15} 26}
diff --git a/shared/models/server/debug.model.ts b/shared/models/server/debug.model.ts
index 7ceff9137..2ecabdeca 100644
--- a/shared/models/server/debug.model.ts
+++ b/shared/models/server/debug.model.ts
@@ -1,5 +1,6 @@
1export interface Debug { 1export interface Debug {
2 ip: string 2 ip: string
3 activityPubMessagesWaiting: number
3} 4}
4 5
5export interface SendDebugCommand { 6export interface SendDebugCommand {
diff --git a/shared/models/server/index.ts b/shared/models/server/index.ts
index 06bf5c599..0f7646c7a 100644
--- a/shared/models/server/index.ts
+++ b/shared/models/server/index.ts
@@ -10,4 +10,5 @@ export * from './peertube-problem-document.model'
10export * from './server-config.model' 10export * from './server-config.model'
11export * from './server-debug.model' 11export * from './server-debug.model'
12export * from './server-error-code.enum' 12export * from './server-error-code.enum'
13export * from './server-follow-create.model'
13export * from './server-stats.model' 14export * from './server-stats.model'
diff --git a/shared/models/server/peertube-problem-document.model.ts b/shared/models/server/peertube-problem-document.model.ts
index e391d5aad..8dd96f7a3 100644
--- a/shared/models/server/peertube-problem-document.model.ts
+++ b/shared/models/server/peertube-problem-document.model.ts
@@ -1,4 +1,4 @@
1import { HttpStatusCode } from '../../core-utils' 1import { HttpStatusCode } from '../../models'
2import { OAuth2ErrorCode, ServerErrorCode } from './server-error-code.enum' 2import { OAuth2ErrorCode, ServerErrorCode } from './server-error-code.enum'
3 3
4export interface PeerTubeProblemDocumentData { 4export interface PeerTubeProblemDocumentData {
diff --git a/shared/models/server/server-follow-create.model.ts b/shared/models/server/server-follow-create.model.ts
new file mode 100644
index 000000000..3f90c7d6f
--- /dev/null
+++ b/shared/models/server/server-follow-create.model.ts
@@ -0,0 +1,4 @@
1export interface ServerFollowCreate {
2 hosts?: string[]
3 handles?: string[]
4}
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts
index a9d578054..b61a8cd40 100644
--- a/shared/models/users/index.ts
+++ b/shared/models/users/index.ts
@@ -1,3 +1,4 @@
1export * from './user-create-result.model'
1export * from './user-create.model' 2export * from './user-create.model'
2export * from './user-flag.model' 3export * from './user-flag.model'
3export * from './user-login.model' 4export * from './user-login.model'
diff --git a/shared/models/users/user-create-result.model.ts b/shared/models/users/user-create-result.model.ts
new file mode 100644
index 000000000..835b241ed
--- /dev/null
+++ b/shared/models/users/user-create-result.model.ts
@@ -0,0 +1,7 @@
1export interface UserCreateResult {
2 id: number
3
4 account: {
5 id: number
6 }
7}
diff --git a/shared/models/users/user-notification.model.ts b/shared/models/users/user-notification.model.ts
index 8b33e3fbd..5820589fe 100644
--- a/shared/models/users/user-notification.model.ts
+++ b/shared/models/users/user-notification.model.ts
@@ -36,6 +36,7 @@ export const enum UserNotificationType {
36export interface VideoInfo { 36export interface VideoInfo {
37 id: number 37 id: number
38 uuid: string 38 uuid: string
39 shortUUID: string
39 name: string 40 name: string
40} 41}
41 42
@@ -82,11 +83,7 @@ export interface UserNotification {
82 comment?: { 83 comment?: {
83 threadId: number 84 threadId: number
84 85
85 video: { 86 video: VideoInfo
86 id: number
87 uuid: string
88 name: string
89 }
90 } 87 }
91 88
92 account?: ActorInfo 89 account?: ActorInfo
diff --git a/shared/models/videos/channel/index.ts b/shared/models/videos/channel/index.ts
index 9dbaa42da..6cdabffbd 100644
--- a/shared/models/videos/channel/index.ts
+++ b/shared/models/videos/channel/index.ts
@@ -1,3 +1,4 @@
1export * from './video-channel-create-result.model'
1export * from './video-channel-create.model' 2export * from './video-channel-create.model'
2export * from './video-channel-update.model' 3export * from './video-channel-update.model'
3export * from './video-channel.model' 4export * from './video-channel.model'
diff --git a/shared/models/videos/channel/video-channel-create-result.model.ts b/shared/models/videos/channel/video-channel-create-result.model.ts
new file mode 100644
index 000000000..e3d7aeb4c
--- /dev/null
+++ b/shared/models/videos/channel/video-channel-create-result.model.ts
@@ -0,0 +1,3 @@
1export interface VideoChannelCreateResult {
2 id: number
3}
diff --git a/shared/models/videos/comment/index.ts b/shared/models/videos/comment/index.ts
index 7b9261a36..80c6c0724 100644
--- a/shared/models/videos/comment/index.ts
+++ b/shared/models/videos/comment/index.ts
@@ -1 +1,2 @@
1export * from './video-comment-create.model'
1export * from './video-comment.model' 2export * from './video-comment.model'
diff --git a/shared/models/videos/comment/video-comment-create.model.ts b/shared/models/videos/comment/video-comment-create.model.ts
new file mode 100644
index 000000000..1f0135405
--- /dev/null
+++ b/shared/models/videos/comment/video-comment-create.model.ts
@@ -0,0 +1,3 @@
1export interface VideoCommentCreate {
2 text: string
3}
diff --git a/shared/models/videos/comment/video-comment.model.ts b/shared/models/videos/comment/video-comment.model.ts
index 79c0e4c0a..737cfe098 100644
--- a/shared/models/videos/comment/video-comment.model.ts
+++ b/shared/models/videos/comment/video-comment.model.ts
@@ -1,3 +1,4 @@
1import { ResultList } from '../../common'
1import { Account } from '../../actors' 2import { Account } from '../../actors'
2 3
3export interface VideoComment { 4export interface VideoComment {
@@ -36,11 +37,9 @@ export interface VideoCommentAdmin {
36 } 37 }
37} 38}
38 39
40export type VideoCommentThreads = ResultList<VideoComment> & { totalNotDeletedComments: number }
41
39export interface VideoCommentThreadTree { 42export interface VideoCommentThreadTree {
40 comment: VideoComment 43 comment: VideoComment
41 children: VideoCommentThreadTree[] 44 children: VideoCommentThreadTree[]
42} 45}
43
44export interface VideoCommentCreate {
45 text: string
46}
diff --git a/shared/models/videos/playlist/index.ts b/shared/models/videos/playlist/index.ts
index f11a4bd28..a9e8ce496 100644
--- a/shared/models/videos/playlist/index.ts
+++ b/shared/models/videos/playlist/index.ts
@@ -1,6 +1,7 @@
1export * from './video-exist-in-playlist.model' 1export * from './video-exist-in-playlist.model'
2export * from './video-playlist-create-result.model' 2export * from './video-playlist-create-result.model'
3export * from './video-playlist-create.model' 3export * from './video-playlist-create.model'
4export * from './video-playlist-element-create-result.model'
4export * from './video-playlist-element-create.model' 5export * from './video-playlist-element-create.model'
5export * from './video-playlist-element-update.model' 6export * from './video-playlist-element-update.model'
6export * from './video-playlist-element.model' 7export * from './video-playlist-element.model'
diff --git a/shared/models/videos/playlist/video-playlist-element-create-result.model.ts b/shared/models/videos/playlist/video-playlist-element-create-result.model.ts
new file mode 100644
index 000000000..dc475e7d8
--- /dev/null
+++ b/shared/models/videos/playlist/video-playlist-element-create-result.model.ts
@@ -0,0 +1,3 @@
1export interface VideoPlaylistElementCreateResult {
2 id: number
3}
diff --git a/shared/models/videos/video-update.model.ts b/shared/models/videos/video-update.model.ts
index e21ccae04..86653b959 100644
--- a/shared/models/videos/video-update.model.ts
+++ b/shared/models/videos/video-update.model.ts
@@ -1,5 +1,6 @@
1import { VideoPrivacy } from './video-privacy.enum' 1import { VideoPrivacy } from './video-privacy.enum'
2import { VideoScheduleUpdate } from './video-schedule-update.model' 2import { VideoScheduleUpdate } from './video-schedule-update.model'
3
3export interface VideoUpdate { 4export interface VideoUpdate {
4 name?: string 5 name?: string
5 category?: number 6 category?: number