aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/extra-utils
diff options
context:
space:
mode:
Diffstat (limited to 'shared/extra-utils')
-rw-r--r--shared/extra-utils/bulk/bulk-command.ts20
-rw-r--r--shared/extra-utils/bulk/index.ts1
-rw-r--r--shared/extra-utils/cli/cli-command.ts27
-rw-r--r--shared/extra-utils/cli/index.ts1
-rw-r--r--shared/extra-utils/crypto.ts20
-rw-r--r--shared/extra-utils/custom-pages/custom-pages-command.ts33
-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/index.ts1
-rw-r--r--shared/extra-utils/ffprobe.ts187
-rw-r--r--shared/extra-utils/file.ts11
-rw-r--r--shared/extra-utils/index.ts19
-rw-r--r--shared/extra-utils/logs/index.ts1
-rw-r--r--shared/extra-utils/logs/logs-command.ts44
-rw-r--r--shared/extra-utils/miscs/checks.ts58
-rw-r--r--shared/extra-utils/miscs/generate.ts75
-rw-r--r--shared/extra-utils/miscs/index.ts5
-rw-r--r--shared/extra-utils/miscs/sql-command.ts141
-rw-r--r--shared/extra-utils/miscs/tests.ts101
-rw-r--r--shared/extra-utils/miscs/webtorrent.ts46
-rw-r--r--shared/extra-utils/mock-servers/index.ts5
-rw-r--r--shared/extra-utils/mock-servers/mock-429.ts33
-rw-r--r--shared/extra-utils/mock-servers/mock-email.ts63
-rw-r--r--shared/extra-utils/mock-servers/mock-instances-index.ts46
-rw-r--r--shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts34
-rw-r--r--shared/extra-utils/mock-servers/mock-object-storage.ts41
-rw-r--r--shared/extra-utils/mock-servers/mock-plugin-blocklist.ts36
-rw-r--r--shared/extra-utils/mock-servers/mock-proxy.ts25
-rw-r--r--shared/extra-utils/mock-servers/utils.ts33
-rw-r--r--shared/extra-utils/moderation/abuses-command.ts228
-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/requests/activitypub.ts42
-rw-r--r--shared/extra-utils/requests/check-api-params.ts48
-rw-r--r--shared/extra-utils/requests/index.ts3
-rw-r--r--shared/extra-utils/requests/requests.ts208
-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/server/config-command.ts341
-rw-r--r--shared/extra-utils/server/contact-form-command.ts31
-rw-r--r--shared/extra-utils/server/debug-command.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.ts20
-rw-r--r--shared/extra-utils/server/index.ts17
-rw-r--r--shared/extra-utils/server/jobs-command.ts61
-rw-r--r--shared/extra-utils/server/jobs.ts84
-rw-r--r--shared/extra-utils/server/object-storage-command.ts77
-rw-r--r--shared/extra-utils/server/plugins-command.ts256
-rw-r--r--shared/extra-utils/server/plugins.ts18
-rw-r--r--shared/extra-utils/server/redundancy-command.ts80
-rw-r--r--shared/extra-utils/server/server.ts389
-rw-r--r--shared/extra-utils/server/servers-command.ts92
-rw-r--r--shared/extra-utils/server/servers.ts49
-rw-r--r--shared/extra-utils/server/stats-command.ts25
-rw-r--r--shared/extra-utils/server/tracker.ts27
-rw-r--r--shared/extra-utils/shared/abstract-command.ts211
-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/users/accounts-command.ts78
-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/index.ts9
-rw-r--r--shared/extra-utils/users/login-command.ts132
-rw-r--r--shared/extra-utils/users/login.ts19
-rw-r--r--shared/extra-utils/users/notifications-command.ts86
-rw-r--r--shared/extra-utils/users/notifications.ts795
-rw-r--r--shared/extra-utils/users/subscriptions-command.ts99
-rw-r--r--shared/extra-utils/users/users-command.ts415
-rw-r--r--shared/extra-utils/uuid.ts32
-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.ts21
-rw-r--r--shared/extra-utils/videos/change-ownership-command.ts68
-rw-r--r--shared/extra-utils/videos/channels-command.ts178
-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.ts155
-rw-r--r--shared/extra-utils/videos/live.ts137
-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/streaming-playlists-command.ts44
-rw-r--r--shared/extra-utils/videos/streaming-playlists.ts77
-rw-r--r--shared/extra-utils/videos/videos-command.ts687
-rw-r--r--shared/extra-utils/videos/videos.ts253
91 files changed, 254 insertions, 7818 deletions
diff --git a/shared/extra-utils/bulk/bulk-command.ts b/shared/extra-utils/bulk/bulk-command.ts
deleted file mode 100644
index b5c5673ce..000000000
--- a/shared/extra-utils/bulk/bulk-command.ts
+++ /dev/null
@@ -1,20 +0,0 @@
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/index.ts b/shared/extra-utils/bulk/index.ts
deleted file mode 100644
index 391597243..000000000
--- a/shared/extra-utils/bulk/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './bulk-command'
diff --git a/shared/extra-utils/cli/cli-command.ts b/shared/extra-utils/cli/cli-command.ts
deleted file mode 100644
index ab9738174..000000000
--- a/shared/extra-utils/cli/cli-command.ts
+++ /dev/null
@@ -1,27 +0,0 @@
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, configOverride?: any) {
21 const prefix = configOverride
22 ? `NODE_CONFIG='${JSON.stringify(configOverride)}'`
23 : ''
24
25 return CLICommand.exec(`${prefix} ${this.getEnv()} ${command}`)
26 }
27}
diff --git a/shared/extra-utils/cli/index.ts b/shared/extra-utils/cli/index.ts
deleted file mode 100644
index 91b5abfbe..000000000
--- a/shared/extra-utils/cli/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './cli-command'
diff --git a/shared/extra-utils/crypto.ts b/shared/extra-utils/crypto.ts
new file mode 100644
index 000000000..1a583f1a0
--- /dev/null
+++ b/shared/extra-utils/crypto.ts
@@ -0,0 +1,20 @@
1import { BinaryToTextEncoding, createHash } from 'crypto'
2
3function sha256 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') {
4 return createHash('sha256').update(str).digest(encoding)
5}
6
7function sha1 (str: string | Buffer, encoding: BinaryToTextEncoding = 'hex') {
8 return createHash('sha1').update(str).digest(encoding)
9}
10
11// high excluded
12function randomInt (low: number, high: number) {
13 return Math.floor(Math.random() * (high - low) + low)
14}
15
16export {
17 randomInt,
18 sha256,
19 sha1
20}
diff --git a/shared/extra-utils/custom-pages/custom-pages-command.ts b/shared/extra-utils/custom-pages/custom-pages-command.ts
deleted file mode 100644
index cd869a8de..000000000
--- a/shared/extra-utils/custom-pages/custom-pages-command.ts
+++ /dev/null
@@ -1,33 +0,0 @@
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/index.ts b/shared/extra-utils/custom-pages/index.ts
deleted file mode 100644
index 58aed04f2..000000000
--- a/shared/extra-utils/custom-pages/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './custom-pages-command'
diff --git a/shared/extra-utils/feeds/feeds-command.ts b/shared/extra-utils/feeds/feeds-command.ts
deleted file mode 100644
index 3c95f9536..000000000
--- a/shared/extra-utils/feeds/feeds-command.ts
+++ /dev/null
@@ -1,44 +0,0 @@
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/index.ts b/shared/extra-utils/feeds/index.ts
deleted file mode 100644
index 662a22b6f..000000000
--- a/shared/extra-utils/feeds/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './feeds-command'
diff --git a/shared/extra-utils/ffprobe.ts b/shared/extra-utils/ffprobe.ts
new file mode 100644
index 000000000..53a3aa001
--- /dev/null
+++ b/shared/extra-utils/ffprobe.ts
@@ -0,0 +1,187 @@
1import { ffprobe, FfprobeData } from 'fluent-ffmpeg'
2import { VideoFileMetadata } from '@shared/models/videos'
3
4/**
5 *
6 * Helpers to run ffprobe and extract data from the JSON output
7 *
8 */
9
10function ffprobePromise (path: string) {
11 return new Promise<FfprobeData>((res, rej) => {
12 ffprobe(path, (err, data) => {
13 if (err) return rej(err)
14
15 return res(data)
16 })
17 })
18}
19
20async function isAudioFile (path: string, existingProbe?: FfprobeData) {
21 const videoStream = await getVideoStreamFromFile(path, existingProbe)
22
23 return !videoStream
24}
25
26async function getAudioStream (videoPath: string, existingProbe?: FfprobeData) {
27 // without position, ffprobe considers the last input only
28 // we make it consider the first input only
29 // if you pass a file path to pos, then ffprobe acts on that file directly
30 const data = existingProbe || await ffprobePromise(videoPath)
31
32 if (Array.isArray(data.streams)) {
33 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
34
35 if (audioStream) {
36 return {
37 absolutePath: data.format.filename,
38 audioStream,
39 bitrate: parseInt(audioStream['bit_rate'] + '', 10)
40 }
41 }
42 }
43
44 return { absolutePath: data.format.filename }
45}
46
47function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
48 const maxKBitrate = 384
49 const kToBits = (kbits: number) => kbits * 1000
50
51 // If we did not manage to get the bitrate, use an average value
52 if (!bitrate) return 256
53
54 if (type === 'aac') {
55 switch (true) {
56 case bitrate > kToBits(maxKBitrate):
57 return maxKBitrate
58
59 default:
60 return -1 // we interpret it as a signal to copy the audio stream as is
61 }
62 }
63
64 /*
65 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
66 That's why, when using aac, we can go to lower kbit/sec. The equivalences
67 made here are not made to be accurate, especially with good mp3 encoders.
68 */
69 switch (true) {
70 case bitrate <= kToBits(192):
71 return 128
72
73 case bitrate <= kToBits(384):
74 return 256
75
76 default:
77 return maxKBitrate
78 }
79}
80
81async function getVideoStreamSize (path: string, existingProbe?: FfprobeData): Promise<{ width: number, height: number }> {
82 const videoStream = await getVideoStreamFromFile(path, existingProbe)
83
84 return videoStream === null
85 ? { width: 0, height: 0 }
86 : { width: videoStream.width, height: videoStream.height }
87}
88
89async function getVideoFileResolution (path: string, existingProbe?: FfprobeData) {
90 const size = await getVideoStreamSize(path, existingProbe)
91
92 return {
93 width: size.width,
94 height: size.height,
95 ratio: Math.max(size.height, size.width) / Math.min(size.height, size.width),
96 resolution: Math.min(size.height, size.width),
97 isPortraitMode: size.height > size.width
98 }
99}
100
101async function getVideoFileFPS (path: string, existingProbe?: FfprobeData) {
102 const videoStream = await getVideoStreamFromFile(path, existingProbe)
103 if (videoStream === null) return 0
104
105 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
106 const valuesText: string = videoStream[key]
107 if (!valuesText) continue
108
109 const [ frames, seconds ] = valuesText.split('/')
110 if (!frames || !seconds) continue
111
112 const result = parseInt(frames, 10) / parseInt(seconds, 10)
113 if (result > 0) return Math.round(result)
114 }
115
116 return 0
117}
118
119async function getMetadataFromFile (path: string, existingProbe?: FfprobeData) {
120 const metadata = existingProbe || await ffprobePromise(path)
121
122 return new VideoFileMetadata(metadata)
123}
124
125async function getVideoFileBitrate (path: string, existingProbe?: FfprobeData): Promise<number> {
126 const metadata = await getMetadataFromFile(path, existingProbe)
127
128 let bitrate = metadata.format.bit_rate as number
129 if (bitrate && !isNaN(bitrate)) return bitrate
130
131 const videoStream = await getVideoStreamFromFile(path, existingProbe)
132 if (!videoStream) return undefined
133
134 bitrate = videoStream?.bit_rate
135 if (bitrate && !isNaN(bitrate)) return bitrate
136
137 return undefined
138}
139
140async function getDurationFromVideoFile (path: string, existingProbe?: FfprobeData) {
141 const metadata = await getMetadataFromFile(path, existingProbe)
142
143 return Math.round(metadata.format.duration)
144}
145
146async function getVideoStreamFromFile (path: string, existingProbe?: FfprobeData) {
147 const metadata = await getMetadataFromFile(path, existingProbe)
148
149 return metadata.streams.find(s => s.codec_type === 'video') || null
150}
151
152async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
153 const parsedAudio = await getAudioStream(path, probe)
154
155 if (!parsedAudio.audioStream) return true
156
157 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
158
159 const audioBitrate = parsedAudio.bitrate
160 if (!audioBitrate) return false
161
162 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
163 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
164
165 const channelLayout = parsedAudio.audioStream['channel_layout']
166 // Causes playback issues with Chrome
167 if (!channelLayout || channelLayout === 'unknown') return false
168
169 return true
170}
171
172// ---------------------------------------------------------------------------
173
174export {
175 getVideoStreamSize,
176 getVideoFileResolution,
177 getMetadataFromFile,
178 getMaxAudioBitrate,
179 getVideoStreamFromFile,
180 getDurationFromVideoFile,
181 getAudioStream,
182 getVideoFileFPS,
183 isAudioFile,
184 ffprobePromise,
185 getVideoFileBitrate,
186 canDoQuickAudioTranscode
187}
diff --git a/shared/extra-utils/file.ts b/shared/extra-utils/file.ts
new file mode 100644
index 000000000..8060ab520
--- /dev/null
+++ b/shared/extra-utils/file.ts
@@ -0,0 +1,11 @@
1import { stat } from 'fs-extra'
2
3async function getFileSize (path: string) {
4 const stats = await stat(path)
5
6 return stats.size
7}
8
9export {
10 getFileSize
11}
diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts
index 4b3636d06..e2e161a7b 100644
--- a/shared/extra-utils/index.ts
+++ b/shared/extra-utils/index.ts
@@ -1,15 +1,4 @@
1export * from './bulk' 1export * from './crypto'
2export * from './cli' 2export * from './ffprobe'
3export * from './custom-pages' 3export * from './file'
4export * from './feeds' 4export * from './uuid'
5export * from './logs'
6export * from './miscs'
7export * from './mock-servers'
8export * from './moderation'
9export * from './overviews'
10export * from './requests'
11export * from './search'
12export * from './server'
13export * from './socket'
14export * from './users'
15export * from './videos'
diff --git a/shared/extra-utils/logs/index.ts b/shared/extra-utils/logs/index.ts
deleted file mode 100644
index 69452d7f0..000000000
--- a/shared/extra-utils/logs/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './logs-command'
diff --git a/shared/extra-utils/logs/logs-command.ts b/shared/extra-utils/logs/logs-command.ts
deleted file mode 100644
index 7b5c66c0c..000000000
--- a/shared/extra-utils/logs/logs-command.ts
+++ /dev/null
@@ -1,44 +0,0 @@
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 tagsOneOf?: string[]
12 }) {
13 const { startDate, endDate, tagsOneOf, level } = options
14 const path = '/api/v1/server/logs'
15
16 return this.getRequestBody<any[]>({
17 ...options,
18
19 path,
20 query: { startDate, endDate, level, tagsOneOf },
21 implicitToken: true,
22 defaultExpectedStatus: HttpStatusCode.OK_200
23 })
24 }
25
26 getAuditLogs (options: OverrideCommandOptions & {
27 startDate: Date
28 endDate?: Date
29 }) {
30 const { startDate, endDate } = options
31
32 const path = '/api/v1/server/audit-logs'
33
34 return this.getRequestBody({
35 ...options,
36
37 path,
38 query: { startDate, endDate },
39 implicitToken: true,
40 defaultExpectedStatus: HttpStatusCode.OK_200
41 })
42 }
43
44}
diff --git a/shared/extra-utils/miscs/checks.ts b/shared/extra-utils/miscs/checks.ts
deleted file mode 100644
index b1be214b1..000000000
--- a/shared/extra-utils/miscs/checks.ts
+++ /dev/null
@@ -1,58 +0,0 @@
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
19function expectStartWith (str: string, start: string) {
20 expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.true
21}
22
23async function expectLogDoesNotContain (server: PeerTubeServer, str: string) {
24 const content = await server.servers.getLogContent()
25
26 expect(content.toString()).to.not.contain(str)
27}
28
29async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') {
30 const res = await makeGetRequest({
31 url,
32 path: imagePath,
33 expectedStatus: HttpStatusCode.OK_200
34 })
35
36 const body = res.body
37
38 const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension))
39 const minLength = body.length - ((30 * body.length) / 100)
40 const maxLength = body.length + ((30 * body.length) / 100)
41
42 expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture')
43 expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture')
44}
45
46async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) {
47 const base = server.servers.buildDirectory(directory)
48
49 expect(await pathExists(join(base, filePath))).to.equal(exist)
50}
51
52export {
53 dateIsValid,
54 testImage,
55 expectLogDoesNotContain,
56 testFileExistsOrNot,
57 expectStartWith
58}
diff --git a/shared/extra-utils/miscs/generate.ts b/shared/extra-utils/miscs/generate.ts
deleted file mode 100644
index 3b29c0ad4..000000000
--- a/shared/extra-utils/miscs/generate.ts
+++ /dev/null
@@ -1,75 +0,0 @@
1import { expect } from 'chai'
2import ffmpeg from 'fluent-ffmpeg'
3import { ensureDir, pathExists } from 'fs-extra'
4import { dirname } from 'path'
5import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
6import { getMaxBitrate } from '@shared/core-utils'
7import { buildAbsoluteFixturePath } from './tests'
8
9async function ensureHasTooBigBitrate (fixturePath: string) {
10 const bitrate = await getVideoFileBitrate(fixturePath)
11 const dataResolution = await getVideoFileResolution(fixturePath)
12 const fps = await getVideoFileFPS(fixturePath)
13
14 const maxBitrate = getMaxBitrate({ ...dataResolution, fps })
15 expect(bitrate).to.be.above(maxBitrate)
16}
17
18async function generateHighBitrateVideo () {
19 const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true)
20
21 await ensureDir(dirname(tempFixturePath))
22
23 const exists = await pathExists(tempFixturePath)
24 if (!exists) {
25 console.log('Generating high bitrate video.')
26
27 // Generate a random, high bitrate video on the fly, so we don't have to include
28 // a large file in the repo. The video needs to have a certain minimum length so
29 // that FFmpeg properly applies bitrate limits.
30 // https://stackoverflow.com/a/15795112
31 return new Promise<string>((res, rej) => {
32 ffmpeg()
33 .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ])
34 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
35 .outputOptions([ '-maxrate 10M', '-bufsize 10M' ])
36 .output(tempFixturePath)
37 .on('error', rej)
38 .on('end', () => res(tempFixturePath))
39 .run()
40 })
41 }
42
43 await ensureHasTooBigBitrate(tempFixturePath)
44
45 return tempFixturePath
46}
47
48async function generateVideoWithFramerate (fps = 60) {
49 const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true)
50
51 await ensureDir(dirname(tempFixturePath))
52
53 const exists = await pathExists(tempFixturePath)
54 if (!exists) {
55 console.log('Generating video with framerate %d.', fps)
56
57 return new Promise<string>((res, rej) => {
58 ffmpeg()
59 .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ])
60 .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ])
61 .outputOptions([ `-r ${fps}` ])
62 .output(tempFixturePath)
63 .on('error', rej)
64 .on('end', () => res(tempFixturePath))
65 .run()
66 })
67 }
68
69 return tempFixturePath
70}
71
72export {
73 generateHighBitrateVideo,
74 generateVideoWithFramerate
75}
diff --git a/shared/extra-utils/miscs/index.ts b/shared/extra-utils/miscs/index.ts
deleted file mode 100644
index 4474661de..000000000
--- a/shared/extra-utils/miscs/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
1export * from './checks'
2export * from './generate'
3export * from './sql-command'
4export * from './tests'
5export * from './webtorrent'
diff --git a/shared/extra-utils/miscs/sql-command.ts b/shared/extra-utils/miscs/sql-command.ts
deleted file mode 100644
index bedb3349b..000000000
--- a/shared/extra-utils/miscs/sql-command.ts
+++ /dev/null
@@ -1,141 +0,0 @@
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 const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' +
54 `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'`
55
56 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
57 const [ { total } ] = await seq.query<{ total: number }>(query, options)
58
59 if (!total) return 0
60
61 return parseInt(total + '', 10)
62 }
63
64 getActorImage (filename: string) {
65 return this.selectQuery(`SELECT * FROM "actorImage" WHERE filename = '${filename}'`)
66 .then(rows => rows[0])
67 }
68
69 selectQuery (query: string) {
70 const seq = this.getSequelize()
71 const options = { type: QueryTypes.SELECT as QueryTypes.SELECT }
72
73 return seq.query<any>(query, options)
74 }
75
76 updateQuery (query: string) {
77 const seq = this.getSequelize()
78 const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE }
79
80 return seq.query(query, options)
81 }
82
83 setPluginField (pluginName: string, field: string, value: string) {
84 const seq = this.getSequelize()
85
86 const options = { type: QueryTypes.UPDATE }
87
88 return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options)
89 }
90
91 setPluginVersion (pluginName: string, newVersion: string) {
92 return this.setPluginField(pluginName, 'version', newVersion)
93 }
94
95 setPluginLatestVersion (pluginName: string, newVersion: string) {
96 return this.setPluginField(pluginName, 'latestVersion', newVersion)
97 }
98
99 setActorFollowScores (newScore: number) {
100 const seq = this.getSequelize()
101
102 const options = { type: QueryTypes.UPDATE }
103
104 return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options)
105 }
106
107 setTokenField (accessToken: string, field: string, value: string) {
108 const seq = this.getSequelize()
109
110 const options = { type: QueryTypes.UPDATE }
111
112 return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options)
113 }
114
115 async cleanup () {
116 if (!this.sequelize) return
117
118 await this.sequelize.close()
119 this.sequelize = undefined
120 }
121
122 private getSequelize () {
123 if (this.sequelize) return this.sequelize
124
125 const dbname = 'peertube_test' + this.server.internalServerNumber
126 const username = 'peertube'
127 const password = 'peertube'
128 const host = 'localhost'
129 const port = 5432
130
131 this.sequelize = new Sequelize(dbname, username, password, {
132 dialect: 'postgres',
133 host,
134 port,
135 logging: false
136 })
137
138 return this.sequelize
139 }
140
141}
diff --git a/shared/extra-utils/miscs/tests.ts b/shared/extra-utils/miscs/tests.ts
deleted file mode 100644
index 658fe5fd3..000000000
--- a/shared/extra-utils/miscs/tests.ts
+++ /dev/null
@@ -1,101 +0,0 @@
1import { stat } from 'fs-extra'
2import { basename, isAbsolute, join, resolve } from 'path'
3
4const FIXTURE_URLS = {
5 peertube_long: 'https://peertube2.cpy.re/videos/watch/122d093a-1ede-43bd-bd34-59d2931ffc5e',
6 peertube_short: 'https://peertube2.cpy.re/w/3fbif9S3WmtTP8gGsC5HBd',
7
8 youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM',
9
10 /**
11 * The video is used to check format-selection correctness wrt. HDR,
12 * which brings its own set of oddities outside of a MediaSource.
13 *
14 * The video needs to have the following format_ids:
15 * (which you can check by using `youtube-dl <url> -F`):
16 * - (webm vp9)
17 * - (mp4 avc1)
18 * - (webm vp9.2 HDR)
19 */
20 youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4',
21
22 // eslint-disable-next-line max-len
23 magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Flazy-static%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4',
24
25 badVideo: 'https://download.cpy.re/peertube/bad_video.mp4',
26 goodVideo: 'https://download.cpy.re/peertube/good_video.mp4',
27 goodVideo720: 'https://download.cpy.re/peertube/good_video_720.mp4',
28
29 file4K: 'https://download.cpy.re/peertube/4k_file.txt'
30}
31
32function parallelTests () {
33 return process.env.MOCHA_PARALLEL === 'true'
34}
35
36function isGithubCI () {
37 return !!process.env.GITHUB_WORKSPACE
38}
39
40function areHttpImportTestsDisabled () {
41 const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true'
42
43 if (disabled) console.log('DISABLE_HTTP_IMPORT_TESTS env set to "true" so import tests are disabled')
44
45 return disabled
46}
47
48function areObjectStorageTestsDisabled () {
49 const disabled = process.env.ENABLE_OBJECT_STORAGE_TESTS !== 'true'
50
51 if (disabled) console.log('ENABLE_OBJECT_STORAGE_TESTS env is not set to "true" so object storage tests are disabled')
52
53 return disabled
54}
55
56function buildAbsoluteFixturePath (path: string, customCIPath = false) {
57 if (isAbsolute(path)) return path
58
59 if (customCIPath && process.env.GITHUB_WORKSPACE) {
60 return join(process.env.GITHUB_WORKSPACE, 'fixtures', path)
61 }
62
63 return join(root(), 'server', 'tests', 'fixtures', path)
64}
65
66function root () {
67 // We are in /miscs
68 let root = join(__dirname, '..', '..', '..')
69
70 if (basename(root) === 'dist') root = resolve(root, '..')
71
72 return root
73}
74
75function wait (milliseconds: number) {
76 return new Promise(resolve => setTimeout(resolve, milliseconds))
77}
78
79async function getFileSize (path: string) {
80 const stats = await stat(path)
81
82 return stats.size
83}
84
85function buildRequestStub (): any {
86 return { }
87}
88
89export {
90 FIXTURE_URLS,
91
92 parallelTests,
93 isGithubCI,
94 areHttpImportTestsDisabled,
95 buildAbsoluteFixturePath,
96 getFileSize,
97 buildRequestStub,
98 areObjectStorageTestsDisabled,
99 wait,
100 root
101}
diff --git a/shared/extra-utils/miscs/webtorrent.ts b/shared/extra-utils/miscs/webtorrent.ts
deleted file mode 100644
index 0683f8893..000000000
--- a/shared/extra-utils/miscs/webtorrent.ts
+++ /dev/null
@@ -1,46 +0,0 @@
1import { readFile } from 'fs-extra'
2import 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 (torrentId: string, refreshWebTorrent = false) {
11 const WebTorrent = require('webtorrent')
12
13 if (webtorrent && refreshWebTorrent) webtorrent.destroy()
14 if (!webtorrent || refreshWebTorrent) webtorrent = new WebTorrent()
15
16 webtorrent.on('error', err => console.error('Error in webtorrent', err))
17
18 return new Promise<WebTorrent.Torrent>(res => {
19 const torrent = webtorrent.add(torrentId, res)
20
21 torrent.on('error', err => console.error('Error in webtorrent torrent', err))
22 torrent.on('warning', warn => {
23 const msg = typeof warn === 'string'
24 ? warn
25 : warn.message
26
27 if (msg.includes('Unsupported')) return
28
29 console.error('Warning in webtorrent torrent', warn)
30 })
31 })
32}
33
34async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) {
35 const torrentName = basename(file.torrentUrl)
36 const torrentPath = server.servers.buildDirectory(join('torrents', torrentName))
37
38 const data = await readFile(torrentPath)
39
40 return parseTorrent(data)
41}
42
43export {
44 webtorrentAdd,
45 parseTorrentVideo
46}
diff --git a/shared/extra-utils/mock-servers/index.ts b/shared/extra-utils/mock-servers/index.ts
deleted file mode 100644
index 93c00c788..000000000
--- a/shared/extra-utils/mock-servers/index.ts
+++ /dev/null
@@ -1,5 +0,0 @@
1export * from './mock-email'
2export * from './mock-instances-index'
3export * from './mock-joinpeertube-versions'
4export * from './mock-plugin-blocklist'
5export * from './mock-object-storage'
diff --git a/shared/extra-utils/mock-servers/mock-429.ts b/shared/extra-utils/mock-servers/mock-429.ts
deleted file mode 100644
index 9e0d1281a..000000000
--- a/shared/extra-utils/mock-servers/mock-429.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import express from 'express'
2import { Server } from 'http'
3import { getPort, randomListen, terminateServer } from './utils'
4
5export class Mock429 {
6 private server: Server
7 private responseSent = false
8
9 async initialize () {
10 const app = express()
11
12 app.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
13
14 if (!this.responseSent) {
15 this.responseSent = true
16
17 // Retry after 5 seconds
18 res.header('retry-after', '2')
19 return res.sendStatus(429)
20 }
21
22 return res.sendStatus(200)
23 })
24
25 this.server = await randomListen(app)
26
27 return getPort(this.server)
28 }
29
30 terminate () {
31 return terminateServer(this.server)
32 }
33}
diff --git a/shared/extra-utils/mock-servers/mock-email.ts b/shared/extra-utils/mock-servers/mock-email.ts
deleted file mode 100644
index f646c1621..000000000
--- a/shared/extra-utils/mock-servers/mock-email.ts
+++ /dev/null
@@ -1,63 +0,0 @@
1import { ChildProcess } from 'child_process'
2import MailDev from '@peertube/maildev'
3import { randomInt } from '@shared/core-utils'
4import { parallelTests } from '../miscs'
5
6class MockSmtpServer {
7
8 private static instance: MockSmtpServer
9 private started = false
10 private emailChildProcess: ChildProcess
11 private emails: object[]
12
13 private constructor () { }
14
15 collectEmails (emailsCollection: object[]) {
16 return new Promise<number>((res, rej) => {
17 const port = parallelTests() ? randomInt(1000, 2000) : 1025
18 this.emails = emailsCollection
19
20 if (this.started) {
21 return res(undefined)
22 }
23
24 const maildev = new MailDev({
25 ip: '127.0.0.1',
26 smtp: port,
27 disableWeb: true,
28 silent: true
29 })
30
31 maildev.on('new', email => {
32 this.emails.push(email)
33 })
34
35 maildev.listen(err => {
36 if (err) return rej(err)
37
38 this.started = true
39
40 return res(port)
41 })
42 })
43 }
44
45 kill () {
46 if (!this.emailChildProcess) return
47
48 process.kill(this.emailChildProcess.pid)
49
50 this.emailChildProcess = null
51 MockSmtpServer.instance = null
52 }
53
54 static get Instance () {
55 return this.instance || (this.instance = new this())
56 }
57}
58
59// ---------------------------------------------------------------------------
60
61export {
62 MockSmtpServer
63}
diff --git a/shared/extra-utils/mock-servers/mock-instances-index.ts b/shared/extra-utils/mock-servers/mock-instances-index.ts
deleted file mode 100644
index 92b12d6f3..000000000
--- a/shared/extra-utils/mock-servers/mock-instances-index.ts
+++ /dev/null
@@ -1,46 +0,0 @@
1import express from 'express'
2import { Server } from 'http'
3import { getPort, randomListen, terminateServer } from './utils'
4
5export class MockInstancesIndex {
6 private server: Server
7
8 private readonly indexInstances: { host: string, createdAt: string }[] = []
9
10 async initialize () {
11 const app = express()
12
13 app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
14 if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
15
16 return next()
17 })
18
19 app.get('/api/v1/instances/hosts', (req: express.Request, res: express.Response) => {
20 const since = req.query.since
21
22 const filtered = this.indexInstances.filter(i => {
23 if (!since) return true
24
25 return i.createdAt > since
26 })
27
28 return res.json({
29 total: filtered.length,
30 data: filtered
31 })
32 })
33
34 this.server = await randomListen(app)
35
36 return getPort(this.server)
37 }
38
39 addInstance (host: string) {
40 this.indexInstances.push({ host, createdAt: new Date().toISOString() })
41 }
42
43 terminate () {
44 return terminateServer(this.server)
45 }
46}
diff --git a/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts b/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts
deleted file mode 100644
index e7906ea56..000000000
--- a/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts
+++ /dev/null
@@ -1,34 +0,0 @@
1import express from 'express'
2import { Server } from 'http'
3import { getPort, randomListen } from './utils'
4
5export class MockJoinPeerTubeVersions {
6 private server: Server
7 private latestVersion: string
8
9 async initialize () {
10 const app = express()
11
12 app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => {
13 if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url)
14
15 return next()
16 })
17
18 app.get('/versions.json', (req: express.Request, res: express.Response) => {
19 return res.json({
20 peertube: {
21 latestVersion: this.latestVersion
22 }
23 })
24 })
25
26 this.server = await randomListen(app)
27
28 return getPort(this.server)
29 }
30
31 setLatestVersion (latestVersion: string) {
32 this.latestVersion = latestVersion
33 }
34}
diff --git a/shared/extra-utils/mock-servers/mock-object-storage.ts b/shared/extra-utils/mock-servers/mock-object-storage.ts
deleted file mode 100644
index d135c2631..000000000
--- a/shared/extra-utils/mock-servers/mock-object-storage.ts
+++ /dev/null
@@ -1,41 +0,0 @@
1import express from 'express'
2import got, { RequestError } from 'got'
3import { Server } from 'http'
4import { pipeline } from 'stream'
5import { ObjectStorageCommand } from '../server'
6import { getPort, randomListen, terminateServer } from './utils'
7
8export class MockObjectStorage {
9 private server: Server
10
11 async initialize () {
12 const app = express()
13
14 app.get('/:bucketName/:path(*)', (req: express.Request, res: express.Response, next: express.NextFunction) => {
15 const url = `http://${req.params.bucketName}.${ObjectStorageCommand.getEndpointHost()}/${req.params.path}`
16
17 if (process.env.DEBUG) {
18 console.log('Receiving request on mocked server %s.', req.url)
19 console.log('Proxifying request to %s', url)
20 }
21
22 return pipeline(
23 got.stream(url, { throwHttpErrors: false }),
24 res,
25 (err: RequestError) => {
26 if (!err) return
27
28 console.error('Pipeline failed.', err)
29 }
30 )
31 })
32
33 this.server = await randomListen(app)
34
35 return getPort(this.server)
36 }
37
38 terminate () {
39 return terminateServer(this.server)
40 }
41}
diff --git a/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts b/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts
deleted file mode 100644
index f8a271cba..000000000
--- a/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts
+++ /dev/null
@@ -1,36 +0,0 @@
1import express, { Request, Response } from 'express'
2import { Server } from 'http'
3import { getPort, randomListen, terminateServer } from './utils'
4
5type BlocklistResponse = {
6 data: {
7 value: string
8 action?: 'add' | 'remove'
9 updatedAt?: string
10 }[]
11}
12
13export class MockBlocklist {
14 private body: BlocklistResponse
15 private server: Server
16
17 async initialize () {
18 const app = express()
19
20 app.get('/blocklist', (req: Request, res: Response) => {
21 return res.json(this.body)
22 })
23
24 this.server = await randomListen(app)
25
26 return getPort(this.server)
27 }
28
29 replace (body: BlocklistResponse) {
30 this.body = body
31 }
32
33 terminate () {
34 return terminateServer(this.server)
35 }
36}
diff --git a/shared/extra-utils/mock-servers/mock-proxy.ts b/shared/extra-utils/mock-servers/mock-proxy.ts
deleted file mode 100644
index 75ac79055..000000000
--- a/shared/extra-utils/mock-servers/mock-proxy.ts
+++ /dev/null
@@ -1,25 +0,0 @@
1
2import { createServer, Server } from 'http'
3import proxy from 'proxy'
4import { getPort, terminateServer } from './utils'
5
6class MockProxy {
7 private server: Server
8
9 initialize () {
10 return new Promise<number>(res => {
11 this.server = proxy(createServer())
12 this.server.listen(0, () => res(getPort(this.server)))
13 })
14 }
15
16 terminate () {
17 return terminateServer(this.server)
18 }
19}
20
21// ---------------------------------------------------------------------------
22
23export {
24 MockProxy
25}
diff --git a/shared/extra-utils/mock-servers/utils.ts b/shared/extra-utils/mock-servers/utils.ts
deleted file mode 100644
index 235642439..000000000
--- a/shared/extra-utils/mock-servers/utils.ts
+++ /dev/null
@@ -1,33 +0,0 @@
1import { Express } from 'express'
2import { Server } from 'http'
3import { AddressInfo } from 'net'
4
5function randomListen (app: Express) {
6 return new Promise<Server>(res => {
7 const server = app.listen(0, () => res(server))
8 })
9}
10
11function getPort (server: Server) {
12 const address = server.address() as AddressInfo
13
14 return address.port
15}
16
17function terminateServer (server: Server) {
18 if (!server) return Promise.resolve()
19
20 return new Promise<void>((res, rej) => {
21 server.close(err => {
22 if (err) return rej(err)
23
24 return res()
25 })
26 })
27}
28
29export {
30 randomListen,
31 getPort,
32 terminateServer
33}
diff --git a/shared/extra-utils/moderation/abuses-command.ts b/shared/extra-utils/moderation/abuses-command.ts
deleted file mode 100644
index 0db32ba46..000000000
--- a/shared/extra-utils/moderation/abuses-command.ts
+++ /dev/null
@@ -1,228 +0,0 @@
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/index.ts b/shared/extra-utils/moderation/index.ts
deleted file mode 100644
index b37643956..000000000
--- a/shared/extra-utils/moderation/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './abuses-command'
diff --git a/shared/extra-utils/overviews/index.ts b/shared/extra-utils/overviews/index.ts
deleted file mode 100644
index e19551907..000000000
--- a/shared/extra-utils/overviews/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './overviews-command'
diff --git a/shared/extra-utils/overviews/overviews-command.ts b/shared/extra-utils/overviews/overviews-command.ts
deleted file mode 100644
index 06b4892d2..000000000
--- a/shared/extra-utils/overviews/overviews-command.ts
+++ /dev/null
@@ -1,23 +0,0 @@
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/requests/activitypub.ts b/shared/extra-utils/requests/activitypub.ts
deleted file mode 100644
index 4ae878384..000000000
--- a/shared/extra-utils/requests/activitypub.ts
+++ /dev/null
@@ -1,42 +0,0 @@
1import { activityPubContextify } from '../../../server/helpers/activitypub'
2import { doRequest } from '../../../server/helpers/requests'
3import { HTTP_SIGNATURE } from '../../../server/initializers/constants'
4import { buildGlobalHeaders } from '../../../server/lib/job-queue/handlers/utils/activitypub-http-utils'
5
6function makePOSTAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = {
8 method: 'POST' as 'POST',
9 json: body,
10 httpSignature,
11 headers
12 }
13
14 return doRequest(url, options)
15}
16
17async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
18 const follow = {
19 type: 'Follow',
20 id: by.url + '/' + new Date().getTime(),
21 actor: by.url,
22 object: to.url
23 }
24
25 const body = activityPubContextify(follow)
26
27 const httpSignature = {
28 algorithm: HTTP_SIGNATURE.ALGORITHM,
29 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
30 keyId: by.url,
31 key: by.privateKey,
32 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
33 }
34 const headers = buildGlobalHeaders(body)
35
36 return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
37}
38
39export {
40 makePOSTAPRequest,
41 makeFollowRequest
42}
diff --git a/shared/extra-utils/requests/check-api-params.ts b/shared/extra-utils/requests/check-api-params.ts
deleted file mode 100644
index 26ba1e913..000000000
--- a/shared/extra-utils/requests/check-api-params.ts
+++ /dev/null
@@ -1,48 +0,0 @@
1import { HttpStatusCode } from '@shared/models'
2import { makeGetRequest } from './requests'
3
4function checkBadStartPagination (url: string, path: string, token?: string, query = {}) {
5 return makeGetRequest({
6 url,
7 path,
8 token,
9 query: { ...query, start: 'hello' },
10 expectedStatus: HttpStatusCode.BAD_REQUEST_400
11 })
12}
13
14async function checkBadCountPagination (url: string, path: string, token?: string, query = {}) {
15 await makeGetRequest({
16 url,
17 path,
18 token,
19 query: { ...query, count: 'hello' },
20 expectedStatus: HttpStatusCode.BAD_REQUEST_400
21 })
22
23 await makeGetRequest({
24 url,
25 path,
26 token,
27 query: { ...query, count: 2000 },
28 expectedStatus: HttpStatusCode.BAD_REQUEST_400
29 })
30}
31
32function checkBadSortPagination (url: string, path: string, token?: string, query = {}) {
33 return makeGetRequest({
34 url,
35 path,
36 token,
37 query: { ...query, sort: 'hello' },
38 expectedStatus: HttpStatusCode.BAD_REQUEST_400
39 })
40}
41
42// ---------------------------------------------------------------------------
43
44export {
45 checkBadStartPagination,
46 checkBadCountPagination,
47 checkBadSortPagination
48}
diff --git a/shared/extra-utils/requests/index.ts b/shared/extra-utils/requests/index.ts
deleted file mode 100644
index 501163f92..000000000
--- a/shared/extra-utils/requests/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
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
deleted file mode 100644
index b6b9024ed..000000000
--- a/shared/extra-utils/requests/requests.ts
+++ /dev/null
@@ -1,208 +0,0 @@
1/* eslint-disable @typescript-eslint/no-floating-promises */
2
3import { decode } from 'querystring'
4import request from 'supertest'
5import { URL } from 'url'
6import { HttpStatusCode } from '@shared/models'
7import { buildAbsoluteFixturePath } from '../miscs/tests'
8
9export type CommonRequestParams = {
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
22}
23
24function makeRawRequest (url: string, expectedStatus?: HttpStatusCode, range?: string) {
25 const { host, protocol, pathname } = new URL(url)
26
27 return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, expectedStatus, range })
28}
29
30function makeGetRequest (options: CommonRequestParams & {
31 query?: any
32 rawQuery?: string
33}) {
34 const req = request(options.url).get(options.path)
35
36 if (options.query) req.query(options.query)
37 if (options.rawQuery) req.query(options.rawQuery)
38
39 return buildRequest(req, { contentType: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
40}
41
42function makeHTMLRequest (url: string, path: string) {
43 return makeGetRequest({
44 url,
45 path,
46 accept: 'text/html',
47 expectedStatus: HttpStatusCode.OK_200
48 })
49}
50
51function makeActivityPubGetRequest (url: string, path: string, expectedStatus = HttpStatusCode.OK_200) {
52 return makeGetRequest({
53 url,
54 path,
55 expectedStatus: expectedStatus,
56 accept: 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8'
57 })
58}
59
60function makeDeleteRequest (options: CommonRequestParams & {
61 query?: any
62 rawQuery?: string
63}) {
64 const req = request(options.url).delete(options.path)
65
66 if (options.query) req.query(options.query)
67 if (options.rawQuery) req.query(options.rawQuery)
68
69 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
70}
71
72function makeUploadRequest (options: CommonRequestParams & {
73 method?: 'POST' | 'PUT'
74
75 fields: { [ fieldName: string ]: any }
76 attaches?: { [ attachName: string ]: any | any[] }
77}) {
78 let req = options.method === 'PUT'
79 ? request(options.url).put(options.path)
80 : request(options.url).post(options.path)
81
82 req = buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
83
84 buildFields(req, options.fields)
85
86 Object.keys(options.attaches || {}).forEach(attach => {
87 const value = options.attaches[attach]
88 if (!value) return
89
90 if (Array.isArray(value)) {
91 req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
92 } else {
93 req.attach(attach, buildAbsoluteFixturePath(value))
94 }
95 })
96
97 return req
98}
99
100function makePostBodyRequest (options: CommonRequestParams & {
101 fields?: { [ fieldName: string ]: any }
102}) {
103 const req = request(options.url).post(options.path)
104 .send(options.fields)
105
106 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
107}
108
109function makePutBodyRequest (options: {
110 url: string
111 path: string
112 token?: string
113 fields: { [ fieldName: string ]: any }
114 expectedStatus?: HttpStatusCode
115}) {
116 const req = request(options.url).put(options.path)
117 .send(options.fields)
118
119 return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
120}
121
122function decodeQueryString (path: string) {
123 return decode(path.split('?')[1])
124}
125
126function unwrapBody <T> (test: request.Test): Promise<T> {
127 return test.then(res => res.body)
128}
129
130function unwrapText (test: request.Test): Promise<string> {
131 return test.then(res => res.text)
132}
133
134function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> {
135 return test.then(res => {
136 if (res.body instanceof Buffer) {
137 return JSON.parse(new TextDecoder().decode(res.body))
138 }
139
140 return res.body
141 })
142}
143
144function unwrapTextOrDecode (test: request.Test): Promise<string> {
145 return test.then(res => res.text || new TextDecoder().decode(res.body))
146}
147
148// ---------------------------------------------------------------------------
149
150export {
151 makeHTMLRequest,
152 makeGetRequest,
153 decodeQueryString,
154 makeUploadRequest,
155 makePostBodyRequest,
156 makePutBodyRequest,
157 makeDeleteRequest,
158 makeRawRequest,
159 makeActivityPubGetRequest,
160 unwrapBody,
161 unwrapTextOrDecode,
162 unwrapBodyOrDecodeToJSON,
163 unwrapText
164}
165
166// ---------------------------------------------------------------------------
167
168function buildRequest (req: request.Test, options: CommonRequestParams) {
169 if (options.contentType) req.set('Accept', options.contentType)
170 if (options.token) req.set('Authorization', 'Bearer ' + options.token)
171 if (options.range) req.set('Range', options.range)
172 if (options.accept) req.set('Accept', options.accept)
173 if (options.host) req.set('Host', options.host)
174 if (options.redirects) req.redirects(options.redirects)
175 if (options.expectedStatus) req.expect(options.expectedStatus)
176 if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor)
177 if (options.type) req.type(options.type)
178
179 Object.keys(options.headers || {}).forEach(name => {
180 req.set(name, options.headers[name])
181 })
182
183 return req
184}
185
186function buildFields (req: request.Test, fields: { [ fieldName: string ]: any }, namespace?: string) {
187 if (!fields) return
188
189 let formKey: string
190
191 for (const key of Object.keys(fields)) {
192 if (namespace) formKey = `${namespace}[${key}]`
193 else formKey = key
194
195 if (fields[key] === undefined) continue
196
197 if (Array.isArray(fields[key]) && fields[key].length === 0) {
198 req.field(key, [])
199 continue
200 }
201
202 if (fields[key] !== null && typeof fields[key] === 'object') {
203 buildFields(req, fields[key], formKey)
204 } else {
205 req.field(formKey, fields[key])
206 }
207 }
208}
diff --git a/shared/extra-utils/search/index.ts b/shared/extra-utils/search/index.ts
deleted file mode 100644
index 48dbe8ae9..000000000
--- a/shared/extra-utils/search/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './search-command'
diff --git a/shared/extra-utils/search/search-command.ts b/shared/extra-utils/search/search-command.ts
deleted file mode 100644
index 0fbbcd6ef..000000000
--- a/shared/extra-utils/search/search-command.ts
+++ /dev/null
@@ -1,98 +0,0 @@
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/server/config-command.ts b/shared/extra-utils/server/config-command.ts
deleted file mode 100644
index 7a768b4df..000000000
--- a/shared/extra-utils/server/config-command.ts
+++ /dev/null
@@ -1,341 +0,0 @@
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 '144p': enabled,
12 '240p': enabled,
13 '360p': enabled,
14 '480p': enabled,
15 '720p': enabled,
16 '1080p': enabled,
17 '1440p': enabled,
18 '2160p': enabled
19 }
20 }
21
22 enableImports () {
23 return this.updateExistingSubConfig({
24 newConfig: {
25 import: {
26 videos: {
27 http: {
28 enabled: true
29 },
30
31 torrent: {
32 enabled: true
33 }
34 }
35 }
36 }
37 })
38 }
39
40 enableLive (options: {
41 allowReplay?: boolean
42 transcoding?: boolean
43 } = {}) {
44 return this.updateExistingSubConfig({
45 newConfig: {
46 live: {
47 enabled: true,
48 allowReplay: options.allowReplay ?? true,
49 transcoding: {
50 enabled: options.transcoding ?? true,
51 resolutions: ConfigCommand.getCustomConfigResolutions(true)
52 }
53 }
54 }
55 })
56 }
57
58 disableTranscoding () {
59 return this.updateExistingSubConfig({
60 newConfig: {
61 transcoding: {
62 enabled: false
63 }
64 }
65 })
66 }
67
68 enableTranscoding (webtorrent = true, hls = true) {
69 return this.updateExistingSubConfig({
70 newConfig: {
71 transcoding: {
72 enabled: true,
73 resolutions: ConfigCommand.getCustomConfigResolutions(true),
74
75 webtorrent: {
76 enabled: webtorrent
77 },
78 hls: {
79 enabled: hls
80 }
81 }
82 }
83 })
84 }
85
86 getConfig (options: OverrideCommandOptions = {}) {
87 const path = '/api/v1/config'
88
89 return this.getRequestBody<ServerConfig>({
90 ...options,
91
92 path,
93 implicitToken: false,
94 defaultExpectedStatus: HttpStatusCode.OK_200
95 })
96 }
97
98 getAbout (options: OverrideCommandOptions = {}) {
99 const path = '/api/v1/config/about'
100
101 return this.getRequestBody<About>({
102 ...options,
103
104 path,
105 implicitToken: false,
106 defaultExpectedStatus: HttpStatusCode.OK_200
107 })
108 }
109
110 getCustomConfig (options: OverrideCommandOptions = {}) {
111 const path = '/api/v1/config/custom'
112
113 return this.getRequestBody<CustomConfig>({
114 ...options,
115
116 path,
117 implicitToken: true,
118 defaultExpectedStatus: HttpStatusCode.OK_200
119 })
120 }
121
122 updateCustomConfig (options: OverrideCommandOptions & {
123 newCustomConfig: CustomConfig
124 }) {
125 const path = '/api/v1/config/custom'
126
127 return this.putBodyRequest({
128 ...options,
129
130 path,
131 fields: options.newCustomConfig,
132 implicitToken: true,
133 defaultExpectedStatus: HttpStatusCode.OK_200
134 })
135 }
136
137 deleteCustomConfig (options: OverrideCommandOptions = {}) {
138 const path = '/api/v1/config/custom'
139
140 return this.deleteRequest({
141 ...options,
142
143 path,
144 implicitToken: true,
145 defaultExpectedStatus: HttpStatusCode.OK_200
146 })
147 }
148
149 async updateExistingSubConfig (options: OverrideCommandOptions & {
150 newConfig: DeepPartial<CustomConfig>
151 }) {
152 const existing = await this.getCustomConfig(options)
153
154 return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) })
155 }
156
157 updateCustomSubConfig (options: OverrideCommandOptions & {
158 newConfig: DeepPartial<CustomConfig>
159 }) {
160 const newCustomConfig: CustomConfig = {
161 instance: {
162 name: 'PeerTube updated',
163 shortDescription: 'my short description',
164 description: 'my super description',
165 terms: 'my super terms',
166 codeOfConduct: 'my super coc',
167
168 creationReason: 'my super creation reason',
169 moderationInformation: 'my super moderation information',
170 administrator: 'Kuja',
171 maintenanceLifetime: 'forever',
172 businessModel: 'my super business model',
173 hardwareInformation: '2vCore 3GB RAM',
174
175 languages: [ 'en', 'es' ],
176 categories: [ 1, 2 ],
177
178 isNSFW: true,
179 defaultNSFWPolicy: 'blur',
180
181 defaultClientRoute: '/videos/recently-added',
182
183 customizations: {
184 javascript: 'alert("coucou")',
185 css: 'body { background-color: red; }'
186 }
187 },
188 theme: {
189 default: 'default'
190 },
191 services: {
192 twitter: {
193 username: '@MySuperUsername',
194 whitelisted: true
195 }
196 },
197 cache: {
198 previews: {
199 size: 2
200 },
201 captions: {
202 size: 3
203 },
204 torrents: {
205 size: 4
206 }
207 },
208 signup: {
209 enabled: false,
210 limit: 5,
211 requiresEmailVerification: false,
212 minimumAge: 16
213 },
214 admin: {
215 email: 'superadmin1@example.com'
216 },
217 contactForm: {
218 enabled: true
219 },
220 user: {
221 videoQuota: 5242881,
222 videoQuotaDaily: 318742
223 },
224 videoChannels: {
225 maxPerUser: 20
226 },
227 transcoding: {
228 enabled: true,
229 allowAdditionalExtensions: true,
230 allowAudioFiles: true,
231 threads: 1,
232 concurrency: 3,
233 profile: 'default',
234 resolutions: {
235 '0p': false,
236 '144p': false,
237 '240p': false,
238 '360p': true,
239 '480p': true,
240 '720p': false,
241 '1080p': false,
242 '1440p': false,
243 '2160p': false
244 },
245 webtorrent: {
246 enabled: true
247 },
248 hls: {
249 enabled: false
250 }
251 },
252 live: {
253 enabled: true,
254 allowReplay: false,
255 maxDuration: -1,
256 maxInstanceLives: -1,
257 maxUserLives: 50,
258 transcoding: {
259 enabled: true,
260 threads: 4,
261 profile: 'default',
262 resolutions: {
263 '144p': true,
264 '240p': true,
265 '360p': true,
266 '480p': true,
267 '720p': true,
268 '1080p': true,
269 '1440p': true,
270 '2160p': true
271 }
272 }
273 },
274 import: {
275 videos: {
276 concurrency: 3,
277 http: {
278 enabled: false
279 },
280 torrent: {
281 enabled: false
282 }
283 }
284 },
285 trending: {
286 videos: {
287 algorithms: {
288 enabled: [ 'best', 'hot', 'most-viewed', 'most-liked' ],
289 default: 'hot'
290 }
291 }
292 },
293 autoBlacklist: {
294 videos: {
295 ofUsers: {
296 enabled: false
297 }
298 }
299 },
300 followers: {
301 instance: {
302 enabled: true,
303 manualApproval: false
304 }
305 },
306 followings: {
307 instance: {
308 autoFollowBack: {
309 enabled: false
310 },
311 autoFollowIndex: {
312 indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
313 enabled: false
314 }
315 }
316 },
317 broadcastMessage: {
318 enabled: true,
319 level: 'warning',
320 message: 'hello',
321 dismissable: true
322 },
323 search: {
324 remoteUri: {
325 users: true,
326 anonymous: true
327 },
328 searchIndex: {
329 enabled: true,
330 url: 'https://search.joinpeertube.org',
331 disableLocalSearch: true,
332 isDefaultSearch: true
333 }
334 }
335 }
336
337 merge(newCustomConfig, options.newConfig)
338
339 return this.updateCustomConfig({ ...options, newCustomConfig })
340 }
341}
diff --git a/shared/extra-utils/server/contact-form-command.ts b/shared/extra-utils/server/contact-form-command.ts
deleted file mode 100644
index 0e8fd6d84..000000000
--- a/shared/extra-utils/server/contact-form-command.ts
+++ /dev/null
@@ -1,31 +0,0 @@
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/debug-command.ts b/shared/extra-utils/server/debug-command.ts
deleted file mode 100644
index 3c5a785bb..000000000
--- a/shared/extra-utils/server/debug-command.ts
+++ /dev/null
@@ -1,33 +0,0 @@
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/directories.ts b/shared/extra-utils/server/directories.ts
deleted file mode 100644
index b6465cbf4..000000000
--- a/shared/extra-utils/server/directories.ts
+++ /dev/null
@@ -1,34 +0,0 @@
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
deleted file mode 100644
index 01ef6f179..000000000
--- a/shared/extra-utils/server/follows-command.ts
+++ /dev/null
@@ -1,139 +0,0 @@
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
deleted file mode 100644
index 698238f29..000000000
--- a/shared/extra-utils/server/follows.ts
+++ /dev/null
@@ -1,20 +0,0 @@
1import { waitJobs } from './jobs'
2import { PeerTubeServer } from './server'
3
4async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
5 await Promise.all([
6 server1.follows.follow({ hosts: [ server2.url ] }),
7 server2.follows.follow({ hosts: [ server1.url ] })
8 ])
9
10 // Wait request propagation
11 await waitJobs([ server1, server2 ])
12
13 return true
14}
15
16// ---------------------------------------------------------------------------
17
18export {
19 doubleFollow
20}
diff --git a/shared/extra-utils/server/index.ts b/shared/extra-utils/server/index.ts
deleted file mode 100644
index 76a2099da..000000000
--- a/shared/extra-utils/server/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
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 './object-storage-command'
10export * from './plugins-command'
11export * from './plugins'
12export * from './redundancy-command'
13export * from './server'
14export * from './servers-command'
15export * from './servers'
16export * from './stats-command'
17export * from './tracker'
diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts
deleted file mode 100644
index 6636e7e4d..000000000
--- a/shared/extra-utils/server/jobs-command.ts
+++ /dev/null
@@ -1,61 +0,0 @@
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 async getLatest (options: OverrideCommandOptions & {
9 jobType: JobType
10 }) {
11 const { data } = await this.list({ ...options, start: 0, count: 1, sort: '-createdAt' })
12
13 if (data.length === 0) return undefined
14
15 return data[0]
16 }
17
18 list (options: OverrideCommandOptions & {
19 state?: JobState
20 jobType?: JobType
21 start?: number
22 count?: number
23 sort?: string
24 } = {}) {
25 const path = this.buildJobsUrl(options.state)
26
27 const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ])
28
29 return this.getRequestBody<ResultList<Job>>({
30 ...options,
31
32 path,
33 query,
34 implicitToken: true,
35 defaultExpectedStatus: HttpStatusCode.OK_200
36 })
37 }
38
39 listFailed (options: OverrideCommandOptions & {
40 jobType?: JobType
41 }) {
42 const path = this.buildJobsUrl('failed')
43
44 return this.getRequestBody<ResultList<Job>>({
45 ...options,
46
47 path,
48 query: { start: 0, count: 50 },
49 implicitToken: true,
50 defaultExpectedStatus: HttpStatusCode.OK_200
51 })
52 }
53
54 private buildJobsUrl (state?: JobState) {
55 let path = '/api/v1/jobs'
56
57 if (state) path += '/' + state
58
59 return path
60 }
61}
diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts
deleted file mode 100644
index 34fefd444..000000000
--- a/shared/extra-utils/server/jobs.ts
+++ /dev/null
@@ -1,84 +0,0 @@
1
2import { expect } from 'chai'
3import { JobState, JobType } from '../../models'
4import { wait } from '../miscs'
5import { PeerTubeServer } from './server'
6
7async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer, skipDelayed = false) {
8 const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
9 ? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
10 : 250
11
12 let servers: PeerTubeServer[]
13
14 if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ]
15 else servers = serversArg as PeerTubeServer[]
16
17 const states: JobState[] = [ 'waiting', 'active' ]
18 if (!skipDelayed) states.push('delayed')
19
20 const repeatableJobs: JobType[] = [ 'videos-views-stats', 'activitypub-cleaner' ]
21 let pendingRequests: boolean
22
23 function tasksBuilder () {
24 const tasks: Promise<any>[] = []
25
26 // Check if each server has pending request
27 for (const server of servers) {
28 for (const state of states) {
29 const p = server.jobs.list({
30 state,
31 start: 0,
32 count: 10,
33 sort: '-createdAt'
34 }).then(body => body.data)
35 .then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type)))
36 .then(jobs => {
37 if (jobs.length !== 0) {
38 pendingRequests = true
39 }
40 })
41
42 tasks.push(p)
43 }
44
45 const p = server.debug.getDebug()
46 .then(obj => {
47 if (obj.activityPubMessagesWaiting !== 0) {
48 pendingRequests = true
49 }
50 })
51
52 tasks.push(p)
53 }
54
55 return tasks
56 }
57
58 do {
59 pendingRequests = false
60 await Promise.all(tasksBuilder())
61
62 // Retry, in case of new jobs were created
63 if (pendingRequests === false) {
64 await wait(pendingJobWait)
65 await Promise.all(tasksBuilder())
66 }
67
68 if (pendingRequests) {
69 await wait(pendingJobWait)
70 }
71 } while (pendingRequests)
72}
73
74async function expectNoFailedTranscodingJob (server: PeerTubeServer) {
75 const { data } = await server.jobs.listFailed({ jobType: 'video-transcoding' })
76 expect(data).to.have.lengthOf(0)
77}
78
79// ---------------------------------------------------------------------------
80
81export {
82 waitJobs,
83 expectNoFailedTranscodingJob
84}
diff --git a/shared/extra-utils/server/object-storage-command.ts b/shared/extra-utils/server/object-storage-command.ts
deleted file mode 100644
index b4de8f4cb..000000000
--- a/shared/extra-utils/server/object-storage-command.ts
+++ /dev/null
@@ -1,77 +0,0 @@
1
2import { HttpStatusCode } from '@shared/models'
3import { makePostBodyRequest } from '../requests'
4import { AbstractCommand } from '../shared'
5
6export class ObjectStorageCommand extends AbstractCommand {
7 static readonly DEFAULT_PLAYLIST_BUCKET = 'streaming-playlists'
8 static readonly DEFAULT_WEBTORRENT_BUCKET = 'videos'
9
10 static getDefaultConfig () {
11 return {
12 object_storage: {
13 enabled: true,
14 endpoint: 'http://' + this.getEndpointHost(),
15 region: this.getRegion(),
16
17 credentials: this.getCredentialsConfig(),
18
19 streaming_playlists: {
20 bucket_name: this.DEFAULT_PLAYLIST_BUCKET
21 },
22
23 videos: {
24 bucket_name: this.DEFAULT_WEBTORRENT_BUCKET
25 }
26 }
27 }
28 }
29
30 static getCredentialsConfig () {
31 return {
32 access_key_id: 'AKIAIOSFODNN7EXAMPLE',
33 secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
34 }
35 }
36
37 static getEndpointHost () {
38 return 'localhost:9444'
39 }
40
41 static getRegion () {
42 return 'us-east-1'
43 }
44
45 static getWebTorrentBaseUrl () {
46 return `http://${this.DEFAULT_WEBTORRENT_BUCKET}.${this.getEndpointHost()}/`
47 }
48
49 static getPlaylistBaseUrl () {
50 return `http://${this.DEFAULT_PLAYLIST_BUCKET}.${this.getEndpointHost()}/`
51 }
52
53 static async prepareDefaultBuckets () {
54 await this.createBucket(this.DEFAULT_PLAYLIST_BUCKET)
55 await this.createBucket(this.DEFAULT_WEBTORRENT_BUCKET)
56 }
57
58 static async createBucket (name: string) {
59 await makePostBodyRequest({
60 url: this.getEndpointHost(),
61 path: '/ui/' + name + '?delete',
62 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
63 })
64
65 await makePostBodyRequest({
66 url: this.getEndpointHost(),
67 path: '/ui/' + name + '?create',
68 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
69 })
70
71 await makePostBodyRequest({
72 url: this.getEndpointHost(),
73 path: '/ui/' + name + '?make-public',
74 expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
75 })
76 }
77}
diff --git a/shared/extra-utils/server/plugins-command.ts b/shared/extra-utils/server/plugins-command.ts
deleted file mode 100644
index b944475a2..000000000
--- a/shared/extra-utils/server/plugins-command.ts
+++ /dev/null
@@ -1,256 +0,0 @@
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
deleted file mode 100644
index 0f5fabd5a..000000000
--- a/shared/extra-utils/server/plugins.ts
+++ /dev/null
@@ -1,18 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { PeerTubeServer } from '../server/server'
5
6async function testHelloWorldRegisteredSettings (server: PeerTubeServer) {
7 const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' })
8
9 const registeredSettings = body.registeredSettings
10 expect(registeredSettings).to.have.length.at.least(1)
11
12 const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name')
13 expect(adminNameSettings).to.not.be.undefined
14}
15
16export {
17 testHelloWorldRegisteredSettings
18}
diff --git a/shared/extra-utils/server/redundancy-command.ts b/shared/extra-utils/server/redundancy-command.ts
deleted file mode 100644
index e7a8b3c29..000000000
--- a/shared/extra-utils/server/redundancy-command.ts
+++ /dev/null
@@ -1,80 +0,0 @@
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/server.ts b/shared/extra-utils/server/server.ts
deleted file mode 100644
index 31224ebe9..000000000
--- a/shared/extra-utils/server/server.ts
+++ /dev/null
@@ -1,389 +0,0 @@
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'
41import { ObjectStorageCommand } from './object-storage-command'
42
43export type RunServerOptions = {
44 hideLogs?: boolean
45 nodeArgs?: string[]
46 peertubeArgs?: string[]
47 env?: { [ id: string ]: string }
48}
49
50export class PeerTubeServer {
51 app?: ChildProcess
52
53 url: string
54 host?: string
55 hostname?: string
56 port?: number
57
58 rtmpPort?: number
59 rtmpsPort?: number
60
61 parallel?: boolean
62 internalServerNumber: number
63
64 serverNumber?: number
65 customConfigFile?: string
66
67 store?: {
68 client?: {
69 id?: string
70 secret?: string
71 }
72
73 user?: {
74 username: string
75 password: string
76 email?: string
77 }
78
79 channel?: VideoChannel
80
81 video?: Video
82 videoCreated?: VideoCreateResult
83 videoDetails?: VideoDetails
84
85 videos?: { id: number, uuid: string }[]
86 }
87
88 accessToken?: string
89 refreshToken?: string
90
91 bulk?: BulkCommand
92 cli?: CLICommand
93 customPage?: CustomPagesCommand
94 feed?: FeedCommand
95 logs?: LogsCommand
96 abuses?: AbusesCommand
97 overviews?: OverviewsCommand
98 search?: SearchCommand
99 contactForm?: ContactFormCommand
100 debug?: DebugCommand
101 follows?: FollowsCommand
102 jobs?: JobsCommand
103 plugins?: PluginsCommand
104 redundancy?: RedundancyCommand
105 stats?: StatsCommand
106 config?: ConfigCommand
107 socketIO?: SocketIOCommand
108 accounts?: AccountsCommand
109 blocklist?: BlocklistCommand
110 subscriptions?: SubscriptionsCommand
111 live?: LiveCommand
112 services?: ServicesCommand
113 blacklist?: BlacklistCommand
114 captions?: CaptionsCommand
115 changeOwnership?: ChangeOwnershipCommand
116 playlists?: PlaylistsCommand
117 history?: HistoryCommand
118 imports?: ImportsCommand
119 streamingPlaylists?: StreamingPlaylistsCommand
120 channels?: ChannelsCommand
121 comments?: CommentsCommand
122 sql?: SQLCommand
123 notifications?: NotificationsCommand
124 servers?: ServersCommand
125 login?: LoginCommand
126 users?: UsersCommand
127 objectStorage?: ObjectStorageCommand
128 videos?: VideosCommand
129
130 constructor (options: { serverNumber: number } | { url: string }) {
131 if ((options as any).url) {
132 this.setUrl((options as any).url)
133 } else {
134 this.setServerNumber((options as any).serverNumber)
135 }
136
137 this.store = {
138 client: {
139 id: null,
140 secret: null
141 },
142 user: {
143 username: null,
144 password: null
145 }
146 }
147
148 this.assignCommands()
149 }
150
151 setServerNumber (serverNumber: number) {
152 this.serverNumber = serverNumber
153
154 this.parallel = parallelTests()
155
156 this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
157 this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
158 this.rtmpsPort = this.parallel ? this.randomRTMP() : 1937
159 this.port = 9000 + this.internalServerNumber
160
161 this.url = `http://localhost:${this.port}`
162 this.host = `localhost:${this.port}`
163 this.hostname = 'localhost'
164 }
165
166 setUrl (url: string) {
167 const parsed = new URL(url)
168
169 this.url = url
170 this.host = parsed.host
171 this.hostname = parsed.hostname
172 this.port = parseInt(parsed.port)
173 }
174
175 async flushAndRun (configOverride?: Object, options: RunServerOptions = {}) {
176 await ServersCommand.flushTests(this.internalServerNumber)
177
178 return this.run(configOverride, options)
179 }
180
181 async run (configOverrideArg?: any, options: RunServerOptions = {}) {
182 // These actions are async so we need to be sure that they have both been done
183 const serverRunString = {
184 'HTTP server listening': false
185 }
186 const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
187 serverRunString[key] = false
188
189 const regexps = {
190 client_id: 'Client id: (.+)',
191 client_secret: 'Client secret: (.+)',
192 user_username: 'Username: (.+)',
193 user_password: 'User password: (.+)'
194 }
195
196 await this.assignCustomConfigFile()
197
198 const configOverride = this.buildConfigOverride()
199
200 if (configOverrideArg !== undefined) {
201 Object.assign(configOverride, configOverrideArg)
202 }
203
204 // Share the environment
205 const env = Object.create(process.env)
206 env['NODE_ENV'] = 'test'
207 env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
208 env['NODE_CONFIG'] = JSON.stringify(configOverride)
209
210 if (options.env) {
211 Object.assign(env, options.env)
212 }
213
214 const forkOptions = {
215 silent: true,
216 env,
217 detached: true,
218 execArgv: options.nodeArgs || []
219 }
220
221 return new Promise<void>((res, rej) => {
222 const self = this
223
224 this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions)
225
226 const onPeerTubeExit = () => rej(new Error('Process exited'))
227 const onParentExit = () => {
228 if (!this.app || !this.app.pid) return
229
230 try {
231 process.kill(self.app.pid)
232 } catch { /* empty */ }
233 }
234
235 this.app.on('exit', onPeerTubeExit)
236 process.on('exit', onParentExit)
237
238 this.app.stdout.on('data', function onStdout (data) {
239 let dontContinue = false
240
241 // Capture things if we want to
242 for (const key of Object.keys(regexps)) {
243 const regexp = regexps[key]
244 const matches = data.toString().match(regexp)
245 if (matches !== null) {
246 if (key === 'client_id') self.store.client.id = matches[1]
247 else if (key === 'client_secret') self.store.client.secret = matches[1]
248 else if (key === 'user_username') self.store.user.username = matches[1]
249 else if (key === 'user_password') self.store.user.password = matches[1]
250 }
251 }
252
253 // Check if all required sentences are here
254 for (const key of Object.keys(serverRunString)) {
255 if (data.toString().indexOf(key) !== -1) serverRunString[key] = true
256 if (serverRunString[key] === false) dontContinue = true
257 }
258
259 // If no, there is maybe one thing not already initialized (client/user credentials generation...)
260 if (dontContinue === true) return
261
262 if (options.hideLogs === false) {
263 console.log(data.toString())
264 } else {
265 process.removeListener('exit', onParentExit)
266 self.app.stdout.removeListener('data', onStdout)
267 self.app.removeListener('exit', onPeerTubeExit)
268 }
269
270 res()
271 })
272 })
273 }
274
275 async kill () {
276 if (!this.app) return
277
278 await this.sql.cleanup()
279
280 process.kill(-this.app.pid)
281
282 this.app = null
283 }
284
285 private randomServer () {
286 const low = 10
287 const high = 10000
288
289 return randomInt(low, high)
290 }
291
292 private randomRTMP () {
293 const low = 1900
294 const high = 2100
295
296 return randomInt(low, high)
297 }
298
299 private async assignCustomConfigFile () {
300 if (this.internalServerNumber === this.serverNumber) return
301
302 const basePath = join(root(), 'config')
303
304 const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
305 await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
306
307 this.customConfigFile = tmpConfigFile
308 }
309
310 private buildConfigOverride () {
311 if (!this.parallel) return {}
312
313 return {
314 listen: {
315 port: this.port
316 },
317 webserver: {
318 port: this.port
319 },
320 database: {
321 suffix: '_test' + this.internalServerNumber
322 },
323 storage: {
324 tmp: `test${this.internalServerNumber}/tmp/`,
325 bin: `test${this.internalServerNumber}/bin/`,
326 avatars: `test${this.internalServerNumber}/avatars/`,
327 videos: `test${this.internalServerNumber}/videos/`,
328 streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`,
329 redundancy: `test${this.internalServerNumber}/redundancy/`,
330 logs: `test${this.internalServerNumber}/logs/`,
331 previews: `test${this.internalServerNumber}/previews/`,
332 thumbnails: `test${this.internalServerNumber}/thumbnails/`,
333 torrents: `test${this.internalServerNumber}/torrents/`,
334 captions: `test${this.internalServerNumber}/captions/`,
335 cache: `test${this.internalServerNumber}/cache/`,
336 plugins: `test${this.internalServerNumber}/plugins/`
337 },
338 admin: {
339 email: `admin${this.internalServerNumber}@example.com`
340 },
341 live: {
342 rtmp: {
343 port: this.rtmpPort
344 }
345 }
346 }
347 }
348
349 private assignCommands () {
350 this.bulk = new BulkCommand(this)
351 this.cli = new CLICommand(this)
352 this.customPage = new CustomPagesCommand(this)
353 this.feed = new FeedCommand(this)
354 this.logs = new LogsCommand(this)
355 this.abuses = new AbusesCommand(this)
356 this.overviews = new OverviewsCommand(this)
357 this.search = new SearchCommand(this)
358 this.contactForm = new ContactFormCommand(this)
359 this.debug = new DebugCommand(this)
360 this.follows = new FollowsCommand(this)
361 this.jobs = new JobsCommand(this)
362 this.plugins = new PluginsCommand(this)
363 this.redundancy = new RedundancyCommand(this)
364 this.stats = new StatsCommand(this)
365 this.config = new ConfigCommand(this)
366 this.socketIO = new SocketIOCommand(this)
367 this.accounts = new AccountsCommand(this)
368 this.blocklist = new BlocklistCommand(this)
369 this.subscriptions = new SubscriptionsCommand(this)
370 this.live = new LiveCommand(this)
371 this.services = new ServicesCommand(this)
372 this.blacklist = new BlacklistCommand(this)
373 this.captions = new CaptionsCommand(this)
374 this.changeOwnership = new ChangeOwnershipCommand(this)
375 this.playlists = new PlaylistsCommand(this)
376 this.history = new HistoryCommand(this)
377 this.imports = new ImportsCommand(this)
378 this.streamingPlaylists = new StreamingPlaylistsCommand(this)
379 this.channels = new ChannelsCommand(this)
380 this.comments = new CommentsCommand(this)
381 this.sql = new SQLCommand(this)
382 this.notifications = new NotificationsCommand(this)
383 this.servers = new ServersCommand(this)
384 this.login = new LoginCommand(this)
385 this.users = new UsersCommand(this)
386 this.videos = new VideosCommand(this)
387 this.objectStorage = new ObjectStorageCommand(this)
388 }
389}
diff --git a/shared/extra-utils/server/servers-command.ts b/shared/extra-utils/server/servers-command.ts
deleted file mode 100644
index 776d2123c..000000000
--- a/shared/extra-utils/server/servers-command.ts
+++ /dev/null
@@ -1,92 +0,0 @@
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.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 getLogContent () {
84 return readFile(this.buildDirectory('logs/peertube.log'))
85 }
86
87 async getServerFileSize (subPath: string) {
88 const path = this.server.servers.buildDirectory(subPath)
89
90 return getFileSize(path)
91 }
92}
diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts
deleted file mode 100644
index 21ab9405b..000000000
--- a/shared/extra-utils/server/servers.ts
+++ /dev/null
@@ -1,49 +0,0 @@
1import { ensureDir } from 'fs-extra'
2import { isGithubCI } from '../miscs'
3import { PeerTubeServer, RunServerOptions } from './server'
4
5async function createSingleServer (serverNumber: number, configOverride?: Object, options: RunServerOptions = {}) {
6 const server = new PeerTubeServer({ serverNumber })
7
8 await server.flushAndRun(configOverride, options)
9
10 return server
11}
12
13function createMultipleServers (totalServers: number, configOverride?: Object, options: RunServerOptions = {}) {
14 const serverPromises: Promise<PeerTubeServer>[] = []
15
16 for (let i = 1; i <= totalServers; i++) {
17 serverPromises.push(createSingleServer(i, configOverride, options))
18 }
19
20 return Promise.all(serverPromises)
21}
22
23async function killallServers (servers: PeerTubeServer[]) {
24 return Promise.all(servers.map(s => s.kill()))
25}
26
27async function cleanupTests (servers: PeerTubeServer[]) {
28 await killallServers(servers)
29
30 if (isGithubCI()) {
31 await ensureDir('artifacts')
32 }
33
34 let p: Promise<any>[] = []
35 for (const server of servers) {
36 p = p.concat(server.servers.cleanupTests())
37 }
38
39 return Promise.all(p)
40}
41
42// ---------------------------------------------------------------------------
43
44export {
45 createSingleServer,
46 createMultipleServers,
47 cleanupTests,
48 killallServers
49}
diff --git a/shared/extra-utils/server/stats-command.ts b/shared/extra-utils/server/stats-command.ts
deleted file mode 100644
index 64a452306..000000000
--- a/shared/extra-utils/server/stats-command.ts
+++ /dev/null
@@ -1,25 +0,0 @@
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/tracker.ts b/shared/extra-utils/server/tracker.ts
deleted file mode 100644
index f04e8f8a1..000000000
--- a/shared/extra-utils/server/tracker.ts
+++ /dev/null
@@ -1,27 +0,0 @@
1import { expect } from 'chai'
2import { sha1 } from '@server/helpers/core-utils'
3import { makeGetRequest } from '../requests'
4
5async function hlsInfohashExist (serverUrl: string, masterPlaylistUrl: string, fileNumber: number) {
6 const path = '/tracker/announce'
7
8 const infohash = sha1(`2${masterPlaylistUrl}+V${fileNumber}`)
9
10 // From bittorrent-tracker
11 const infohashBinary = escape(Buffer.from(infohash, 'hex').toString('binary')).replace(/[@*/+]/g, function (char) {
12 return '%' + char.charCodeAt(0).toString(16).toUpperCase()
13 })
14
15 const res = await makeGetRequest({
16 url: serverUrl,
17 path,
18 rawQuery: `peer_id=-WW0105-NkvYO/egUAr4&info_hash=${infohashBinary}&port=42100`,
19 expectedStatus: 200
20 })
21
22 expect(res.text).to.not.contain('failure')
23}
24
25export {
26 hlsInfohashExist
27}
diff --git a/shared/extra-utils/shared/abstract-command.ts b/shared/extra-utils/shared/abstract-command.ts
deleted file mode 100644
index a57c857fc..000000000
--- a/shared/extra-utils/shared/abstract-command.ts
+++ /dev/null
@@ -1,211 +0,0 @@
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
43interface InternalDeleteCommandOptions extends InternalCommonCommandOptions {
44 query?: { [ id: string ]: any }
45 rawQuery?: string
46}
47
48abstract class AbstractCommand {
49
50 constructor (
51 protected server: PeerTubeServer
52 ) {
53
54 }
55
56 protected getRequestBody <T> (options: InternalGetCommandOptions) {
57 return unwrapBody<T>(this.getRequest(options))
58 }
59
60 protected getRequestText (options: InternalGetCommandOptions) {
61 return unwrapText(this.getRequest(options))
62 }
63
64 protected getRawRequest (options: Omit<InternalGetCommandOptions, 'path'>) {
65 const { url, range } = options
66 const { host, protocol, pathname } = new URL(url)
67
68 return this.getRequest({
69 ...options,
70
71 token: this.buildCommonRequestToken(options),
72 defaultExpectedStatus: this.buildExpectedStatus(options),
73
74 url: `${protocol}//${host}`,
75 path: pathname,
76 range
77 })
78 }
79
80 protected getRequest (options: InternalGetCommandOptions) {
81 const { query } = options
82
83 return makeGetRequest({
84 ...this.buildCommonRequestOptions(options),
85
86 query
87 })
88 }
89
90 protected deleteRequest (options: InternalDeleteCommandOptions) {
91 const { query, rawQuery } = options
92
93 return makeDeleteRequest({
94 ...this.buildCommonRequestOptions(options),
95
96 query,
97 rawQuery
98 })
99 }
100
101 protected putBodyRequest (options: InternalCommonCommandOptions & {
102 fields?: { [ fieldName: string ]: any }
103 }) {
104 const { fields } = options
105
106 return makePutBodyRequest({
107 ...this.buildCommonRequestOptions(options),
108
109 fields
110 })
111 }
112
113 protected postBodyRequest (options: InternalCommonCommandOptions & {
114 fields?: { [ fieldName: string ]: any }
115 }) {
116 const { fields } = options
117
118 return makePostBodyRequest({
119 ...this.buildCommonRequestOptions(options),
120
121 fields
122 })
123 }
124
125 protected postUploadRequest (options: InternalCommonCommandOptions & {
126 fields?: { [ fieldName: string ]: any }
127 attaches?: { [ fieldName: string ]: any }
128 }) {
129 const { fields, attaches } = options
130
131 return makeUploadRequest({
132 ...this.buildCommonRequestOptions(options),
133
134 method: 'POST',
135 fields,
136 attaches
137 })
138 }
139
140 protected putUploadRequest (options: InternalCommonCommandOptions & {
141 fields?: { [ fieldName: string ]: any }
142 attaches?: { [ fieldName: string ]: any }
143 }) {
144 const { fields, attaches } = options
145
146 return makeUploadRequest({
147 ...this.buildCommonRequestOptions(options),
148
149 method: 'PUT',
150 fields,
151 attaches
152 })
153 }
154
155 protected updateImageRequest (options: InternalCommonCommandOptions & {
156 fixture: string
157 fieldname: string
158 }) {
159 const filePath = isAbsolute(options.fixture)
160 ? options.fixture
161 : join(root(), 'server', 'tests', 'fixtures', options.fixture)
162
163 return this.postUploadRequest({
164 ...options,
165
166 fields: {},
167 attaches: { [options.fieldname]: filePath }
168 })
169 }
170
171 protected buildCommonRequestOptions (options: InternalCommonCommandOptions) {
172 const { url, path, redirects, contentType, accept, range, host, headers, requestType, xForwardedFor } = options
173
174 return {
175 url: url ?? this.server.url,
176 path,
177
178 token: this.buildCommonRequestToken(options),
179 expectedStatus: this.buildExpectedStatus(options),
180
181 redirects,
182 contentType,
183 range,
184 host,
185 accept,
186 headers,
187 type: requestType,
188 xForwardedFor
189 }
190 }
191
192 protected buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
193 const { token } = options
194
195 const fallbackToken = options.implicitToken
196 ? this.server.accessToken
197 : undefined
198
199 return token !== undefined ? token : fallbackToken
200 }
201
202 protected buildExpectedStatus (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
203 const { expectedStatus, defaultExpectedStatus } = options
204
205 return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus
206 }
207}
208
209export {
210 AbstractCommand
211}
diff --git a/shared/extra-utils/shared/index.ts b/shared/extra-utils/shared/index.ts
deleted file mode 100644
index e807ab4f7..000000000
--- a/shared/extra-utils/shared/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './abstract-command'
diff --git a/shared/extra-utils/socket/index.ts b/shared/extra-utils/socket/index.ts
deleted file mode 100644
index 594329b2f..000000000
--- a/shared/extra-utils/socket/index.ts
+++ /dev/null
@@ -1 +0,0 @@
1export * from './socket-io-command'
diff --git a/shared/extra-utils/socket/socket-io-command.ts b/shared/extra-utils/socket/socket-io-command.ts
deleted file mode 100644
index c277ead28..000000000
--- a/shared/extra-utils/socket/socket-io-command.ts
+++ /dev/null
@@ -1,15 +0,0 @@
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/users/accounts-command.ts b/shared/extra-utils/users/accounts-command.ts
deleted file mode 100644
index 98d9d5927..000000000
--- a/shared/extra-utils/users/accounts-command.ts
+++ /dev/null
@@ -1,78 +0,0 @@
1import { HttpStatusCode, ResultList } from '@shared/models'
2import { Account, ActorFollow } 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
57 listFollowers (options: OverrideCommandOptions & {
58 accountName: string
59 start?: number
60 count?: number
61 sort?: string
62 search?: string
63 }) {
64 const { accountName, start, count, sort, search } = options
65 const path = '/api/v1/accounts/' + accountName + '/followers'
66
67 const query = { start, count, sort, search }
68
69 return this.getRequestBody<ResultList<ActorFollow>>({
70 ...options,
71
72 path,
73 query,
74 implicitToken: true,
75 defaultExpectedStatus: HttpStatusCode.OK_200
76 })
77 }
78}
diff --git a/shared/extra-utils/users/actors.ts b/shared/extra-utils/users/actors.ts
deleted file mode 100644
index cfcc7d0a7..000000000
--- a/shared/extra-utils/users/actors.ts
+++ /dev/null
@@ -1,73 +0,0 @@
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
deleted file mode 100644
index 14491a1ae..000000000
--- a/shared/extra-utils/users/blocklist-command.ts
+++ /dev/null
@@ -1,139 +0,0 @@
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/index.ts b/shared/extra-utils/users/index.ts
deleted file mode 100644
index 460a06f70..000000000
--- a/shared/extra-utils/users/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
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
deleted file mode 100644
index 143f72a59..000000000
--- a/shared/extra-utils/users/login-command.ts
+++ /dev/null
@@ -1,132 +0,0 @@
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
deleted file mode 100644
index f1df027d3..000000000
--- a/shared/extra-utils/users/login.ts
+++ /dev/null
@@ -1,19 +0,0 @@
1import { PeerTubeServer } from '../server/server'
2
3function setAccessTokensToServers (servers: PeerTubeServer[]) {
4 const tasks: Promise<any>[] = []
5
6 for (const server of servers) {
7 const p = server.login.getAccessToken()
8 .then(t => { server.accessToken = t })
9 tasks.push(p)
10 }
11
12 return Promise.all(tasks)
13}
14
15// ---------------------------------------------------------------------------
16
17export {
18 setAccessTokensToServers
19}
diff --git a/shared/extra-utils/users/notifications-command.ts b/shared/extra-utils/users/notifications-command.ts
deleted file mode 100644
index 692420b8b..000000000
--- a/shared/extra-utils/users/notifications-command.ts
+++ /dev/null
@@ -1,86 +0,0 @@
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 getLatest (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/notifications.ts b/shared/extra-utils/users/notifications.ts
deleted file mode 100644
index 07ccb0f8d..000000000
--- a/shared/extra-utils/users/notifications.ts
+++ /dev/null
@@ -1,795 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { inspect } from 'util'
5import { AbuseState, PluginType } from '@shared/models'
6import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users'
7import { MockSmtpServer } from '../mock-servers/mock-email'
8import { PeerTubeServer } from '../server'
9import { doubleFollow } from '../server/follows'
10import { createMultipleServers } from '../server/servers'
11import { setAccessTokensToServers } from './login'
12
13type CheckerBaseParams = {
14 server: PeerTubeServer
15 emails: any[]
16 socketNotifications: UserNotification[]
17 token: string
18 check?: { web: boolean, mail: boolean }
19}
20
21type CheckerType = 'presence' | 'absence'
22
23function getAllNotificationsSettings (): UserNotificationSetting {
24 return {
25 newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
26 newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
27 abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
28 videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
29 blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
30 myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
31 myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
32 commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
33 newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
34 newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
35 newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
36 abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
37 abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
38 autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
39 newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL,
40 newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL
41 }
42}
43
44async function checkNewVideoFromSubscription (options: CheckerBaseParams & {
45 videoName: string
46 shortUUID: string
47 checkType: CheckerType
48}) {
49 const { videoName, shortUUID } = options
50 const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION
51
52 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
53 if (checkType === 'presence') {
54 expect(notification).to.not.be.undefined
55 expect(notification.type).to.equal(notificationType)
56
57 checkVideo(notification.video, videoName, shortUUID)
58 checkActor(notification.video.channel)
59 } else {
60 expect(notification).to.satisfy((n: UserNotification) => {
61 return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName
62 })
63 }
64 }
65
66 function emailNotificationFinder (email: object) {
67 const text = email['text']
68 return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1
69 }
70
71 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
72}
73
74async function checkVideoIsPublished (options: CheckerBaseParams & {
75 videoName: string
76 shortUUID: string
77 checkType: CheckerType
78}) {
79 const { videoName, shortUUID } = options
80 const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED
81
82 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
83 if (checkType === 'presence') {
84 expect(notification).to.not.be.undefined
85 expect(notification.type).to.equal(notificationType)
86
87 checkVideo(notification.video, videoName, shortUUID)
88 checkActor(notification.video.channel)
89 } else {
90 expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName)
91 }
92 }
93
94 function emailNotificationFinder (email: object) {
95 const text: string = email['text']
96 return text.includes(shortUUID) && text.includes('Your video')
97 }
98
99 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
100}
101
102async function checkMyVideoImportIsFinished (options: CheckerBaseParams & {
103 videoName: string
104 shortUUID: string
105 url: string
106 success: boolean
107 checkType: CheckerType
108}) {
109 const { videoName, shortUUID, url, success } = options
110
111 const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR
112
113 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
114 if (checkType === 'presence') {
115 expect(notification).to.not.be.undefined
116 expect(notification.type).to.equal(notificationType)
117
118 expect(notification.videoImport.targetUrl).to.equal(url)
119
120 if (success) checkVideo(notification.videoImport.video, videoName, shortUUID)
121 } else {
122 expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url)
123 }
124 }
125
126 function emailNotificationFinder (email: object) {
127 const text: string = email['text']
128 const toFind = success ? ' finished' : ' error'
129
130 return text.includes(url) && text.includes(toFind)
131 }
132
133 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
134}
135
136async function checkUserRegistered (options: CheckerBaseParams & {
137 username: string
138 checkType: CheckerType
139}) {
140 const { username } = options
141 const notificationType = UserNotificationType.NEW_USER_REGISTRATION
142
143 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
144 if (checkType === 'presence') {
145 expect(notification).to.not.be.undefined
146 expect(notification.type).to.equal(notificationType)
147
148 checkActor(notification.account)
149 expect(notification.account.name).to.equal(username)
150 } else {
151 expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username)
152 }
153 }
154
155 function emailNotificationFinder (email: object) {
156 const text: string = email['text']
157
158 return text.includes(' registered.') && text.includes(username)
159 }
160
161 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
162}
163
164async function checkNewActorFollow (options: CheckerBaseParams & {
165 followType: 'channel' | 'account'
166 followerName: string
167 followerDisplayName: string
168 followingDisplayName: string
169 checkType: CheckerType
170}) {
171 const { followType, followerName, followerDisplayName, followingDisplayName } = options
172 const notificationType = UserNotificationType.NEW_FOLLOW
173
174 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
175 if (checkType === 'presence') {
176 expect(notification).to.not.be.undefined
177 expect(notification.type).to.equal(notificationType)
178
179 checkActor(notification.actorFollow.follower)
180 expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName)
181 expect(notification.actorFollow.follower.name).to.equal(followerName)
182 expect(notification.actorFollow.follower.host).to.not.be.undefined
183
184 const following = notification.actorFollow.following
185 expect(following.displayName).to.equal(followingDisplayName)
186 expect(following.type).to.equal(followType)
187 } else {
188 expect(notification).to.satisfy(n => {
189 return n.type !== notificationType ||
190 (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName)
191 })
192 }
193 }
194
195 function emailNotificationFinder (email: object) {
196 const text: string = email['text']
197
198 return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName)
199 }
200
201 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
202}
203
204async function checkNewInstanceFollower (options: CheckerBaseParams & {
205 followerHost: string
206 checkType: CheckerType
207}) {
208 const { followerHost } = options
209 const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER
210
211 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
212 if (checkType === 'presence') {
213 expect(notification).to.not.be.undefined
214 expect(notification.type).to.equal(notificationType)
215
216 checkActor(notification.actorFollow.follower)
217 expect(notification.actorFollow.follower.name).to.equal('peertube')
218 expect(notification.actorFollow.follower.host).to.equal(followerHost)
219
220 expect(notification.actorFollow.following.name).to.equal('peertube')
221 } else {
222 expect(notification).to.satisfy(n => {
223 return n.type !== notificationType || n.actorFollow.follower.host !== followerHost
224 })
225 }
226 }
227
228 function emailNotificationFinder (email: object) {
229 const text: string = email['text']
230
231 return text.includes('instance has a new follower') && text.includes(followerHost)
232 }
233
234 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
235}
236
237async function checkAutoInstanceFollowing (options: CheckerBaseParams & {
238 followerHost: string
239 followingHost: string
240 checkType: CheckerType
241}) {
242 const { followerHost, followingHost } = options
243 const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING
244
245 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
246 if (checkType === 'presence') {
247 expect(notification).to.not.be.undefined
248 expect(notification.type).to.equal(notificationType)
249
250 const following = notification.actorFollow.following
251 checkActor(following)
252 expect(following.name).to.equal('peertube')
253 expect(following.host).to.equal(followingHost)
254
255 expect(notification.actorFollow.follower.name).to.equal('peertube')
256 expect(notification.actorFollow.follower.host).to.equal(followerHost)
257 } else {
258 expect(notification).to.satisfy(n => {
259 return n.type !== notificationType || n.actorFollow.following.host !== followingHost
260 })
261 }
262 }
263
264 function emailNotificationFinder (email: object) {
265 const text: string = email['text']
266
267 return text.includes(' automatically followed a new instance') && text.includes(followingHost)
268 }
269
270 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
271}
272
273async function checkCommentMention (options: CheckerBaseParams & {
274 shortUUID: string
275 commentId: number
276 threadId: number
277 byAccountDisplayName: string
278 checkType: CheckerType
279}) {
280 const { shortUUID, commentId, threadId, byAccountDisplayName } = options
281 const notificationType = UserNotificationType.COMMENT_MENTION
282
283 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
284 if (checkType === 'presence') {
285 expect(notification).to.not.be.undefined
286 expect(notification.type).to.equal(notificationType)
287
288 checkComment(notification.comment, commentId, threadId)
289 checkActor(notification.comment.account)
290 expect(notification.comment.account.displayName).to.equal(byAccountDisplayName)
291
292 checkVideo(notification.comment.video, undefined, shortUUID)
293 } else {
294 expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId)
295 }
296 }
297
298 function emailNotificationFinder (email: object) {
299 const text: string = email['text']
300
301 return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName)
302 }
303
304 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
305}
306
307let lastEmailCount = 0
308
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
316 const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO
317
318 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
319 if (checkType === 'presence') {
320 expect(notification).to.not.be.undefined
321 expect(notification.type).to.equal(notificationType)
322
323 checkComment(notification.comment, commentId, threadId)
324 checkActor(notification.comment.account)
325 checkVideo(notification.comment.video, undefined, shortUUID)
326 } else {
327 expect(notification).to.satisfy((n: UserNotification) => {
328 return n === undefined || n.comment === undefined || n.comment.id !== commentId
329 })
330 }
331 }
332
333 const commentUrl = `http://localhost:${server.port}/w/${shortUUID};threadId=${threadId}`
334
335 function emailNotificationFinder (email: object) {
336 return email['text'].indexOf(commentUrl) !== -1
337 }
338
339 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
340
341 if (checkType === 'presence') {
342 // We cannot detect email duplicates, so check we received another email
343 expect(emails).to.have.length.above(lastEmailCount)
344 lastEmailCount = emails.length
345 }
346}
347
348async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & {
349 shortUUID: string
350 videoName: string
351 checkType: CheckerType
352}) {
353 const { shortUUID, videoName } = options
354 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
355
356 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
357 if (checkType === 'presence') {
358 expect(notification).to.not.be.undefined
359 expect(notification.type).to.equal(notificationType)
360
361 expect(notification.abuse.id).to.be.a('number')
362 checkVideo(notification.abuse.video, videoName, shortUUID)
363 } else {
364 expect(notification).to.satisfy((n: UserNotification) => {
365 return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID
366 })
367 }
368 }
369
370 function emailNotificationFinder (email: object) {
371 const text = email['text']
372 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
373 }
374
375 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
376}
377
378async function checkNewAbuseMessage (options: CheckerBaseParams & {
379 abuseId: number
380 message: string
381 toEmail: string
382 checkType: CheckerType
383}) {
384 const { abuseId, message, toEmail } = options
385 const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE
386
387 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
388 if (checkType === 'presence') {
389 expect(notification).to.not.be.undefined
390 expect(notification.type).to.equal(notificationType)
391
392 expect(notification.abuse.id).to.equal(abuseId)
393 } else {
394 expect(notification).to.satisfy((n: UserNotification) => {
395 return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId
396 })
397 }
398 }
399
400 function emailNotificationFinder (email: object) {
401 const text = email['text']
402 const to = email['to'].filter(t => t.address === toEmail)
403
404 return text.indexOf(message) !== -1 && to.length !== 0
405 }
406
407 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
408}
409
410async function checkAbuseStateChange (options: CheckerBaseParams & {
411 abuseId: number
412 state: AbuseState
413 checkType: CheckerType
414}) {
415 const { abuseId, state } = options
416 const notificationType = UserNotificationType.ABUSE_STATE_CHANGE
417
418 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
419 if (checkType === 'presence') {
420 expect(notification).to.not.be.undefined
421 expect(notification.type).to.equal(notificationType)
422
423 expect(notification.abuse.id).to.equal(abuseId)
424 expect(notification.abuse.state).to.equal(state)
425 } else {
426 expect(notification).to.satisfy((n: UserNotification) => {
427 return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId
428 })
429 }
430 }
431
432 function emailNotificationFinder (email: object) {
433 const text = email['text']
434
435 const contains = state === AbuseState.ACCEPTED
436 ? ' accepted'
437 : ' rejected'
438
439 return text.indexOf(contains) !== -1
440 }
441
442 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
443}
444
445async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & {
446 shortUUID: string
447 videoName: string
448 checkType: CheckerType
449}) {
450 const { shortUUID, videoName } = options
451 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
452
453 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
454 if (checkType === 'presence') {
455 expect(notification).to.not.be.undefined
456 expect(notification.type).to.equal(notificationType)
457
458 expect(notification.abuse.id).to.be.a('number')
459 checkVideo(notification.abuse.comment.video, videoName, shortUUID)
460 } else {
461 expect(notification).to.satisfy((n: UserNotification) => {
462 return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID
463 })
464 }
465 }
466
467 function emailNotificationFinder (email: object) {
468 const text = email['text']
469 return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1
470 }
471
472 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
473}
474
475async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & {
476 displayName: string
477 checkType: CheckerType
478}) {
479 const { displayName } = options
480 const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS
481
482 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
483 if (checkType === 'presence') {
484 expect(notification).to.not.be.undefined
485 expect(notification.type).to.equal(notificationType)
486
487 expect(notification.abuse.id).to.be.a('number')
488 expect(notification.abuse.account.displayName).to.equal(displayName)
489 } else {
490 expect(notification).to.satisfy((n: UserNotification) => {
491 return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName
492 })
493 }
494 }
495
496 function emailNotificationFinder (email: object) {
497 const text = email['text']
498 return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1
499 }
500
501 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
502}
503
504async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & {
505 shortUUID: string
506 videoName: string
507 checkType: CheckerType
508}) {
509 const { shortUUID, videoName } = options
510 const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS
511
512 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
513 if (checkType === 'presence') {
514 expect(notification).to.not.be.undefined
515 expect(notification.type).to.equal(notificationType)
516
517 expect(notification.videoBlacklist.video.id).to.be.a('number')
518 checkVideo(notification.videoBlacklist.video, videoName, shortUUID)
519 } else {
520 expect(notification).to.satisfy((n: UserNotification) => {
521 return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID
522 })
523 }
524 }
525
526 function emailNotificationFinder (email: object) {
527 const text = email['text']
528 return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1
529 }
530
531 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
532}
533
534async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & {
535 shortUUID: string
536 videoName: string
537 blacklistType: 'blacklist' | 'unblacklist'
538}) {
539 const { videoName, shortUUID, blacklistType } = options
540 const notificationType = blacklistType === 'blacklist'
541 ? UserNotificationType.BLACKLIST_ON_MY_VIDEO
542 : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO
543
544 function notificationChecker (notification: UserNotification) {
545 expect(notification).to.not.be.undefined
546 expect(notification.type).to.equal(notificationType)
547
548 const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video
549
550 checkVideo(video, videoName, shortUUID)
551 }
552
553 function emailNotificationFinder (email: object) {
554 const text = email['text']
555 const blacklistText = blacklistType === 'blacklist'
556 ? 'blacklisted'
557 : 'unblacklisted'
558
559 return text.includes(shortUUID) && text.includes(blacklistText)
560 }
561
562 await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' })
563}
564
565async function checkNewPeerTubeVersion (options: CheckerBaseParams & {
566 latestVersion: string
567 checkType: CheckerType
568}) {
569 const { latestVersion } = options
570 const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION
571
572 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
573 if (checkType === 'presence') {
574 expect(notification).to.not.be.undefined
575 expect(notification.type).to.equal(notificationType)
576
577 expect(notification.peertube).to.exist
578 expect(notification.peertube.latestVersion).to.equal(latestVersion)
579 } else {
580 expect(notification).to.satisfy((n: UserNotification) => {
581 return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion
582 })
583 }
584 }
585
586 function emailNotificationFinder (email: object) {
587 const text = email['text']
588
589 return text.includes(latestVersion)
590 }
591
592 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
593}
594
595async function checkNewPluginVersion (options: CheckerBaseParams & {
596 pluginType: PluginType
597 pluginName: string
598 checkType: CheckerType
599}) {
600 const { pluginName, pluginType } = options
601 const notificationType = UserNotificationType.NEW_PLUGIN_VERSION
602
603 function notificationChecker (notification: UserNotification, checkType: CheckerType) {
604 if (checkType === 'presence') {
605 expect(notification).to.not.be.undefined
606 expect(notification.type).to.equal(notificationType)
607
608 expect(notification.plugin.name).to.equal(pluginName)
609 expect(notification.plugin.type).to.equal(pluginType)
610 } else {
611 expect(notification).to.satisfy((n: UserNotification) => {
612 return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName
613 })
614 }
615 }
616
617 function emailNotificationFinder (email: object) {
618 const text = email['text']
619
620 return text.includes(pluginName)
621 }
622
623 await checkNotification({ ...options, notificationChecker, emailNotificationFinder })
624}
625
626async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) {
627 const userNotifications: UserNotification[] = []
628 const adminNotifications: UserNotification[] = []
629 const adminNotificationsServer2: UserNotification[] = []
630 const emails: object[] = []
631
632 const port = await MockSmtpServer.Instance.collectEmails(emails)
633
634 const overrideConfig = {
635 smtp: {
636 hostname: 'localhost',
637 port
638 },
639 signup: {
640 limit: 20
641 }
642 }
643 const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg))
644
645 await setAccessTokensToServers(servers)
646
647 if (serversCount > 1) {
648 await doubleFollow(servers[0], servers[1])
649 }
650
651 const user = { username: 'user_1', password: 'super password' }
652 await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 })
653 const userAccessToken = await servers[0].login.getAccessToken(user)
654
655 await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() })
656 await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
657
658 if (serversCount > 1) {
659 await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() })
660 }
661
662 {
663 const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken })
664 socket.on('new-notification', n => userNotifications.push(n))
665 }
666 {
667 const socket = servers[0].socketIO.getUserNotificationSocket()
668 socket.on('new-notification', n => adminNotifications.push(n))
669 }
670
671 if (serversCount > 1) {
672 const socket = servers[1].socketIO.getUserNotificationSocket()
673 socket.on('new-notification', n => adminNotificationsServer2.push(n))
674 }
675
676 const { videoChannels } = await servers[0].users.getMyInfo()
677 const channelId = videoChannels[0].id
678
679 return {
680 userNotifications,
681 adminNotifications,
682 adminNotificationsServer2,
683 userAccessToken,
684 emails,
685 servers,
686 channelId
687 }
688}
689
690// ---------------------------------------------------------------------------
691
692export {
693 getAllNotificationsSettings,
694
695 CheckerBaseParams,
696 CheckerType,
697 checkMyVideoImportIsFinished,
698 checkUserRegistered,
699 checkAutoInstanceFollowing,
700 checkVideoIsPublished,
701 checkNewVideoFromSubscription,
702 checkNewActorFollow,
703 checkNewCommentOnMyVideo,
704 checkNewBlacklistOnMyVideo,
705 checkCommentMention,
706 checkNewVideoAbuseForModerators,
707 checkVideoAutoBlacklistForModerators,
708 checkNewAbuseMessage,
709 checkAbuseStateChange,
710 checkNewInstanceFollower,
711 prepareNotificationsTest,
712 checkNewCommentAbuseForModerators,
713 checkNewAccountAbuseForModerators,
714 checkNewPeerTubeVersion,
715 checkNewPluginVersion
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.getLatest({ 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
deleted file mode 100644
index edc60e612..000000000
--- a/shared/extra-utils/users/subscriptions-command.ts
+++ /dev/null
@@ -1,99 +0,0 @@
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/users-command.ts b/shared/extra-utils/users/users-command.ts
deleted file mode 100644
index 2a10e4fc8..000000000
--- a/shared/extra-utils/users/users-command.ts
+++ /dev/null
@@ -1,415 +0,0 @@
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, role?: UserRole) {
195 const password = 'password'
196 const user = await this.create({ username, password, role })
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, role?: UserRole) {
210 const password = 'password'
211 await this.create({ username, password, role })
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/uuid.ts b/shared/extra-utils/uuid.ts
new file mode 100644
index 000000000..f3c80e046
--- /dev/null
+++ b/shared/extra-utils/uuid.ts
@@ -0,0 +1,32 @@
1import short, { uuid } from 'short-uuid'
2
3const translator = short()
4
5function buildUUID () {
6 return uuid()
7}
8
9function uuidToShort (uuid: string) {
10 if (!uuid) return uuid
11
12 return translator.fromUUID(uuid)
13}
14
15function shortToUUID (shortUUID: string) {
16 if (!shortUUID) return shortUUID
17
18 return translator.toUUID(shortUUID)
19}
20
21function isShortUUID (value: string) {
22 if (!value) return false
23
24 return value.length === translator.maxLength
25}
26
27export {
28 buildUUID,
29 uuidToShort,
30 shortToUUID,
31 isShortUUID
32}
diff --git a/shared/extra-utils/videos/blacklist-command.ts b/shared/extra-utils/videos/blacklist-command.ts
deleted file mode 100644
index 3a2ef89ba..000000000
--- a/shared/extra-utils/videos/blacklist-command.ts
+++ /dev/null
@@ -1,76 +0,0 @@
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
deleted file mode 100644
index a65ea99e3..000000000
--- a/shared/extra-utils/videos/captions-command.ts
+++ /dev/null
@@ -1,65 +0,0 @@
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
deleted file mode 100644
index 35e722408..000000000
--- a/shared/extra-utils/videos/captions.ts
+++ /dev/null
@@ -1,21 +0,0 @@
1import { expect } from 'chai'
2import request from 'supertest'
3import { HttpStatusCode } from '@shared/models'
4
5async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) {
6 const res = await request(url)
7 .get(captionPath)
8 .expect(HttpStatusCode.OK_200)
9
10 if (toTest instanceof RegExp) {
11 expect(res.text).to.match(toTest)
12 } else {
13 expect(res.text).to.contain(toTest)
14 }
15}
16
17// ---------------------------------------------------------------------------
18
19export {
20 testCaptionFile
21}
diff --git a/shared/extra-utils/videos/change-ownership-command.ts b/shared/extra-utils/videos/change-ownership-command.ts
deleted file mode 100644
index ad4c726ef..000000000
--- a/shared/extra-utils/videos/change-ownership-command.ts
+++ /dev/null
@@ -1,68 +0,0 @@
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
deleted file mode 100644
index e406e570b..000000000
--- a/shared/extra-utils/videos/channels-command.ts
+++ /dev/null
@@ -1,178 +0,0 @@
1import { pick } from '@shared/core-utils'
2import { ActorFollow, 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: Partial<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
157 listFollowers (options: OverrideCommandOptions & {
158 channelName: string
159 start?: number
160 count?: number
161 sort?: string
162 search?: string
163 }) {
164 const { channelName, start, count, sort, search } = options
165 const path = '/api/v1/video-channels/' + channelName + '/followers'
166
167 const query = { start, count, sort, search }
168
169 return this.getRequestBody<ResultList<ActorFollow>>({
170 ...options,
171
172 path,
173 query,
174 implicitToken: true,
175 defaultExpectedStatus: HttpStatusCode.OK_200
176 })
177 }
178}
diff --git a/shared/extra-utils/videos/channels.ts b/shared/extra-utils/videos/channels.ts
deleted file mode 100644
index 756c47453..000000000
--- a/shared/extra-utils/videos/channels.ts
+++ /dev/null
@@ -1,18 +0,0 @@
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
deleted file mode 100644
index f0d163a07..000000000
--- a/shared/extra-utils/videos/comments-command.ts
+++ /dev/null
@@ -1,152 +0,0 @@
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
deleted file mode 100644
index 13b7150c1..000000000
--- a/shared/extra-utils/videos/history-command.ts
+++ /dev/null
@@ -1,58 +0,0 @@
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
deleted file mode 100644
index e4944694d..000000000
--- a/shared/extra-utils/videos/imports-command.ts
+++ /dev/null
@@ -1,47 +0,0 @@
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
deleted file mode 100644
index 26e663f46..000000000
--- a/shared/extra-utils/videos/index.ts
+++ /dev/null
@@ -1,19 +0,0 @@
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
deleted file mode 100644
index 74f5d3089..000000000
--- a/shared/extra-utils/videos/live-command.ts
+++ /dev/null
@@ -1,155 +0,0 @@
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 copyCodecs?: boolean
72 }) {
73 const { videoId, fixtureName, copyCodecs } = options
74 const videoLive = await this.get({ videoId })
75
76 return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs })
77 }
78
79 async runAndTestStreamError (options: OverrideCommandOptions & {
80 videoId: number | string
81 shouldHaveError: boolean
82 }) {
83 const command = await this.sendRTMPStreamInVideo(options)
84
85 return testFfmpegStreamError(command, options.shouldHaveError)
86 }
87
88 waitUntilPublished (options: OverrideCommandOptions & {
89 videoId: number | string
90 }) {
91 const { videoId } = options
92 return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
93 }
94
95 waitUntilWaiting (options: OverrideCommandOptions & {
96 videoId: number | string
97 }) {
98 const { videoId } = options
99 return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
100 }
101
102 waitUntilEnded (options: OverrideCommandOptions & {
103 videoId: number | string
104 }) {
105 const { videoId } = options
106 return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
107 }
108
109 waitUntilSegmentGeneration (options: OverrideCommandOptions & {
110 videoUUID: string
111 resolution: number
112 segment: number
113 }) {
114 const { resolution, segment, videoUUID } = options
115 const segmentName = `${resolution}-00000${segment}.ts`
116
117 return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false)
118 }
119
120 async waitUntilSaved (options: OverrideCommandOptions & {
121 videoId: number | string
122 }) {
123 let video: VideoDetails
124
125 do {
126 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
127
128 await wait(500)
129 } while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
130 }
131
132 async countPlaylists (options: OverrideCommandOptions & {
133 videoUUID: string
134 }) {
135 const basePath = this.server.servers.buildDirectory('streaming-playlists')
136 const hlsPath = join(basePath, 'hls', options.videoUUID)
137
138 const files = await readdir(hlsPath)
139
140 return files.filter(f => f.endsWith('.m3u8')).length
141 }
142
143 private async waitUntilState (options: OverrideCommandOptions & {
144 videoId: number | string
145 state: VideoState
146 }) {
147 let video: VideoDetails
148
149 do {
150 video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
151
152 await wait(500)
153 } while (video.state.id !== options.state)
154 }
155}
diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts
deleted file mode 100644
index d3665bc90..000000000
--- a/shared/extra-utils/videos/live.ts
+++ /dev/null
@@ -1,137 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg'
5import { pathExists, readdir } from 'fs-extra'
6import { join } from 'path'
7import { buildAbsoluteFixturePath, wait } from '../miscs'
8import { PeerTubeServer } from '../server/server'
9
10function sendRTMPStream (options: {
11 rtmpBaseUrl: string
12 streamKey: string
13 fixtureName?: string // default video_short.mp4
14 copyCodecs?: boolean // default false
15}) {
16 const { rtmpBaseUrl, streamKey, fixtureName = 'video_short.mp4', copyCodecs = false } = options
17
18 const fixture = buildAbsoluteFixturePath(fixtureName)
19
20 const command = ffmpeg(fixture)
21 command.inputOption('-stream_loop -1')
22 command.inputOption('-re')
23
24 if (copyCodecs) {
25 command.outputOption('-c copy')
26 } else {
27 command.outputOption('-c:v libx264')
28 command.outputOption('-g 50')
29 command.outputOption('-keyint_min 2')
30 command.outputOption('-r 60')
31 }
32
33 command.outputOption('-f flv')
34
35 const rtmpUrl = rtmpBaseUrl + '/' + streamKey
36 command.output(rtmpUrl)
37
38 command.on('error', err => {
39 if (err?.message?.includes('Exiting normally')) return
40
41 if (process.env.DEBUG) console.error(err)
42 })
43
44 if (process.env.DEBUG) {
45 command.on('stderr', data => console.log(data))
46 }
47
48 command.run()
49
50 return command
51}
52
53function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) {
54 return new Promise<void>((res, rej) => {
55 command.on('error', err => {
56 return rej(err)
57 })
58
59 setTimeout(() => {
60 res()
61 }, successAfterMS)
62 })
63}
64
65async function testFfmpegStreamError (command: FfmpegCommand, shouldHaveError: boolean) {
66 let error: Error
67
68 try {
69 await waitFfmpegUntilError(command, 35000)
70 } catch (err) {
71 error = err
72 }
73
74 await stopFfmpeg(command)
75
76 if (shouldHaveError && !error) throw new Error('Ffmpeg did not have an error')
77 if (!shouldHaveError && error) throw error
78}
79
80async function stopFfmpeg (command: FfmpegCommand) {
81 command.kill('SIGINT')
82
83 await wait(500)
84}
85
86async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
87 for (const server of servers) {
88 await server.live.waitUntilPublished({ videoId })
89 }
90}
91
92async function waitUntilLiveSavedOnAllServers (servers: PeerTubeServer[], videoId: string) {
93 for (const server of servers) {
94 await server.live.waitUntilSaved({ videoId })
95 }
96}
97
98async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) {
99 const basePath = server.servers.buildDirectory('streaming-playlists')
100 const hlsPath = join(basePath, 'hls', videoUUID)
101
102 if (resolutions.length === 0) {
103 const result = await pathExists(hlsPath)
104 expect(result).to.be.false
105
106 return
107 }
108
109 const files = await readdir(hlsPath)
110
111 // fragmented file and playlist per resolution + master playlist + segments sha256 json file
112 expect(files).to.have.lengthOf(resolutions.length * 2 + 2)
113
114 for (const resolution of resolutions) {
115 const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`))
116 expect(fragmentedFile).to.exist
117
118 const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`))
119 expect(playlistFile).to.exist
120 }
121
122 const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8'))
123 expect(masterPlaylistFile).to.exist
124
125 const shaFile = files.find(f => f.endsWith('-segments-sha256.json'))
126 expect(shaFile).to.exist
127}
128
129export {
130 sendRTMPStream,
131 waitFfmpegUntilError,
132 testFfmpegStreamError,
133 stopFfmpeg,
134 waitUntilLivePublishedOnAllServers,
135 waitUntilLiveSavedOnAllServers,
136 checkLiveCleanupAfterSave
137}
diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts
deleted file mode 100644
index ce23900d3..000000000
--- a/shared/extra-utils/videos/playlists-command.ts
+++ /dev/null
@@ -1,280 +0,0 @@
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
deleted file mode 100644
index 3dde52bb9..000000000
--- a/shared/extra-utils/videos/playlists.ts
+++ /dev/null
@@ -1,25 +0,0 @@
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
deleted file mode 100644
index 06760df42..000000000
--- a/shared/extra-utils/videos/services-command.ts
+++ /dev/null
@@ -1,29 +0,0 @@
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/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts
deleted file mode 100644
index 5d40d35cb..000000000
--- a/shared/extra-utils/videos/streaming-playlists-command.ts
+++ /dev/null
@@ -1,44 +0,0 @@
1import { HttpStatusCode } from '@shared/models'
2import { unwrapBody, unwrapTextOrDecode, unwrapBodyOrDecodeToJSON } from '../requests'
3import { AbstractCommand, OverrideCommandOptions } from '../shared'
4
5export class StreamingPlaylistsCommand extends AbstractCommand {
6
7 get (options: OverrideCommandOptions & {
8 url: string
9 }) {
10 return unwrapTextOrDecode(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 unwrapBodyOrDecodeToJSON<{ [ 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
deleted file mode 100644
index 6671e3fa6..000000000
--- a/shared/extra-utils/videos/streaming-playlists.ts
+++ /dev/null
@@ -1,77 +0,0 @@
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 resolution: number
13 hlsPlaylist: VideoStreamingPlaylist
14}) {
15 const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options
16 const command = server.streamingPlaylists
17
18 const file = hlsPlaylist.files.find(f => f.resolution.id === resolution)
19 const videoName = basename(file.fileUrl)
20
21 const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` })
22
23 const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
24
25 const length = parseInt(matches[1], 10)
26 const offset = parseInt(matches[2], 10)
27 const range = `${offset}-${offset + length - 1}`
28
29 const segmentBody = await command.getSegment({
30 url: `${baseUrlSegment}/${videoName}`,
31 expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206,
32 range: `bytes=${range}`
33 })
34
35 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
36 expect(sha256(segmentBody)).to.equal(shaBody[videoName][range])
37}
38
39async function checkLiveSegmentHash (options: {
40 server: PeerTubeServer
41 baseUrlSegment: string
42 videoUUID: string
43 segmentName: string
44 hlsPlaylist: VideoStreamingPlaylist
45}) {
46 const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options
47 const command = server.streamingPlaylists
48
49 const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` })
50 const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url })
51
52 expect(sha256(segmentBody)).to.equal(shaBody[segmentName])
53}
54
55async function checkResolutionsInMasterPlaylist (options: {
56 server: PeerTubeServer
57 playlistUrl: string
58 resolutions: number[]
59}) {
60 const { server, playlistUrl, resolutions } = options
61
62 const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl })
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
73export {
74 checkSegmentHash,
75 checkLiveSegmentHash,
76 checkResolutionsInMasterPlaylist
77}
diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts
deleted file mode 100644
index 7ec9c3647..000000000
--- a/shared/extra-utils/videos/videos-command.ts
+++ /dev/null
@@ -1,687 +0,0 @@
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 VideoTranscodingCreate
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 async listFiles (options: OverrideCommandOptions & {
192 id: number | string
193 }) {
194 const video = await this.get(options)
195
196 const files = video.files || []
197 const hlsFiles = video.streamingPlaylists[0]?.files || []
198
199 return files.concat(hlsFiles)
200 }
201
202 // ---------------------------------------------------------------------------
203
204 listMyVideos (options: OverrideCommandOptions & {
205 start?: number
206 count?: number
207 sort?: string
208 search?: string
209 isLive?: boolean
210 channelId?: number
211 } = {}) {
212 const path = '/api/v1/users/me/videos'
213
214 return this.getRequestBody<ResultList<Video>>({
215 ...options,
216
217 path,
218 query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]),
219 implicitToken: true,
220 defaultExpectedStatus: HttpStatusCode.OK_200
221 })
222 }
223
224 // ---------------------------------------------------------------------------
225
226 list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
227 const path = '/api/v1/videos'
228
229 const query = this.buildListQuery(options)
230
231 return this.getRequestBody<ResultList<Video>>({
232 ...options,
233
234 path,
235 query: { sort: 'name', ...query },
236 implicitToken: false,
237 defaultExpectedStatus: HttpStatusCode.OK_200
238 })
239 }
240
241 listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
242 return this.list({
243 ...options,
244
245 token: this.buildCommonRequestToken({ ...options, implicitToken: true })
246 })
247 }
248
249 listByAccount (options: OverrideCommandOptions & VideosCommonQuery & {
250 handle: string
251 }) {
252 const { handle, search } = options
253 const path = '/api/v1/accounts/' + handle + '/videos'
254
255 return this.getRequestBody<ResultList<Video>>({
256 ...options,
257
258 path,
259 query: { search, ...this.buildListQuery(options) },
260 implicitToken: true,
261 defaultExpectedStatus: HttpStatusCode.OK_200
262 })
263 }
264
265 listByChannel (options: OverrideCommandOptions & VideosCommonQuery & {
266 handle: string
267 }) {
268 const { handle } = options
269 const path = '/api/v1/video-channels/' + handle + '/videos'
270
271 return this.getRequestBody<ResultList<Video>>({
272 ...options,
273
274 path,
275 query: this.buildListQuery(options),
276 implicitToken: true,
277 defaultExpectedStatus: HttpStatusCode.OK_200
278 })
279 }
280
281 // ---------------------------------------------------------------------------
282
283 async find (options: OverrideCommandOptions & {
284 name: string
285 }) {
286 const { data } = await this.list(options)
287
288 return data.find(v => v.name === options.name)
289 }
290
291 // ---------------------------------------------------------------------------
292
293 update (options: OverrideCommandOptions & {
294 id: number | string
295 attributes?: VideoEdit
296 }) {
297 const { id, attributes = {} } = options
298 const path = '/api/v1/videos/' + id
299
300 // Upload request
301 if (attributes.thumbnailfile || attributes.previewfile) {
302 const attaches: any = {}
303 if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
304 if (attributes.previewfile) attaches.previewfile = attributes.previewfile
305
306 return this.putUploadRequest({
307 ...options,
308
309 path,
310 fields: options.attributes,
311 attaches: {
312 thumbnailfile: attributes.thumbnailfile,
313 previewfile: attributes.previewfile
314 },
315 implicitToken: true,
316 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
317 })
318 }
319
320 return this.putBodyRequest({
321 ...options,
322
323 path,
324 fields: options.attributes,
325 implicitToken: true,
326 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
327 })
328 }
329
330 remove (options: OverrideCommandOptions & {
331 id: number | string
332 }) {
333 const path = '/api/v1/videos/' + options.id
334
335 return unwrapBody(this.deleteRequest({
336 ...options,
337
338 path,
339 implicitToken: true,
340 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
341 }))
342 }
343
344 async removeAll () {
345 const { data } = await this.list()
346
347 for (const v of data) {
348 await this.remove({ id: v.id })
349 }
350 }
351
352 // ---------------------------------------------------------------------------
353
354 async upload (options: OverrideCommandOptions & {
355 attributes?: VideoEdit
356 mode?: 'legacy' | 'resumable' // default legacy
357 } = {}) {
358 const { mode = 'legacy' } = options
359 let defaultChannelId = 1
360
361 try {
362 const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
363 defaultChannelId = videoChannels[0].id
364 } catch (e) { /* empty */ }
365
366 // Override default attributes
367 const attributes = {
368 name: 'my super video',
369 category: 5,
370 licence: 4,
371 language: 'zh',
372 channelId: defaultChannelId,
373 nsfw: true,
374 waitTranscoding: false,
375 description: 'my super description',
376 support: 'my super support text',
377 tags: [ 'tag' ],
378 privacy: VideoPrivacy.PUBLIC,
379 commentsEnabled: true,
380 downloadEnabled: true,
381 fixture: 'video_short.webm',
382
383 ...options.attributes
384 }
385
386 const created = mode === 'legacy'
387 ? await this.buildLegacyUpload({ ...options, attributes })
388 : await this.buildResumeUpload({ ...options, attributes })
389
390 // Wait torrent generation
391 const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
392 if (expectedStatus === HttpStatusCode.OK_200) {
393 let video: VideoDetails
394
395 do {
396 video = await this.getWithToken({ ...options, id: created.uuid })
397
398 await wait(50)
399 } while (!video.files[0].torrentUrl)
400 }
401
402 return created
403 }
404
405 async buildLegacyUpload (options: OverrideCommandOptions & {
406 attributes: VideoEdit
407 }): Promise<VideoCreateResult> {
408 const path = '/api/v1/videos/upload'
409
410 return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
411 ...options,
412
413 path,
414 fields: this.buildUploadFields(options.attributes),
415 attaches: this.buildUploadAttaches(options.attributes),
416 implicitToken: true,
417 defaultExpectedStatus: HttpStatusCode.OK_200
418 })).then(body => body.video || body as any)
419 }
420
421 async buildResumeUpload (options: OverrideCommandOptions & {
422 attributes: VideoEdit
423 }): Promise<VideoCreateResult> {
424 const { attributes, expectedStatus } = options
425
426 let size = 0
427 let videoFilePath: string
428 let mimetype = 'video/mp4'
429
430 if (attributes.fixture) {
431 videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
432 size = (await stat(videoFilePath)).size
433
434 if (videoFilePath.endsWith('.mkv')) {
435 mimetype = 'video/x-matroska'
436 } else if (videoFilePath.endsWith('.webm')) {
437 mimetype = 'video/webm'
438 }
439 }
440
441 // Do not check status automatically, we'll check it manually
442 const initializeSessionRes = await this.prepareResumableUpload({ ...options, expectedStatus: null, attributes, size, mimetype })
443 const initStatus = initializeSessionRes.status
444
445 if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
446 const locationHeader = initializeSessionRes.header['location']
447 expect(locationHeader).to.not.be.undefined
448
449 const pathUploadId = locationHeader.split('?')[1]
450
451 const result = await this.sendResumableChunks({ ...options, pathUploadId, videoFilePath, size })
452
453 if (result.statusCode === HttpStatusCode.OK_200) {
454 await this.endResumableUpload({ ...options, expectedStatus: HttpStatusCode.NO_CONTENT_204, pathUploadId })
455 }
456
457 return result.body?.video || result.body as any
458 }
459
460 const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
461 ? HttpStatusCode.CREATED_201
462 : expectedStatus
463
464 expect(initStatus).to.equal(expectedInitStatus)
465
466 return initializeSessionRes.body.video || initializeSessionRes.body
467 }
468
469 async prepareResumableUpload (options: OverrideCommandOptions & {
470 attributes: VideoEdit
471 size: number
472 mimetype: string
473
474 originalName?: string
475 lastModified?: number
476 }) {
477 const { attributes, originalName, lastModified, size, mimetype } = options
478
479 const path = '/api/v1/videos/upload-resumable'
480
481 return this.postUploadRequest({
482 ...options,
483
484 path,
485 headers: {
486 'X-Upload-Content-Type': mimetype,
487 'X-Upload-Content-Length': size.toString()
488 },
489 fields: {
490 filename: attributes.fixture,
491 originalName,
492 lastModified,
493
494 ...this.buildUploadFields(options.attributes)
495 },
496
497 // Fixture will be sent later
498 attaches: this.buildUploadAttaches(omit(options.attributes, 'fixture')),
499 implicitToken: true,
500
501 defaultExpectedStatus: null
502 })
503 }
504
505 sendResumableChunks (options: OverrideCommandOptions & {
506 pathUploadId: string
507 videoFilePath: string
508 size: number
509 contentLength?: number
510 contentRangeBuilder?: (start: number, chunk: any) => string
511 }) {
512 const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options
513
514 const path = '/api/v1/videos/upload-resumable'
515 let start = 0
516
517 const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
518 const url = this.server.url
519
520 const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
521 return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
522 readable.on('data', async function onData (chunk) {
523 readable.pause()
524
525 const headers = {
526 'Authorization': 'Bearer ' + token,
527 'Content-Type': 'application/octet-stream',
528 'Content-Range': contentRangeBuilder
529 ? contentRangeBuilder(start, chunk)
530 : `bytes ${start}-${start + chunk.length - 1}/${size}`,
531 'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
532 }
533
534 const res = await got<{ video: VideoCreateResult }>({
535 url,
536 method: 'put',
537 headers,
538 path: path + '?' + pathUploadId,
539 body: chunk,
540 responseType: 'json',
541 throwHttpErrors: false
542 })
543
544 start += chunk.length
545
546 if (res.statusCode === expectedStatus) {
547 return resolve(res)
548 }
549
550 if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
551 readable.off('data', onData)
552 return reject(new Error('Incorrect transient behaviour sending intermediary chunks'))
553 }
554
555 readable.resume()
556 })
557 })
558 }
559
560 endResumableUpload (options: OverrideCommandOptions & {
561 pathUploadId: string
562 }) {
563 return this.deleteRequest({
564 ...options,
565
566 path: '/api/v1/videos/upload-resumable',
567 rawQuery: options.pathUploadId,
568 implicitToken: true,
569 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
570 })
571 }
572
573 quickUpload (options: OverrideCommandOptions & {
574 name: string
575 nsfw?: boolean
576 privacy?: VideoPrivacy
577 fixture?: string
578 }) {
579 const attributes: VideoEdit = { name: options.name }
580 if (options.nsfw) attributes.nsfw = options.nsfw
581 if (options.privacy) attributes.privacy = options.privacy
582 if (options.fixture) attributes.fixture = options.fixture
583
584 return this.upload({ ...options, attributes })
585 }
586
587 async randomUpload (options: OverrideCommandOptions & {
588 wait?: boolean // default true
589 additionalParams?: VideoEdit & { prefixName?: string }
590 } = {}) {
591 const { wait = true, additionalParams } = options
592 const prefixName = additionalParams?.prefixName || ''
593 const name = prefixName + buildUUID()
594
595 const attributes = { name, ...additionalParams }
596
597 const result = await this.upload({ ...options, attributes })
598
599 if (wait) await waitJobs([ this.server ])
600
601 return { ...result, name }
602 }
603
604 // ---------------------------------------------------------------------------
605
606 removeHLSFiles (options: OverrideCommandOptions & {
607 videoId: number | string
608 }) {
609 const path = '/api/v1/videos/' + options.videoId + '/hls'
610
611 return this.deleteRequest({
612 ...options,
613
614 path,
615 implicitToken: true,
616 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
617 })
618 }
619
620 removeWebTorrentFiles (options: OverrideCommandOptions & {
621 videoId: number | string
622 }) {
623 const path = '/api/v1/videos/' + options.videoId + '/webtorrent'
624
625 return this.deleteRequest({
626 ...options,
627
628 path,
629 implicitToken: true,
630 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
631 })
632 }
633
634 runTranscoding (options: OverrideCommandOptions & {
635 videoId: number | string
636 transcodingType: 'hls' | 'webtorrent'
637 }) {
638 const path = '/api/v1/videos/' + options.videoId + '/transcoding'
639
640 const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ])
641
642 return this.postBodyRequest({
643 ...options,
644
645 path,
646 fields,
647 implicitToken: true,
648 defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
649 })
650 }
651
652 // ---------------------------------------------------------------------------
653
654 private buildListQuery (options: VideosCommonQuery) {
655 return pick(options, [
656 'start',
657 'count',
658 'sort',
659 'nsfw',
660 'isLive',
661 'categoryOneOf',
662 'licenceOneOf',
663 'languageOneOf',
664 'tagsOneOf',
665 'tagsAllOf',
666 'isLocal',
667 'include',
668 'skipCount'
669 ])
670 }
671
672 private buildUploadFields (attributes: VideoEdit) {
673 return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
674 }
675
676 private buildUploadAttaches (attributes: VideoEdit) {
677 const attaches: { [ name: string ]: string } = {}
678
679 for (const key of [ 'thumbnailfile', 'previewfile' ]) {
680 if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
681 }
682
683 if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
684
685 return attaches
686 }
687}
diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts
deleted file mode 100644
index 4d2784dde..000000000
--- a/shared/extra-utils/videos/videos.ts
+++ /dev/null
@@ -1,253 +0,0 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
2
3import { expect } from 'chai'
4import { pathExists, readdir } from 'fs-extra'
5import { basename, join } from 'path'
6import { getLowercaseExtension } from '@server/helpers/core-utils'
7import { uuidRegex } from '@shared/core-utils'
8import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models'
9import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../server/initializers/constants'
10import { dateIsValid, testImage, webtorrentAdd } from '../miscs'
11import { makeRawRequest } from '../requests/requests'
12import { waitJobs } from '../server'
13import { PeerTubeServer } from '../server/server'
14import { VideoEdit } from './videos-command'
15
16async function checkVideoFilesWereRemoved (options: {
17 server: PeerTubeServer
18 video: VideoDetails
19 captions?: VideoCaption[]
20 onlyVideoFiles?: boolean // default false
21}) {
22 const { video, server, captions = [], onlyVideoFiles = false } = options
23
24 const webtorrentFiles = video.files || []
25 const hlsFiles = video.streamingPlaylists[0]?.files || []
26
27 const thumbnailName = basename(video.thumbnailPath)
28 const previewName = basename(video.previewPath)
29
30 const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl))
31
32 const captionNames = captions.map(c => basename(c.captionPath))
33
34 const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl))
35 const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl))
36
37 let directories: { [ directory: string ]: string[] } = {
38 videos: webtorrentFilenames,
39 redundancy: webtorrentFilenames,
40 [join('playlists', 'hls')]: hlsFilenames,
41 [join('redundancy', 'hls')]: hlsFilenames
42 }
43
44 if (onlyVideoFiles !== true) {
45 directories = {
46 ...directories,
47
48 thumbnails: [ thumbnailName ],
49 previews: [ previewName ],
50 torrents: torrentNames,
51 captions: captionNames
52 }
53 }
54
55 for (const directory of Object.keys(directories)) {
56 const directoryPath = server.servers.buildDirectory(directory)
57
58 const directoryExists = await pathExists(directoryPath)
59 if (directoryExists === false) continue
60
61 const existingFiles = await readdir(directoryPath)
62 for (const existingFile of existingFiles) {
63 for (const shouldNotExist of directories[directory]) {
64 expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist)
65 }
66 }
67 }
68}
69
70async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) {
71 for (const server of servers) {
72 server.store.videoDetails = await server.videos.get({ id: uuid })
73 }
74}
75
76function checkUploadVideoParam (
77 server: PeerTubeServer,
78 token: string,
79 attributes: Partial<VideoEdit>,
80 expectedStatus = HttpStatusCode.OK_200,
81 mode: 'legacy' | 'resumable' = 'legacy'
82) {
83 return mode === 'legacy'
84 ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus })
85 : server.videos.buildResumeUpload({ token, attributes, expectedStatus })
86}
87
88async function completeVideoCheck (
89 server: PeerTubeServer,
90 video: any,
91 attributes: {
92 name: string
93 category: number
94 licence: number
95 language: string
96 nsfw: boolean
97 commentsEnabled: boolean
98 downloadEnabled: boolean
99 description: string
100 publishedAt?: string
101 support: string
102 originallyPublishedAt?: string
103 account: {
104 name: string
105 host: string
106 }
107 isLocal: boolean
108 tags: string[]
109 privacy: number
110 likes?: number
111 dislikes?: number
112 duration: number
113 channel: {
114 displayName: string
115 name: string
116 description: string
117 isLocal: boolean
118 }
119 fixture: string
120 files: {
121 resolution: number
122 size: number
123 }[]
124 thumbnailfile?: string
125 previewfile?: string
126 }
127) {
128 if (!attributes.likes) attributes.likes = 0
129 if (!attributes.dislikes) attributes.dislikes = 0
130
131 const host = new URL(server.url).host
132 const originHost = attributes.account.host
133
134 expect(video.name).to.equal(attributes.name)
135 expect(video.category.id).to.equal(attributes.category)
136 expect(video.category.label).to.equal(attributes.category !== null ? VIDEO_CATEGORIES[attributes.category] : 'Misc')
137 expect(video.licence.id).to.equal(attributes.licence)
138 expect(video.licence.label).to.equal(attributes.licence !== null ? VIDEO_LICENCES[attributes.licence] : 'Unknown')
139 expect(video.language.id).to.equal(attributes.language)
140 expect(video.language.label).to.equal(attributes.language !== null ? VIDEO_LANGUAGES[attributes.language] : 'Unknown')
141 expect(video.privacy.id).to.deep.equal(attributes.privacy)
142 expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
143 expect(video.nsfw).to.equal(attributes.nsfw)
144 expect(video.description).to.equal(attributes.description)
145 expect(video.account.id).to.be.a('number')
146 expect(video.account.host).to.equal(attributes.account.host)
147 expect(video.account.name).to.equal(attributes.account.name)
148 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
149 expect(video.channel.name).to.equal(attributes.channel.name)
150 expect(video.likes).to.equal(attributes.likes)
151 expect(video.dislikes).to.equal(attributes.dislikes)
152 expect(video.isLocal).to.equal(attributes.isLocal)
153 expect(video.duration).to.equal(attributes.duration)
154 expect(video.url).to.contain(originHost)
155 expect(dateIsValid(video.createdAt)).to.be.true
156 expect(dateIsValid(video.publishedAt)).to.be.true
157 expect(dateIsValid(video.updatedAt)).to.be.true
158
159 if (attributes.publishedAt) {
160 expect(video.publishedAt).to.equal(attributes.publishedAt)
161 }
162
163 if (attributes.originallyPublishedAt) {
164 expect(video.originallyPublishedAt).to.equal(attributes.originallyPublishedAt)
165 } else {
166 expect(video.originallyPublishedAt).to.be.null
167 }
168
169 const videoDetails = await server.videos.get({ id: video.uuid })
170
171 expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
172 expect(videoDetails.tags).to.deep.equal(attributes.tags)
173 expect(videoDetails.account.name).to.equal(attributes.account.name)
174 expect(videoDetails.account.host).to.equal(attributes.account.host)
175 expect(video.channel.displayName).to.equal(attributes.channel.displayName)
176 expect(video.channel.name).to.equal(attributes.channel.name)
177 expect(videoDetails.channel.host).to.equal(attributes.account.host)
178 expect(videoDetails.channel.isLocal).to.equal(attributes.channel.isLocal)
179 expect(dateIsValid(videoDetails.channel.createdAt.toString())).to.be.true
180 expect(dateIsValid(videoDetails.channel.updatedAt.toString())).to.be.true
181 expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
182 expect(videoDetails.downloadEnabled).to.equal(attributes.downloadEnabled)
183
184 for (const attributeFile of attributes.files) {
185 const file = videoDetails.files.find(f => f.resolution.id === attributeFile.resolution)
186 expect(file).not.to.be.undefined
187
188 let extension = getLowercaseExtension(attributes.fixture)
189 // Transcoding enabled: extension will always be .mp4
190 if (attributes.files.length > 1) extension = '.mp4'
191
192 expect(file.magnetUri).to.have.lengthOf.above(2)
193
194 expect(file.torrentDownloadUrl).to.match(new RegExp(`http://${host}/download/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
195 expect(file.torrentUrl).to.match(new RegExp(`http://${host}/lazy-static/torrents/${uuidRegex}-${file.resolution.id}.torrent`))
196
197 expect(file.fileUrl).to.match(new RegExp(`http://${originHost}/static/webseed/${uuidRegex}-${file.resolution.id}${extension}`))
198 expect(file.fileDownloadUrl).to.match(new RegExp(`http://${originHost}/download/videos/${uuidRegex}-${file.resolution.id}${extension}`))
199
200 await Promise.all([
201 makeRawRequest(file.torrentUrl, 200),
202 makeRawRequest(file.torrentDownloadUrl, 200),
203 makeRawRequest(file.metadataUrl, 200)
204 ])
205
206 expect(file.resolution.id).to.equal(attributeFile.resolution)
207 expect(file.resolution.label).to.equal(attributeFile.resolution + 'p')
208
209 const minSize = attributeFile.size - ((10 * attributeFile.size) / 100)
210 const maxSize = attributeFile.size + ((10 * attributeFile.size) / 100)
211 expect(
212 file.size,
213 'File size for resolution ' + file.resolution.label + ' outside confidence interval (' + minSize + '> size <' + maxSize + ')'
214 ).to.be.above(minSize).and.below(maxSize)
215
216 const torrent = await webtorrentAdd(file.magnetUri, true)
217 expect(torrent.files).to.be.an('array')
218 expect(torrent.files.length).to.equal(1)
219 expect(torrent.files[0].path).to.exist.and.to.not.equal('')
220 }
221
222 expect(videoDetails.thumbnailPath).to.exist
223 await testImage(server.url, attributes.thumbnailfile || attributes.fixture, videoDetails.thumbnailPath)
224
225 if (attributes.previewfile) {
226 expect(videoDetails.previewPath).to.exist
227 await testImage(server.url, attributes.previewfile, videoDetails.previewPath)
228 }
229}
230
231// serverNumber starts from 1
232async function uploadRandomVideoOnServers (
233 servers: PeerTubeServer[],
234 serverNumber: number,
235 additionalParams?: VideoEdit & { prefixName?: string }
236) {
237 const server = servers.find(s => s.serverNumber === serverNumber)
238 const res = await server.videos.randomUpload({ wait: false, additionalParams })
239
240 await waitJobs(servers)
241
242 return res
243}
244
245// ---------------------------------------------------------------------------
246
247export {
248 checkUploadVideoParam,
249 completeVideoCheck,
250 uploadRandomVideoOnServers,
251 checkVideoFilesWereRemoved,
252 saveVideoInServers
253}