mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-10-04 10:19:35 +02:00
Merge branch 'feature/esm-and-nx' into develop
This commit is contained in:
commit
c380e39285
2196 changed files with 12690 additions and 11574 deletions
19
packages/server-commands/package.json
Normal file
19
packages/server-commands/package.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "@peertube/peertube-server-commands",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.js",
|
||||
"files": [ "dist" ],
|
||||
"exports": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"peertube:tsx": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
20
packages/server-commands/src/bulk/bulk-command.ts
Normal file
20
packages/server-commands/src/bulk/bulk-command.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { BulkRemoveCommentsOfBody, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class BulkCommand extends AbstractCommand {
|
||||
|
||||
removeCommentsOf (options: OverrideCommandOptions & {
|
||||
attributes: BulkRemoveCommentsOfBody
|
||||
}) {
|
||||
const { attributes } = options
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: '/api/v1/bulk/remove-comments-of',
|
||||
fields: attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
1
packages/server-commands/src/bulk/index.ts
Normal file
1
packages/server-commands/src/bulk/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './bulk-command.js'
|
27
packages/server-commands/src/cli/cli-command.ts
Normal file
27
packages/server-commands/src/cli/cli-command.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { exec } from 'child_process'
|
||||
import { AbstractCommand } from '../shared/index.js'
|
||||
|
||||
export class CLICommand extends AbstractCommand {
|
||||
|
||||
static exec (command: string) {
|
||||
return new Promise<string>((res, rej) => {
|
||||
exec(command, (err, stdout, _stderr) => {
|
||||
if (err) return rej(err)
|
||||
|
||||
return res(stdout)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
getEnv () {
|
||||
return `NODE_ENV=test NODE_APP_INSTANCE=${this.server.internalServerNumber}`
|
||||
}
|
||||
|
||||
async execWithEnv (command: string, configOverride?: any) {
|
||||
const prefix = configOverride
|
||||
? `NODE_CONFIG='${JSON.stringify(configOverride)}'`
|
||||
: ''
|
||||
|
||||
return CLICommand.exec(`${prefix} ${this.getEnv()} ${command}`)
|
||||
}
|
||||
}
|
1
packages/server-commands/src/cli/index.ts
Normal file
1
packages/server-commands/src/cli/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './cli-command.js'
|
|
@ -0,0 +1,33 @@
|
|||
import { CustomPage, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class CustomPagesCommand extends AbstractCommand {
|
||||
|
||||
getInstanceHomepage (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/custom-pages/homepage/instance'
|
||||
|
||||
return this.getRequestBody<CustomPage>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
updateInstanceHomepage (options: OverrideCommandOptions & {
|
||||
content: string
|
||||
}) {
|
||||
const { content } = options
|
||||
const path = '/api/v1/custom-pages/homepage/instance'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { content },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
1
packages/server-commands/src/custom-pages/index.ts
Normal file
1
packages/server-commands/src/custom-pages/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './custom-pages-command.js'
|
78
packages/server-commands/src/feeds/feeds-command.ts
Normal file
78
packages/server-commands/src/feeds/feeds-command.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
type FeedType = 'videos' | 'video-comments' | 'subscriptions'
|
||||
|
||||
export class FeedCommand extends AbstractCommand {
|
||||
|
||||
getXML (options: OverrideCommandOptions & {
|
||||
feed: FeedType
|
||||
ignoreCache: boolean
|
||||
format?: string
|
||||
}) {
|
||||
const { feed, format, ignoreCache } = options
|
||||
const path = '/feeds/' + feed + '.xml'
|
||||
|
||||
const query: { [id: string]: string } = {}
|
||||
|
||||
if (ignoreCache) query.v = buildUUID()
|
||||
if (format) query.format = format
|
||||
|
||||
return this.getRequestText({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
accept: 'application/xml',
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getPodcastXML (options: OverrideCommandOptions & {
|
||||
ignoreCache: boolean
|
||||
channelId: number
|
||||
}) {
|
||||
const { ignoreCache, channelId } = options
|
||||
const path = `/feeds/podcast/videos.xml`
|
||||
|
||||
const query: { [id: string]: string } = {}
|
||||
|
||||
if (ignoreCache) query.v = buildUUID()
|
||||
if (channelId) query.videoChannelId = channelId + ''
|
||||
|
||||
return this.getRequestText({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
accept: 'application/xml',
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getJSON (options: OverrideCommandOptions & {
|
||||
feed: FeedType
|
||||
ignoreCache: boolean
|
||||
query?: { [ id: string ]: any }
|
||||
}) {
|
||||
const { feed, query = {}, ignoreCache } = options
|
||||
const path = '/feeds/' + feed + '.json'
|
||||
|
||||
const cacheQuery = ignoreCache
|
||||
? { v: buildUUID() }
|
||||
: {}
|
||||
|
||||
return this.getRequestText({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { ...query, ...cacheQuery },
|
||||
accept: 'application/json',
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
1
packages/server-commands/src/feeds/index.ts
Normal file
1
packages/server-commands/src/feeds/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './feeds-command.js'
|
14
packages/server-commands/src/index.ts
Normal file
14
packages/server-commands/src/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export * from './bulk/index.js'
|
||||
export * from './cli/index.js'
|
||||
export * from './custom-pages/index.js'
|
||||
export * from './feeds/index.js'
|
||||
export * from './logs/index.js'
|
||||
export * from './moderation/index.js'
|
||||
export * from './overviews/index.js'
|
||||
export * from './requests/index.js'
|
||||
export * from './runners/index.js'
|
||||
export * from './search/index.js'
|
||||
export * from './server/index.js'
|
||||
export * from './socket/index.js'
|
||||
export * from './users/index.js'
|
||||
export * from './videos/index.js'
|
1
packages/server-commands/src/logs/index.ts
Normal file
1
packages/server-commands/src/logs/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './logs-command.js'
|
56
packages/server-commands/src/logs/logs-command.ts
Normal file
56
packages/server-commands/src/logs/logs-command.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { ClientLogCreate, HttpStatusCode, ServerLogLevel } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class LogsCommand extends AbstractCommand {
|
||||
|
||||
createLogClient (options: OverrideCommandOptions & { payload: ClientLogCreate }) {
|
||||
const path = '/api/v1/server/logs/client'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.payload,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
getLogs (options: OverrideCommandOptions & {
|
||||
startDate: Date
|
||||
endDate?: Date
|
||||
level?: ServerLogLevel
|
||||
tagsOneOf?: string[]
|
||||
}) {
|
||||
const { startDate, endDate, tagsOneOf, level } = options
|
||||
const path = '/api/v1/server/logs'
|
||||
|
||||
return this.getRequestBody<any[]>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { startDate, endDate, level, tagsOneOf },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getAuditLogs (options: OverrideCommandOptions & {
|
||||
startDate: Date
|
||||
endDate?: Date
|
||||
}) {
|
||||
const { startDate, endDate } = options
|
||||
|
||||
const path = '/api/v1/server/audit-logs'
|
||||
|
||||
return this.getRequestBody({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { startDate, endDate },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
}
|
228
packages/server-commands/src/moderation/abuses-command.ts
Normal file
228
packages/server-commands/src/moderation/abuses-command.ts
Normal file
|
@ -0,0 +1,228 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
AbuseFilter,
|
||||
AbuseMessage,
|
||||
AbusePredefinedReasonsString,
|
||||
AbuseStateType,
|
||||
AbuseUpdate,
|
||||
AbuseVideoIs,
|
||||
AdminAbuse,
|
||||
HttpStatusCode,
|
||||
ResultList,
|
||||
UserAbuse
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/requests.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class AbusesCommand extends AbstractCommand {
|
||||
|
||||
report (options: OverrideCommandOptions & {
|
||||
reason: string
|
||||
|
||||
accountId?: number
|
||||
videoId?: number
|
||||
commentId?: number
|
||||
|
||||
predefinedReasons?: AbusePredefinedReasonsString[]
|
||||
|
||||
startAt?: number
|
||||
endAt?: number
|
||||
}) {
|
||||
const path = '/api/v1/abuses'
|
||||
|
||||
const video = options.videoId
|
||||
? {
|
||||
id: options.videoId,
|
||||
startAt: options.startAt,
|
||||
endAt: options.endAt
|
||||
}
|
||||
: undefined
|
||||
|
||||
const comment = options.commentId
|
||||
? { id: options.commentId }
|
||||
: undefined
|
||||
|
||||
const account = options.accountId
|
||||
? { id: options.accountId }
|
||||
: undefined
|
||||
|
||||
const body = {
|
||||
account,
|
||||
video,
|
||||
comment,
|
||||
|
||||
reason: options.reason,
|
||||
predefinedReasons: options.predefinedReasons
|
||||
}
|
||||
|
||||
return unwrapBody<{ abuse: { id: number } }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: body,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
getAdminList (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
|
||||
id?: number
|
||||
predefinedReason?: AbusePredefinedReasonsString
|
||||
search?: string
|
||||
filter?: AbuseFilter
|
||||
state?: AbuseStateType
|
||||
videoIs?: AbuseVideoIs
|
||||
searchReporter?: string
|
||||
searchReportee?: string
|
||||
searchVideo?: string
|
||||
searchVideoChannel?: string
|
||||
} = {}) {
|
||||
const toPick: (keyof typeof options)[] = [
|
||||
'count',
|
||||
'filter',
|
||||
'id',
|
||||
'predefinedReason',
|
||||
'search',
|
||||
'searchReportee',
|
||||
'searchReporter',
|
||||
'searchVideo',
|
||||
'searchVideoChannel',
|
||||
'sort',
|
||||
'start',
|
||||
'state',
|
||||
'videoIs'
|
||||
]
|
||||
|
||||
const path = '/api/v1/abuses'
|
||||
|
||||
const defaultQuery = { sort: 'createdAt' }
|
||||
const query = { ...defaultQuery, ...pick(options, toPick) }
|
||||
|
||||
return this.getRequestBody<ResultList<AdminAbuse>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getUserList (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
|
||||
id?: number
|
||||
search?: string
|
||||
state?: AbuseStateType
|
||||
}) {
|
||||
const toPick: (keyof typeof options)[] = [
|
||||
'id',
|
||||
'search',
|
||||
'state',
|
||||
'start',
|
||||
'count',
|
||||
'sort'
|
||||
]
|
||||
|
||||
const path = '/api/v1/users/me/abuses'
|
||||
|
||||
const defaultQuery = { sort: 'createdAt' }
|
||||
const query = { ...defaultQuery, ...pick(options, toPick) }
|
||||
|
||||
return this.getRequestBody<ResultList<UserAbuse>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
abuseId: number
|
||||
body: AbuseUpdate
|
||||
}) {
|
||||
const { abuseId, body } = options
|
||||
const path = '/api/v1/abuses/' + abuseId
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: body,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
abuseId: number
|
||||
}) {
|
||||
const { abuseId } = options
|
||||
const path = '/api/v1/abuses/' + abuseId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
listMessages (options: OverrideCommandOptions & {
|
||||
abuseId: number
|
||||
}) {
|
||||
const { abuseId } = options
|
||||
const path = '/api/v1/abuses/' + abuseId + '/messages'
|
||||
|
||||
return this.getRequestBody<ResultList<AbuseMessage>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
deleteMessage (options: OverrideCommandOptions & {
|
||||
abuseId: number
|
||||
messageId: number
|
||||
}) {
|
||||
const { abuseId, messageId } = options
|
||||
const path = '/api/v1/abuses/' + abuseId + '/messages/' + messageId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
addMessage (options: OverrideCommandOptions & {
|
||||
abuseId: number
|
||||
message: string
|
||||
}) {
|
||||
const { abuseId, message } = options
|
||||
const path = '/api/v1/abuses/' + abuseId + '/messages'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { message },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
}
|
1
packages/server-commands/src/moderation/index.ts
Normal file
1
packages/server-commands/src/moderation/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './abuses-command.js'
|
1
packages/server-commands/src/overviews/index.ts
Normal file
1
packages/server-commands/src/overviews/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './overviews-command.js'
|
23
packages/server-commands/src/overviews/overviews-command.ts
Normal file
23
packages/server-commands/src/overviews/overviews-command.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { HttpStatusCode, VideosOverview } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class OverviewsCommand extends AbstractCommand {
|
||||
|
||||
getVideos (options: OverrideCommandOptions & {
|
||||
page: number
|
||||
}) {
|
||||
const { page } = options
|
||||
const path = '/api/v1/overviews/videos'
|
||||
|
||||
const query = { page }
|
||||
|
||||
return this.getRequestBody<VideosOverview>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
1
packages/server-commands/src/requests/index.ts
Normal file
1
packages/server-commands/src/requests/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './requests.js'
|
260
packages/server-commands/src/requests/requests.ts
Normal file
260
packages/server-commands/src/requests/requests.ts
Normal file
|
@ -0,0 +1,260 @@
|
|||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
|
||||
import { decode } from 'querystring'
|
||||
import request from 'supertest'
|
||||
import { URL } from 'url'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, HttpStatusCodeType } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
|
||||
export type CommonRequestParams = {
|
||||
url: string
|
||||
path?: string
|
||||
contentType?: string
|
||||
responseType?: string
|
||||
range?: string
|
||||
redirects?: number
|
||||
accept?: string
|
||||
host?: string
|
||||
token?: string
|
||||
headers?: { [ name: string ]: string }
|
||||
type?: string
|
||||
xForwardedFor?: string
|
||||
expectedStatus?: HttpStatusCodeType
|
||||
}
|
||||
|
||||
function makeRawRequest (options: {
|
||||
url: string
|
||||
token?: string
|
||||
expectedStatus?: HttpStatusCodeType
|
||||
range?: string
|
||||
query?: { [ id: string ]: string }
|
||||
method?: 'GET' | 'POST'
|
||||
headers?: { [ name: string ]: string }
|
||||
}) {
|
||||
const { host, protocol, pathname } = new URL(options.url)
|
||||
|
||||
const reqOptions = {
|
||||
url: `${protocol}//${host}`,
|
||||
path: pathname,
|
||||
contentType: undefined,
|
||||
|
||||
...pick(options, [ 'expectedStatus', 'range', 'token', 'query', 'headers' ])
|
||||
}
|
||||
|
||||
if (options.method === 'POST') {
|
||||
return makePostBodyRequest(reqOptions)
|
||||
}
|
||||
|
||||
return makeGetRequest(reqOptions)
|
||||
}
|
||||
|
||||
function makeGetRequest (options: CommonRequestParams & {
|
||||
query?: any
|
||||
rawQuery?: string
|
||||
}) {
|
||||
const req = request(options.url).get(options.path)
|
||||
|
||||
if (options.query) req.query(options.query)
|
||||
if (options.rawQuery) req.query(options.rawQuery)
|
||||
|
||||
return buildRequest(req, { contentType: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
|
||||
}
|
||||
|
||||
function makeHTMLRequest (url: string, path: string) {
|
||||
return makeGetRequest({
|
||||
url,
|
||||
path,
|
||||
accept: 'text/html',
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
function makeActivityPubGetRequest (url: string, path: string, expectedStatus: HttpStatusCodeType = HttpStatusCode.OK_200) {
|
||||
return makeGetRequest({
|
||||
url,
|
||||
path,
|
||||
expectedStatus,
|
||||
accept: 'application/activity+json,text/html;q=0.9,\\*/\\*;q=0.8'
|
||||
})
|
||||
}
|
||||
|
||||
function makeDeleteRequest (options: CommonRequestParams & {
|
||||
query?: any
|
||||
rawQuery?: string
|
||||
}) {
|
||||
const req = request(options.url).delete(options.path)
|
||||
|
||||
if (options.query) req.query(options.query)
|
||||
if (options.rawQuery) req.query(options.rawQuery)
|
||||
|
||||
return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
|
||||
}
|
||||
|
||||
function makeUploadRequest (options: CommonRequestParams & {
|
||||
method?: 'POST' | 'PUT'
|
||||
|
||||
fields: { [ fieldName: string ]: any }
|
||||
attaches?: { [ attachName: string ]: any | any[] }
|
||||
}) {
|
||||
let req = options.method === 'PUT'
|
||||
? request(options.url).put(options.path)
|
||||
: request(options.url).post(options.path)
|
||||
|
||||
req = buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
|
||||
|
||||
buildFields(req, options.fields)
|
||||
|
||||
Object.keys(options.attaches || {}).forEach(attach => {
|
||||
const value = options.attaches[attach]
|
||||
if (!value) return
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
req.attach(attach, buildAbsoluteFixturePath(value[0]), value[1])
|
||||
} else {
|
||||
req.attach(attach, buildAbsoluteFixturePath(value))
|
||||
}
|
||||
})
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
function makePostBodyRequest (options: CommonRequestParams & {
|
||||
fields?: { [ fieldName: string ]: any }
|
||||
}) {
|
||||
const req = request(options.url).post(options.path)
|
||||
.send(options.fields)
|
||||
|
||||
return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
|
||||
}
|
||||
|
||||
function makePutBodyRequest (options: {
|
||||
url: string
|
||||
path: string
|
||||
token?: string
|
||||
fields: { [ fieldName: string ]: any }
|
||||
expectedStatus?: HttpStatusCodeType
|
||||
headers?: { [name: string]: string }
|
||||
}) {
|
||||
const req = request(options.url).put(options.path)
|
||||
.send(options.fields)
|
||||
|
||||
return buildRequest(req, { accept: 'application/json', expectedStatus: HttpStatusCode.BAD_REQUEST_400, ...options })
|
||||
}
|
||||
|
||||
function decodeQueryString (path: string) {
|
||||
return decode(path.split('?')[1])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function unwrapBody <T> (test: request.Test): Promise<T> {
|
||||
return test.then(res => res.body)
|
||||
}
|
||||
|
||||
function unwrapText (test: request.Test): Promise<string> {
|
||||
return test.then(res => res.text)
|
||||
}
|
||||
|
||||
function unwrapBodyOrDecodeToJSON <T> (test: request.Test): Promise<T> {
|
||||
return test.then(res => {
|
||||
if (res.body instanceof Buffer) {
|
||||
try {
|
||||
return JSON.parse(new TextDecoder().decode(res.body))
|
||||
} catch (err) {
|
||||
console.error('Cannot decode JSON.', { res, body: res.body instanceof Buffer ? res.body.toString() : res.body })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
if (res.text) {
|
||||
try {
|
||||
return JSON.parse(res.text)
|
||||
} catch (err) {
|
||||
console.error('Cannot decode json', { res, text: res.text })
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return res.body
|
||||
})
|
||||
}
|
||||
|
||||
function unwrapTextOrDecode (test: request.Test): Promise<string> {
|
||||
return test.then(res => res.text || new TextDecoder().decode(res.body))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
makeHTMLRequest,
|
||||
makeGetRequest,
|
||||
decodeQueryString,
|
||||
makeUploadRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
makeDeleteRequest,
|
||||
makeRawRequest,
|
||||
makeActivityPubGetRequest,
|
||||
unwrapBody,
|
||||
unwrapTextOrDecode,
|
||||
unwrapBodyOrDecodeToJSON,
|
||||
unwrapText
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildRequest (req: request.Test, options: CommonRequestParams) {
|
||||
if (options.contentType) req.set('Accept', options.contentType)
|
||||
if (options.responseType) req.responseType(options.responseType)
|
||||
if (options.token) req.set('Authorization', 'Bearer ' + options.token)
|
||||
if (options.range) req.set('Range', options.range)
|
||||
if (options.accept) req.set('Accept', options.accept)
|
||||
if (options.host) req.set('Host', options.host)
|
||||
if (options.redirects) req.redirects(options.redirects)
|
||||
if (options.xForwardedFor) req.set('X-Forwarded-For', options.xForwardedFor)
|
||||
if (options.type) req.type(options.type)
|
||||
|
||||
Object.keys(options.headers || {}).forEach(name => {
|
||||
req.set(name, options.headers[name])
|
||||
})
|
||||
|
||||
return req.expect(res => {
|
||||
if (options.expectedStatus && res.status !== options.expectedStatus) {
|
||||
const err = new Error(`Expected status ${options.expectedStatus}, got ${res.status}. ` +
|
||||
`\nThe server responded: "${res.body?.error ?? res.text}".\n` +
|
||||
'You may take a closer look at the logs. To see how to do so, check out this page: ' +
|
||||
'https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/development/tests.md#debug-server-logs');
|
||||
|
||||
(err as any).res = res
|
||||
|
||||
throw err
|
||||
}
|
||||
|
||||
return res
|
||||
})
|
||||
}
|
||||
|
||||
function buildFields (req: request.Test, fields: { [ fieldName: string ]: any }, namespace?: string) {
|
||||
if (!fields) return
|
||||
|
||||
let formKey: string
|
||||
|
||||
for (const key of Object.keys(fields)) {
|
||||
if (namespace) formKey = `${namespace}[${key}]`
|
||||
else formKey = key
|
||||
|
||||
if (fields[key] === undefined) continue
|
||||
|
||||
if (Array.isArray(fields[key]) && fields[key].length === 0) {
|
||||
req.field(key, [])
|
||||
continue
|
||||
}
|
||||
|
||||
if (fields[key] !== null && typeof fields[key] === 'object') {
|
||||
buildFields(req, fields[key], formKey)
|
||||
} else {
|
||||
req.field(formKey, fields[key])
|
||||
}
|
||||
}
|
||||
}
|
3
packages/server-commands/src/runners/index.ts
Normal file
3
packages/server-commands/src/runners/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './runner-jobs-command.js'
|
||||
export * from './runner-registration-tokens-command.js'
|
||||
export * from './runners-command.js'
|
297
packages/server-commands/src/runners/runner-jobs-command.ts
Normal file
297
packages/server-commands/src/runners/runner-jobs-command.ts
Normal file
|
@ -0,0 +1,297 @@
|
|||
import { omit, pick, wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
AbortRunnerJobBody,
|
||||
AcceptRunnerJobBody,
|
||||
AcceptRunnerJobResult,
|
||||
ErrorRunnerJobBody,
|
||||
HttpStatusCode,
|
||||
isHLSTranscodingPayloadSuccess,
|
||||
isLiveRTMPHLSTranscodingUpdatePayload,
|
||||
isWebVideoOrAudioMergeTranscodingPayloadSuccess,
|
||||
ListRunnerJobsQuery,
|
||||
RequestRunnerJobBody,
|
||||
RequestRunnerJobResult,
|
||||
ResultList,
|
||||
RunnerJobAdmin,
|
||||
RunnerJobLiveRTMPHLSTranscodingPayload,
|
||||
RunnerJobPayload,
|
||||
RunnerJobState,
|
||||
RunnerJobStateType,
|
||||
RunnerJobSuccessBody,
|
||||
RunnerJobSuccessPayload,
|
||||
RunnerJobType,
|
||||
RunnerJobUpdateBody,
|
||||
RunnerJobVODPayload,
|
||||
VODHLSTranscodingSuccess,
|
||||
VODWebVideoTranscodingSuccess
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { waitJobs } from '../server/jobs.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class RunnerJobsCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & ListRunnerJobsQuery = {}) {
|
||||
const path = '/api/v1/runners/jobs'
|
||||
|
||||
return this.getRequestBody<ResultList<RunnerJobAdmin>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'search', 'stateOneOf' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
cancelByAdmin (options: OverrideCommandOptions & { jobUUID: string }) {
|
||||
const path = '/api/v1/runners/jobs/' + options.jobUUID + '/cancel'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
deleteByAdmin (options: OverrideCommandOptions & { jobUUID: string }) {
|
||||
const path = '/api/v1/runners/jobs/' + options.jobUUID
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
request (options: OverrideCommandOptions & RequestRunnerJobBody) {
|
||||
const path = '/api/v1/runners/jobs/request'
|
||||
|
||||
return unwrapBody<RequestRunnerJobResult>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'runnerToken' ]),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
async requestVOD (options: OverrideCommandOptions & RequestRunnerJobBody) {
|
||||
const vodTypes = new Set<RunnerJobType>([ 'vod-audio-merge-transcoding', 'vod-hls-transcoding', 'vod-web-video-transcoding' ])
|
||||
|
||||
const { availableJobs } = await this.request(options)
|
||||
|
||||
return {
|
||||
availableJobs: availableJobs.filter(j => vodTypes.has(j.type))
|
||||
} as RequestRunnerJobResult<RunnerJobVODPayload>
|
||||
}
|
||||
|
||||
async requestLive (options: OverrideCommandOptions & RequestRunnerJobBody) {
|
||||
const vodTypes = new Set<RunnerJobType>([ 'live-rtmp-hls-transcoding' ])
|
||||
|
||||
const { availableJobs } = await this.request(options)
|
||||
|
||||
return {
|
||||
availableJobs: availableJobs.filter(j => vodTypes.has(j.type))
|
||||
} as RequestRunnerJobResult<RunnerJobLiveRTMPHLSTranscodingPayload>
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
accept <T extends RunnerJobPayload = RunnerJobPayload> (options: OverrideCommandOptions & AcceptRunnerJobBody & { jobUUID: string }) {
|
||||
const path = '/api/v1/runners/jobs/' + options.jobUUID + '/accept'
|
||||
|
||||
return unwrapBody<AcceptRunnerJobResult<T>>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'runnerToken' ]),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
abort (options: OverrideCommandOptions & AbortRunnerJobBody & { jobUUID: string }) {
|
||||
const path = '/api/v1/runners/jobs/' + options.jobUUID + '/abort'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'reason', 'jobToken', 'runnerToken' ]),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & RunnerJobUpdateBody & { jobUUID: string }) {
|
||||
const path = '/api/v1/runners/jobs/' + options.jobUUID + '/update'
|
||||
|
||||
const { payload } = options
|
||||
const attaches: { [id: string]: any } = {}
|
||||
let payloadWithoutFiles = payload
|
||||
|
||||
if (isLiveRTMPHLSTranscodingUpdatePayload(payload)) {
|
||||
if (payload.masterPlaylistFile) {
|
||||
attaches[`payload[masterPlaylistFile]`] = payload.masterPlaylistFile
|
||||
}
|
||||
|
||||
attaches[`payload[resolutionPlaylistFile]`] = payload.resolutionPlaylistFile
|
||||
attaches[`payload[videoChunkFile]`] = payload.videoChunkFile
|
||||
|
||||
payloadWithoutFiles = omit(payloadWithoutFiles, [ 'masterPlaylistFile', 'resolutionPlaylistFile', 'videoChunkFile' ])
|
||||
}
|
||||
|
||||
return this.postUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {
|
||||
...pick(options, [ 'progress', 'jobToken', 'runnerToken' ]),
|
||||
|
||||
payload: payloadWithoutFiles
|
||||
},
|
||||
attaches,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
error (options: OverrideCommandOptions & ErrorRunnerJobBody & { jobUUID: string }) {
|
||||
const path = '/api/v1/runners/jobs/' + options.jobUUID + '/error'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'message', 'jobToken', 'runnerToken' ]),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
success (options: OverrideCommandOptions & RunnerJobSuccessBody & { jobUUID: string }) {
|
||||
const { payload } = options
|
||||
|
||||
const path = '/api/v1/runners/jobs/' + options.jobUUID + '/success'
|
||||
const attaches: { [id: string]: any } = {}
|
||||
let payloadWithoutFiles = payload
|
||||
|
||||
if ((isWebVideoOrAudioMergeTranscodingPayloadSuccess(payload) || isHLSTranscodingPayloadSuccess(payload)) && payload.videoFile) {
|
||||
attaches[`payload[videoFile]`] = payload.videoFile
|
||||
|
||||
payloadWithoutFiles = omit(payloadWithoutFiles as VODWebVideoTranscodingSuccess, [ 'videoFile' ])
|
||||
}
|
||||
|
||||
if (isHLSTranscodingPayloadSuccess(payload) && payload.resolutionPlaylistFile) {
|
||||
attaches[`payload[resolutionPlaylistFile]`] = payload.resolutionPlaylistFile
|
||||
|
||||
payloadWithoutFiles = omit(payloadWithoutFiles as VODHLSTranscodingSuccess, [ 'resolutionPlaylistFile' ])
|
||||
}
|
||||
|
||||
return this.postUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
attaches,
|
||||
fields: {
|
||||
...pick(options, [ 'jobToken', 'runnerToken' ]),
|
||||
|
||||
payload: payloadWithoutFiles
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
getJobFile (options: OverrideCommandOptions & { url: string, jobToken: string, runnerToken: string }) {
|
||||
const { host, protocol, pathname } = new URL(options.url)
|
||||
|
||||
return this.postBodyRequest({
|
||||
url: `${protocol}//${host}`,
|
||||
path: pathname,
|
||||
|
||||
fields: pick(options, [ 'jobToken', 'runnerToken' ]),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async autoAccept (options: OverrideCommandOptions & RequestRunnerJobBody & { type?: RunnerJobType }) {
|
||||
const { availableJobs } = await this.request(options)
|
||||
|
||||
const job = options.type
|
||||
? availableJobs.find(j => j.type === options.type)
|
||||
: availableJobs[0]
|
||||
|
||||
return this.accept({ ...options, jobUUID: job.uuid })
|
||||
}
|
||||
|
||||
async autoProcessWebVideoJob (runnerToken: string, jobUUIDToProcess?: string) {
|
||||
let jobUUID = jobUUIDToProcess
|
||||
|
||||
if (!jobUUID) {
|
||||
const { availableJobs } = await this.request({ runnerToken })
|
||||
jobUUID = availableJobs[0].uuid
|
||||
}
|
||||
|
||||
const { job } = await this.accept({ runnerToken, jobUUID })
|
||||
const jobToken = job.jobToken
|
||||
|
||||
const payload: RunnerJobSuccessPayload = { videoFile: 'video_short.mp4' }
|
||||
await this.success({ runnerToken, jobUUID, jobToken, payload })
|
||||
|
||||
await waitJobs([ this.server ])
|
||||
|
||||
return job
|
||||
}
|
||||
|
||||
async cancelAllJobs (options: { state?: RunnerJobStateType } = {}) {
|
||||
const { state } = options
|
||||
|
||||
const { data } = await this.list({ count: 100 })
|
||||
|
||||
const allowedStates = new Set<RunnerJobStateType>([
|
||||
RunnerJobState.PENDING,
|
||||
RunnerJobState.PROCESSING,
|
||||
RunnerJobState.WAITING_FOR_PARENT_JOB
|
||||
])
|
||||
|
||||
for (const job of data) {
|
||||
if (state && job.state.id !== state) continue
|
||||
else if (allowedStates.has(job.state.id) !== true) continue
|
||||
|
||||
await this.cancelByAdmin({ jobUUID: job.uuid })
|
||||
}
|
||||
}
|
||||
|
||||
async getJob (options: OverrideCommandOptions & { uuid: string }) {
|
||||
const { data } = await this.list({ ...options, count: 100, sort: '-updatedAt' })
|
||||
|
||||
return data.find(j => j.uuid === options.uuid)
|
||||
}
|
||||
|
||||
async requestLiveJob (runnerToken: string) {
|
||||
let availableJobs: RequestRunnerJobResult<RunnerJobLiveRTMPHLSTranscodingPayload>['availableJobs'] = []
|
||||
|
||||
while (availableJobs.length === 0) {
|
||||
const result = await this.requestLive({ runnerToken })
|
||||
availableJobs = result.availableJobs
|
||||
|
||||
if (availableJobs.length === 1) break
|
||||
|
||||
await wait(150)
|
||||
}
|
||||
|
||||
return availableJobs[0]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ResultList, RunnerRegistrationToken } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class RunnerRegistrationTokensCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
} = {}) {
|
||||
const path = '/api/v1/runners/registration-tokens'
|
||||
|
||||
return this.getRequestBody<ResultList<RunnerRegistrationToken>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
generate (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/runners/registration-tokens/generate'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
id: number
|
||||
}) {
|
||||
const path = '/api/v1/runners/registration-tokens/' + options.id
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async getFirstRegistrationToken (options: OverrideCommandOptions = {}) {
|
||||
const { data } = await this.list(options)
|
||||
|
||||
return data[0].registrationToken
|
||||
}
|
||||
}
|
85
packages/server-commands/src/runners/runners-command.ts
Normal file
85
packages/server-commands/src/runners/runners-command.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
RegisterRunnerBody,
|
||||
RegisterRunnerResult,
|
||||
ResultList,
|
||||
Runner,
|
||||
UnregisterRunnerBody
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class RunnersCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
} = {}) {
|
||||
const path = '/api/v1/runners'
|
||||
|
||||
return this.getRequestBody<ResultList<Runner>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
register (options: OverrideCommandOptions & RegisterRunnerBody) {
|
||||
const path = '/api/v1/runners/register'
|
||||
|
||||
return unwrapBody<RegisterRunnerResult>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'name', 'registrationToken', 'description' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
unregister (options: OverrideCommandOptions & UnregisterRunnerBody) {
|
||||
const path = '/api/v1/runners/unregister'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'runnerToken' ]),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
id: number
|
||||
}) {
|
||||
const path = '/api/v1/runners/' + options.id
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async autoRegisterRunner () {
|
||||
const { data } = await this.server.runnerRegistrationTokens.list({ sort: 'createdAt' })
|
||||
|
||||
const { runnerToken } = await this.register({
|
||||
name: 'runner ' + buildUUID(),
|
||||
registrationToken: data[0].registrationToken
|
||||
})
|
||||
|
||||
return runnerToken
|
||||
}
|
||||
}
|
1
packages/server-commands/src/search/index.ts
Normal file
1
packages/server-commands/src/search/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './search-command.js'
|
98
packages/server-commands/src/search/search-command.ts
Normal file
98
packages/server-commands/src/search/search-command.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import {
|
||||
HttpStatusCode,
|
||||
ResultList,
|
||||
Video,
|
||||
VideoChannel,
|
||||
VideoChannelsSearchQuery,
|
||||
VideoPlaylist,
|
||||
VideoPlaylistsSearchQuery,
|
||||
VideosSearchQuery
|
||||
} from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class SearchCommand extends AbstractCommand {
|
||||
|
||||
searchChannels (options: OverrideCommandOptions & {
|
||||
search: string
|
||||
}) {
|
||||
return this.advancedChannelSearch({
|
||||
...options,
|
||||
|
||||
search: { search: options.search }
|
||||
})
|
||||
}
|
||||
|
||||
advancedChannelSearch (options: OverrideCommandOptions & {
|
||||
search: VideoChannelsSearchQuery
|
||||
}) {
|
||||
const { search } = options
|
||||
const path = '/api/v1/search/video-channels'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoChannel>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: search,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
searchPlaylists (options: OverrideCommandOptions & {
|
||||
search: string
|
||||
}) {
|
||||
return this.advancedPlaylistSearch({
|
||||
...options,
|
||||
|
||||
search: { search: options.search }
|
||||
})
|
||||
}
|
||||
|
||||
advancedPlaylistSearch (options: OverrideCommandOptions & {
|
||||
search: VideoPlaylistsSearchQuery
|
||||
}) {
|
||||
const { search } = options
|
||||
const path = '/api/v1/search/video-playlists'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoPlaylist>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: search,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
searchVideos (options: OverrideCommandOptions & {
|
||||
search: string
|
||||
sort?: string
|
||||
}) {
|
||||
const { search, sort } = options
|
||||
|
||||
return this.advancedVideoSearch({
|
||||
...options,
|
||||
|
||||
search: {
|
||||
search,
|
||||
sort: sort ?? '-publishedAt'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
advancedVideoSearch (options: OverrideCommandOptions & {
|
||||
search: VideosSearchQuery
|
||||
}) {
|
||||
const { search } = options
|
||||
const path = '/api/v1/search/videos'
|
||||
|
||||
return this.getRequestBody<ResultList<Video>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: search,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
576
packages/server-commands/src/server/config-command.ts
Normal file
576
packages/server-commands/src/server/config-command.ts
Normal file
|
@ -0,0 +1,576 @@
|
|||
import merge from 'lodash-es/merge.js'
|
||||
import { About, CustomConfig, HttpStatusCode, ServerConfig } from '@peertube/peertube-models'
|
||||
import { DeepPartial } from '@peertube/peertube-typescript-utils'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/abstract-command.js'
|
||||
|
||||
export class ConfigCommand extends AbstractCommand {
|
||||
|
||||
static getCustomConfigResolutions (enabled: boolean, with0p = false) {
|
||||
return {
|
||||
'0p': enabled && with0p,
|
||||
'144p': enabled,
|
||||
'240p': enabled,
|
||||
'360p': enabled,
|
||||
'480p': enabled,
|
||||
'720p': enabled,
|
||||
'1080p': enabled,
|
||||
'1440p': enabled,
|
||||
'2160p': enabled
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static getEmailOverrideConfig (emailPort: number) {
|
||||
return {
|
||||
smtp: {
|
||||
hostname: '127.0.0.1',
|
||||
port: emailPort
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enableSignup (requiresApproval: boolean, limit = -1) {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
signup: {
|
||||
enabled: true,
|
||||
requiresApproval,
|
||||
limit
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
disableImports () {
|
||||
return this.setImportsEnabled(false)
|
||||
}
|
||||
|
||||
enableImports () {
|
||||
return this.setImportsEnabled(true)
|
||||
}
|
||||
|
||||
private setImportsEnabled (enabled: boolean) {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
import: {
|
||||
videos: {
|
||||
http: {
|
||||
enabled
|
||||
},
|
||||
|
||||
torrent: {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
disableFileUpdate () {
|
||||
return this.setFileUpdateEnabled(false)
|
||||
}
|
||||
|
||||
enableFileUpdate () {
|
||||
return this.setFileUpdateEnabled(true)
|
||||
}
|
||||
|
||||
private setFileUpdateEnabled (enabled: boolean) {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
videoFile: {
|
||||
update: {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enableChannelSync () {
|
||||
return this.setChannelSyncEnabled(true)
|
||||
}
|
||||
|
||||
disableChannelSync () {
|
||||
return this.setChannelSyncEnabled(false)
|
||||
}
|
||||
|
||||
private setChannelSyncEnabled (enabled: boolean) {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
import: {
|
||||
videoChannelSynchronization: {
|
||||
enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enableLive (options: {
|
||||
allowReplay?: boolean
|
||||
transcoding?: boolean
|
||||
resolutions?: 'min' | 'max' // Default max
|
||||
} = {}) {
|
||||
const { allowReplay, transcoding, resolutions = 'max' } = options
|
||||
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: allowReplay ?? true,
|
||||
transcoding: {
|
||||
enabled: transcoding ?? true,
|
||||
resolutions: ConfigCommand.getCustomConfigResolutions(resolutions === 'max')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
disableTranscoding () {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
transcoding: {
|
||||
enabled: false
|
||||
},
|
||||
videoStudio: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enableTranscoding (options: {
|
||||
webVideo?: boolean // default true
|
||||
hls?: boolean // default true
|
||||
with0p?: boolean // default false
|
||||
} = {}) {
|
||||
const { webVideo = true, hls = true, with0p = false } = options
|
||||
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
|
||||
allowAudioFiles: true,
|
||||
allowAdditionalExtensions: true,
|
||||
|
||||
resolutions: ConfigCommand.getCustomConfigResolutions(true, with0p),
|
||||
|
||||
webVideos: {
|
||||
enabled: webVideo
|
||||
},
|
||||
hls: {
|
||||
enabled: hls
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enableMinimumTranscoding (options: {
|
||||
webVideo?: boolean // default true
|
||||
hls?: boolean // default true
|
||||
} = {}) {
|
||||
const { webVideo = true, hls = true } = options
|
||||
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
|
||||
allowAudioFiles: true,
|
||||
allowAdditionalExtensions: true,
|
||||
|
||||
resolutions: {
|
||||
...ConfigCommand.getCustomConfigResolutions(false),
|
||||
|
||||
'240p': true
|
||||
},
|
||||
|
||||
webVideos: {
|
||||
enabled: webVideo
|
||||
},
|
||||
hls: {
|
||||
enabled: hls
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enableRemoteTranscoding () {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
transcoding: {
|
||||
remoteRunners: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
live: {
|
||||
transcoding: {
|
||||
remoteRunners: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
enableRemoteStudio () {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
videoStudio: {
|
||||
remoteRunners: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enableStudio () {
|
||||
return this.updateExistingSubConfig({
|
||||
newConfig: {
|
||||
videoStudio: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getConfig (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config'
|
||||
|
||||
return this.getRequestBody<ServerConfig>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async getIndexHTMLConfig (options: OverrideCommandOptions = {}) {
|
||||
const text = await this.getRequestText({
|
||||
...options,
|
||||
|
||||
path: '/',
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
const match = text.match('<script type="application/javascript">window.PeerTubeServerConfig = (".+?")</script>')
|
||||
|
||||
// We parse the string twice, first to extract the string and then to extract the JSON
|
||||
return JSON.parse(JSON.parse(match[1])) as ServerConfig
|
||||
}
|
||||
|
||||
getAbout (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config/about'
|
||||
|
||||
return this.getRequestBody<About>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getCustomConfig (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config/custom'
|
||||
|
||||
return this.getRequestBody<CustomConfig>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
updateCustomConfig (options: OverrideCommandOptions & {
|
||||
newCustomConfig: CustomConfig
|
||||
}) {
|
||||
const path = '/api/v1/config/custom'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.newCustomConfig,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
deleteCustomConfig (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/config/custom'
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async updateExistingSubConfig (options: OverrideCommandOptions & {
|
||||
newConfig: DeepPartial<CustomConfig>
|
||||
}) {
|
||||
const existing = await this.getCustomConfig({ ...options, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) })
|
||||
}
|
||||
|
||||
updateCustomSubConfig (options: OverrideCommandOptions & {
|
||||
newConfig: DeepPartial<CustomConfig>
|
||||
}) {
|
||||
const newCustomConfig: CustomConfig = {
|
||||
instance: {
|
||||
name: 'PeerTube updated',
|
||||
shortDescription: 'my short description',
|
||||
description: 'my super description',
|
||||
terms: 'my super terms',
|
||||
codeOfConduct: 'my super coc',
|
||||
|
||||
creationReason: 'my super creation reason',
|
||||
moderationInformation: 'my super moderation information',
|
||||
administrator: 'Kuja',
|
||||
maintenanceLifetime: 'forever',
|
||||
businessModel: 'my super business model',
|
||||
hardwareInformation: '2vCore 3GB RAM',
|
||||
|
||||
languages: [ 'en', 'es' ],
|
||||
categories: [ 1, 2 ],
|
||||
|
||||
isNSFW: true,
|
||||
defaultNSFWPolicy: 'blur',
|
||||
|
||||
defaultClientRoute: '/videos/recently-added',
|
||||
|
||||
customizations: {
|
||||
javascript: 'alert("coucou")',
|
||||
css: 'body { background-color: red; }'
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
default: 'default'
|
||||
},
|
||||
services: {
|
||||
twitter: {
|
||||
username: '@MySuperUsername',
|
||||
whitelisted: true
|
||||
}
|
||||
},
|
||||
client: {
|
||||
videos: {
|
||||
miniature: {
|
||||
preferAuthorDisplayName: false
|
||||
}
|
||||
},
|
||||
menu: {
|
||||
login: {
|
||||
redirectOnSingleExternalAuth: false
|
||||
}
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: 2
|
||||
},
|
||||
captions: {
|
||||
size: 3
|
||||
},
|
||||
torrents: {
|
||||
size: 4
|
||||
},
|
||||
storyboards: {
|
||||
size: 5
|
||||
}
|
||||
},
|
||||
signup: {
|
||||
enabled: false,
|
||||
limit: 5,
|
||||
requiresApproval: true,
|
||||
requiresEmailVerification: false,
|
||||
minimumAge: 16
|
||||
},
|
||||
admin: {
|
||||
email: 'superadmin1@example.com'
|
||||
},
|
||||
contactForm: {
|
||||
enabled: true
|
||||
},
|
||||
user: {
|
||||
history: {
|
||||
videos: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
videoQuota: 5242881,
|
||||
videoQuotaDaily: 318742
|
||||
},
|
||||
videoChannels: {
|
||||
maxPerUser: 20
|
||||
},
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
remoteRunners: {
|
||||
enabled: false
|
||||
},
|
||||
allowAdditionalExtensions: true,
|
||||
allowAudioFiles: true,
|
||||
threads: 1,
|
||||
concurrency: 3,
|
||||
profile: 'default',
|
||||
resolutions: {
|
||||
'0p': false,
|
||||
'144p': false,
|
||||
'240p': false,
|
||||
'360p': true,
|
||||
'480p': true,
|
||||
'720p': false,
|
||||
'1080p': false,
|
||||
'1440p': false,
|
||||
'2160p': false
|
||||
},
|
||||
alwaysTranscodeOriginalResolution: true,
|
||||
webVideos: {
|
||||
enabled: true
|
||||
},
|
||||
hls: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
live: {
|
||||
enabled: true,
|
||||
allowReplay: false,
|
||||
latencySetting: {
|
||||
enabled: false
|
||||
},
|
||||
maxDuration: -1,
|
||||
maxInstanceLives: -1,
|
||||
maxUserLives: 50,
|
||||
transcoding: {
|
||||
enabled: true,
|
||||
remoteRunners: {
|
||||
enabled: false
|
||||
},
|
||||
threads: 4,
|
||||
profile: 'default',
|
||||
resolutions: {
|
||||
'144p': true,
|
||||
'240p': true,
|
||||
'360p': true,
|
||||
'480p': true,
|
||||
'720p': true,
|
||||
'1080p': true,
|
||||
'1440p': true,
|
||||
'2160p': true
|
||||
},
|
||||
alwaysTranscodeOriginalResolution: true
|
||||
}
|
||||
},
|
||||
videoStudio: {
|
||||
enabled: false,
|
||||
remoteRunners: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
videoFile: {
|
||||
update: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
import: {
|
||||
videos: {
|
||||
concurrency: 3,
|
||||
http: {
|
||||
enabled: false
|
||||
},
|
||||
torrent: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
videoChannelSynchronization: {
|
||||
enabled: false,
|
||||
maxPerUser: 10
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
videos: {
|
||||
algorithms: {
|
||||
enabled: [ 'hot', 'most-viewed', 'most-liked' ],
|
||||
default: 'hot'
|
||||
}
|
||||
}
|
||||
},
|
||||
autoBlacklist: {
|
||||
videos: {
|
||||
ofUsers: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
followers: {
|
||||
instance: {
|
||||
enabled: true,
|
||||
manualApproval: false
|
||||
}
|
||||
},
|
||||
followings: {
|
||||
instance: {
|
||||
autoFollowBack: {
|
||||
enabled: false
|
||||
},
|
||||
autoFollowIndex: {
|
||||
indexUrl: 'https://instances.joinpeertube.org/api/v1/instances/hosts',
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
broadcastMessage: {
|
||||
enabled: true,
|
||||
level: 'warning',
|
||||
message: 'hello',
|
||||
dismissable: true
|
||||
},
|
||||
search: {
|
||||
remoteUri: {
|
||||
users: true,
|
||||
anonymous: true
|
||||
},
|
||||
searchIndex: {
|
||||
enabled: true,
|
||||
url: 'https://search.joinpeertube.org',
|
||||
disableLocalSearch: true,
|
||||
isDefaultSearch: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
merge(newCustomConfig, options.newConfig)
|
||||
|
||||
return this.updateCustomConfig({ ...options, newCustomConfig })
|
||||
}
|
||||
}
|
30
packages/server-commands/src/server/contact-form-command.ts
Normal file
30
packages/server-commands/src/server/contact-form-command.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { ContactForm, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class ContactFormCommand extends AbstractCommand {
|
||||
|
||||
send (options: OverrideCommandOptions & {
|
||||
fromEmail: string
|
||||
fromName: string
|
||||
subject: string
|
||||
body: string
|
||||
}) {
|
||||
const path = '/api/v1/server/contact'
|
||||
|
||||
const body: ContactForm = {
|
||||
fromEmail: options.fromEmail,
|
||||
fromName: options.fromName,
|
||||
subject: options.subject,
|
||||
body: options.body
|
||||
}
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: body,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
33
packages/server-commands/src/server/debug-command.ts
Normal file
33
packages/server-commands/src/server/debug-command.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { Debug, HttpStatusCode, SendDebugCommand } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class DebugCommand extends AbstractCommand {
|
||||
|
||||
getDebug (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/server/debug'
|
||||
|
||||
return this.getRequestBody<Debug>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
sendCommand (options: OverrideCommandOptions & {
|
||||
body: SendDebugCommand
|
||||
}) {
|
||||
const { body } = options
|
||||
const path = '/api/v1/server/debug/run-command'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: body,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
139
packages/server-commands/src/server/follows-command.ts
Normal file
139
packages/server-commands/src/server/follows-command.ts
Normal file
|
@ -0,0 +1,139 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
import { PeerTubeServer } from './server.js'
|
||||
|
||||
export class FollowsCommand extends AbstractCommand {
|
||||
|
||||
getFollowers (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
actorType?: ActivityPubActorType
|
||||
state?: FollowState
|
||||
} = {}) {
|
||||
const path = '/api/v1/server/followers'
|
||||
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<ActorFollow>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getFollowings (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
actorType?: ActivityPubActorType
|
||||
state?: FollowState
|
||||
} = {}) {
|
||||
const path = '/api/v1/server/following'
|
||||
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'state', 'actorType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<ActorFollow>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
follow (options: OverrideCommandOptions & {
|
||||
hosts?: string[]
|
||||
handles?: string[]
|
||||
}) {
|
||||
const path = '/api/v1/server/following'
|
||||
|
||||
const fields: ServerFollowCreate = {}
|
||||
|
||||
if (options.hosts) {
|
||||
fields.hosts = options.hosts.map(f => f.replace(/^http:\/\//, ''))
|
||||
}
|
||||
|
||||
if (options.handles) {
|
||||
fields.handles = options.handles
|
||||
}
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async unfollow (options: OverrideCommandOptions & {
|
||||
target: PeerTubeServer | string
|
||||
}) {
|
||||
const { target } = options
|
||||
|
||||
const handle = typeof target === 'string'
|
||||
? target
|
||||
: target.host
|
||||
|
||||
const path = '/api/v1/server/following/' + handle
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
acceptFollower (options: OverrideCommandOptions & {
|
||||
follower: string
|
||||
}) {
|
||||
const path = '/api/v1/server/followers/' + options.follower + '/accept'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
rejectFollower (options: OverrideCommandOptions & {
|
||||
follower: string
|
||||
}) {
|
||||
const path = '/api/v1/server/followers/' + options.follower + '/reject'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeFollower (options: OverrideCommandOptions & {
|
||||
follower: PeerTubeServer
|
||||
}) {
|
||||
const path = '/api/v1/server/followers/peertube@' + options.follower.host
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
20
packages/server-commands/src/server/follows.ts
Normal file
20
packages/server-commands/src/server/follows.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { waitJobs } from './jobs.js'
|
||||
import { PeerTubeServer } from './server.js'
|
||||
|
||||
async function doubleFollow (server1: PeerTubeServer, server2: PeerTubeServer) {
|
||||
await Promise.all([
|
||||
server1.follows.follow({ hosts: [ server2.url ] }),
|
||||
server2.follows.follow({ hosts: [ server1.url ] })
|
||||
])
|
||||
|
||||
// Wait request propagation
|
||||
await waitJobs([ server1, server2 ])
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doubleFollow
|
||||
}
|
15
packages/server-commands/src/server/index.ts
Normal file
15
packages/server-commands/src/server/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export * from './config-command.js'
|
||||
export * from './contact-form-command.js'
|
||||
export * from './debug-command.js'
|
||||
export * from './follows-command.js'
|
||||
export * from './follows.js'
|
||||
export * from './jobs.js'
|
||||
export * from './jobs-command.js'
|
||||
export * from './metrics-command.js'
|
||||
export * from './object-storage-command.js'
|
||||
export * from './plugins-command.js'
|
||||
export * from './redundancy-command.js'
|
||||
export * from './server.js'
|
||||
export * from './servers-command.js'
|
||||
export * from './servers.js'
|
||||
export * from './stats-command.js'
|
84
packages/server-commands/src/server/jobs-command.ts
Normal file
84
packages/server-commands/src/server/jobs-command.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, Job, JobState, JobType, ResultList } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class JobsCommand extends AbstractCommand {
|
||||
|
||||
async getLatest (options: OverrideCommandOptions & {
|
||||
jobType: JobType
|
||||
}) {
|
||||
const { data } = await this.list({ ...options, start: 0, count: 1, sort: '-createdAt' })
|
||||
|
||||
if (data.length === 0) return undefined
|
||||
|
||||
return data[0]
|
||||
}
|
||||
|
||||
pauseJobQueue (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/jobs/pause'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
resumeJobQueue (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/jobs/resume'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
state?: JobState
|
||||
jobType?: JobType
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
} = {}) {
|
||||
const path = this.buildJobsUrl(options.state)
|
||||
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'jobType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<Job>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listFailed (options: OverrideCommandOptions & {
|
||||
jobType?: JobType
|
||||
}) {
|
||||
const path = this.buildJobsUrl('failed')
|
||||
|
||||
return this.getRequestBody<ResultList<Job>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { start: 0, count: 50 },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
private buildJobsUrl (state?: JobState) {
|
||||
let path = '/api/v1/jobs'
|
||||
|
||||
if (state) path += '/' + state
|
||||
|
||||
return path
|
||||
}
|
||||
}
|
117
packages/server-commands/src/server/jobs.ts
Normal file
117
packages/server-commands/src/server/jobs.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
import { expect } from 'chai'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { JobState, JobType, RunnerJobState } from '@peertube/peertube-models'
|
||||
import { PeerTubeServer } from './server.js'
|
||||
|
||||
async function waitJobs (
|
||||
serversArg: PeerTubeServer[] | PeerTubeServer,
|
||||
options: {
|
||||
skipDelayed?: boolean // default false
|
||||
runnerJobs?: boolean // default false
|
||||
} = {}
|
||||
) {
|
||||
const { skipDelayed = false, runnerJobs = false } = options
|
||||
|
||||
const pendingJobWait = process.env.NODE_PENDING_JOB_WAIT
|
||||
? parseInt(process.env.NODE_PENDING_JOB_WAIT, 10)
|
||||
: 250
|
||||
|
||||
let servers: PeerTubeServer[]
|
||||
|
||||
if (Array.isArray(serversArg) === false) servers = [ serversArg as PeerTubeServer ]
|
||||
else servers = serversArg as PeerTubeServer[]
|
||||
|
||||
const states: JobState[] = [ 'waiting', 'active' ]
|
||||
if (!skipDelayed) states.push('delayed')
|
||||
|
||||
const repeatableJobs: JobType[] = [ 'videos-views-stats', 'activitypub-cleaner' ]
|
||||
let pendingRequests: boolean
|
||||
|
||||
function tasksBuilder () {
|
||||
const tasks: Promise<any>[] = []
|
||||
|
||||
// Check if each server has pending request
|
||||
for (const server of servers) {
|
||||
if (process.env.DEBUG) console.log('Checking ' + server.url)
|
||||
|
||||
for (const state of states) {
|
||||
|
||||
const jobPromise = server.jobs.list({
|
||||
state,
|
||||
start: 0,
|
||||
count: 10,
|
||||
sort: '-createdAt'
|
||||
}).then(body => body.data)
|
||||
.then(jobs => jobs.filter(j => !repeatableJobs.includes(j.type)))
|
||||
.then(jobs => {
|
||||
if (jobs.length !== 0) {
|
||||
pendingRequests = true
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.log(jobs)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
tasks.push(jobPromise)
|
||||
}
|
||||
|
||||
const debugPromise = server.debug.getDebug()
|
||||
.then(obj => {
|
||||
if (obj.activityPubMessagesWaiting !== 0) {
|
||||
pendingRequests = true
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.log('AP messages waiting: ' + obj.activityPubMessagesWaiting)
|
||||
}
|
||||
}
|
||||
})
|
||||
tasks.push(debugPromise)
|
||||
|
||||
if (runnerJobs) {
|
||||
const runnerJobsPromise = server.runnerJobs.list({ count: 100 })
|
||||
.then(({ data }) => {
|
||||
for (const job of data) {
|
||||
if (job.state.id !== RunnerJobState.COMPLETED) {
|
||||
pendingRequests = true
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.log(job)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
tasks.push(runnerJobsPromise)
|
||||
}
|
||||
}
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
do {
|
||||
pendingRequests = false
|
||||
await Promise.all(tasksBuilder())
|
||||
|
||||
// Retry, in case of new jobs were created
|
||||
if (pendingRequests === false) {
|
||||
await wait(pendingJobWait)
|
||||
await Promise.all(tasksBuilder())
|
||||
}
|
||||
|
||||
if (pendingRequests) {
|
||||
await wait(pendingJobWait)
|
||||
}
|
||||
} while (pendingRequests)
|
||||
}
|
||||
|
||||
async function expectNoFailedTranscodingJob (server: PeerTubeServer) {
|
||||
const { data } = await server.jobs.listFailed({ jobType: 'video-transcoding' })
|
||||
expect(data).to.have.lengthOf(0)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
waitJobs,
|
||||
expectNoFailedTranscodingJob
|
||||
}
|
18
packages/server-commands/src/server/metrics-command.ts
Normal file
18
packages/server-commands/src/server/metrics-command.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { HttpStatusCode, PlaybackMetricCreate } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class MetricsCommand extends AbstractCommand {
|
||||
|
||||
addPlaybackMetric (options: OverrideCommandOptions & { metrics: PlaybackMetricCreate }) {
|
||||
const path = '/api/v1/metrics/playback'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.metrics,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
165
packages/server-commands/src/server/object-storage-command.ts
Normal file
165
packages/server-commands/src/server/object-storage-command.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
import { randomInt } from 'crypto'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { makePostBodyRequest } from '../requests/index.js'
|
||||
|
||||
export class ObjectStorageCommand {
|
||||
static readonly DEFAULT_SCALEWAY_BUCKET = 'peertube-ci-test'
|
||||
|
||||
private readonly bucketsCreated: string[] = []
|
||||
private readonly seed: number
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
constructor () {
|
||||
this.seed = randomInt(0, 10000)
|
||||
}
|
||||
|
||||
static getMockCredentialsConfig () {
|
||||
return {
|
||||
access_key_id: 'AKIAIOSFODNN7EXAMPLE',
|
||||
secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
|
||||
}
|
||||
}
|
||||
|
||||
static getMockEndpointHost () {
|
||||
return 'localhost:9444'
|
||||
}
|
||||
|
||||
static getMockRegion () {
|
||||
return 'us-east-1'
|
||||
}
|
||||
|
||||
getDefaultMockConfig () {
|
||||
return {
|
||||
object_storage: {
|
||||
enabled: true,
|
||||
endpoint: 'http://' + ObjectStorageCommand.getMockEndpointHost(),
|
||||
region: ObjectStorageCommand.getMockRegion(),
|
||||
|
||||
credentials: ObjectStorageCommand.getMockCredentialsConfig(),
|
||||
|
||||
streaming_playlists: {
|
||||
bucket_name: this.getMockStreamingPlaylistsBucketName()
|
||||
},
|
||||
|
||||
web_videos: {
|
||||
bucket_name: this.getMockWebVideosBucketName()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getMockWebVideosBaseUrl () {
|
||||
return `http://${this.getMockWebVideosBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
|
||||
}
|
||||
|
||||
getMockPlaylistBaseUrl () {
|
||||
return `http://${this.getMockStreamingPlaylistsBucketName()}.${ObjectStorageCommand.getMockEndpointHost()}/`
|
||||
}
|
||||
|
||||
async prepareDefaultMockBuckets () {
|
||||
await this.createMockBucket(this.getMockStreamingPlaylistsBucketName())
|
||||
await this.createMockBucket(this.getMockWebVideosBucketName())
|
||||
}
|
||||
|
||||
async createMockBucket (name: string) {
|
||||
this.bucketsCreated.push(name)
|
||||
|
||||
await this.deleteMockBucket(name)
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: ObjectStorageCommand.getMockEndpointHost(),
|
||||
path: '/ui/' + name + '?create',
|
||||
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
|
||||
})
|
||||
|
||||
await makePostBodyRequest({
|
||||
url: ObjectStorageCommand.getMockEndpointHost(),
|
||||
path: '/ui/' + name + '?make-public',
|
||||
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupMock () {
|
||||
for (const name of this.bucketsCreated) {
|
||||
await this.deleteMockBucket(name)
|
||||
}
|
||||
}
|
||||
|
||||
getMockStreamingPlaylistsBucketName (name = 'streaming-playlists') {
|
||||
return this.getMockBucketName(name)
|
||||
}
|
||||
|
||||
getMockWebVideosBucketName (name = 'web-videos') {
|
||||
return this.getMockBucketName(name)
|
||||
}
|
||||
|
||||
getMockBucketName (name: string) {
|
||||
return `${this.seed}-${name}`
|
||||
}
|
||||
|
||||
private async deleteMockBucket (name: string) {
|
||||
await makePostBodyRequest({
|
||||
url: ObjectStorageCommand.getMockEndpointHost(),
|
||||
path: '/ui/' + name + '?delete',
|
||||
expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static getDefaultScalewayConfig (options: {
|
||||
serverNumber: number
|
||||
enablePrivateProxy?: boolean // default true
|
||||
privateACL?: 'private' | 'public-read' // default 'private'
|
||||
}) {
|
||||
const { serverNumber, enablePrivateProxy = true, privateACL = 'private' } = options
|
||||
|
||||
return {
|
||||
object_storage: {
|
||||
enabled: true,
|
||||
endpoint: this.getScalewayEndpointHost(),
|
||||
region: this.getScalewayRegion(),
|
||||
|
||||
credentials: this.getScalewayCredentialsConfig(),
|
||||
|
||||
upload_acl: {
|
||||
private: privateACL
|
||||
},
|
||||
|
||||
proxy: {
|
||||
proxify_private_files: enablePrivateProxy
|
||||
},
|
||||
|
||||
streaming_playlists: {
|
||||
bucket_name: this.DEFAULT_SCALEWAY_BUCKET,
|
||||
prefix: `test:server-${serverNumber}-streaming-playlists:`
|
||||
},
|
||||
|
||||
web_videos: {
|
||||
bucket_name: this.DEFAULT_SCALEWAY_BUCKET,
|
||||
prefix: `test:server-${serverNumber}-web-videos:`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getScalewayCredentialsConfig () {
|
||||
return {
|
||||
access_key_id: process.env.OBJECT_STORAGE_SCALEWAY_KEY_ID,
|
||||
secret_access_key: process.env.OBJECT_STORAGE_SCALEWAY_ACCESS_KEY
|
||||
}
|
||||
}
|
||||
|
||||
static getScalewayEndpointHost () {
|
||||
return 's3.fr-par.scw.cloud'
|
||||
}
|
||||
|
||||
static getScalewayRegion () {
|
||||
return 'fr-par'
|
||||
}
|
||||
|
||||
static getScalewayBaseUrl () {
|
||||
return `https://${this.DEFAULT_SCALEWAY_BUCKET}.${this.getScalewayEndpointHost()}/`
|
||||
}
|
||||
}
|
258
packages/server-commands/src/server/plugins-command.ts
Normal file
258
packages/server-commands/src/server/plugins-command.ts
Normal file
|
@ -0,0 +1,258 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { readJSON, writeJSON } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
PeerTubePlugin,
|
||||
PeerTubePluginIndex,
|
||||
PeertubePluginIndexList,
|
||||
PluginPackageJSON,
|
||||
PluginTranslation,
|
||||
PluginType_Type,
|
||||
PublicServerSetting,
|
||||
RegisteredServerSettings,
|
||||
ResultList
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class PluginsCommand extends AbstractCommand {
|
||||
|
||||
static getPluginTestPath (suffix = '') {
|
||||
return buildAbsoluteFixturePath('peertube-plugin-test' + suffix)
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
pluginType?: PluginType_Type
|
||||
uninstalled?: boolean
|
||||
}) {
|
||||
const { start, count, sort, pluginType, uninstalled } = options
|
||||
const path = '/api/v1/plugins'
|
||||
|
||||
return this.getRequestBody<ResultList<PeerTubePlugin>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
start,
|
||||
count,
|
||||
sort,
|
||||
pluginType,
|
||||
uninstalled
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listAvailable (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
pluginType?: PluginType_Type
|
||||
currentPeerTubeEngine?: string
|
||||
search?: string
|
||||
expectedStatus?: HttpStatusCodeType
|
||||
}) {
|
||||
const { start, count, sort, pluginType, search, currentPeerTubeEngine } = options
|
||||
const path = '/api/v1/plugins/available'
|
||||
|
||||
const query: PeertubePluginIndexList = {
|
||||
start,
|
||||
count,
|
||||
sort,
|
||||
pluginType,
|
||||
currentPeerTubeEngine,
|
||||
search
|
||||
}
|
||||
|
||||
return this.getRequestBody<ResultList<PeerTubePluginIndex>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
}) {
|
||||
const path = '/api/v1/plugins/' + options.npmName
|
||||
|
||||
return this.getRequestBody<PeerTubePlugin>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
updateSettings (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
settings: any
|
||||
}) {
|
||||
const { npmName, settings } = options
|
||||
const path = '/api/v1/plugins/' + npmName + '/settings'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { settings },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
getRegisteredSettings (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
}) {
|
||||
const path = '/api/v1/plugins/' + options.npmName + '/registered-settings'
|
||||
|
||||
return this.getRequestBody<RegisteredServerSettings>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getPublicSettings (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
}) {
|
||||
const { npmName } = options
|
||||
const path = '/api/v1/plugins/' + npmName + '/public-settings'
|
||||
|
||||
return this.getRequestBody<PublicServerSetting>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getTranslations (options: OverrideCommandOptions & {
|
||||
locale: string
|
||||
}) {
|
||||
const { locale } = options
|
||||
const path = '/plugins/translations/' + locale + '.json'
|
||||
|
||||
return this.getRequestBody<PluginTranslation>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
install (options: OverrideCommandOptions & {
|
||||
path?: string
|
||||
npmName?: string
|
||||
pluginVersion?: string
|
||||
}) {
|
||||
const { npmName, path, pluginVersion } = options
|
||||
const apiPath = '/api/v1/plugins/install'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: apiPath,
|
||||
fields: { npmName, path, pluginVersion },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
path?: string
|
||||
npmName?: string
|
||||
}) {
|
||||
const { npmName, path } = options
|
||||
const apiPath = '/api/v1/plugins/update'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: apiPath,
|
||||
fields: { npmName, path },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
uninstall (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
}) {
|
||||
const { npmName } = options
|
||||
const apiPath = '/api/v1/plugins/uninstall'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: apiPath,
|
||||
fields: { npmName },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
getCSS (options: OverrideCommandOptions = {}) {
|
||||
const path = '/plugins/global.css'
|
||||
|
||||
return this.getRequestText({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getExternalAuth (options: OverrideCommandOptions & {
|
||||
npmName: string
|
||||
npmVersion: string
|
||||
authName: string
|
||||
query?: any
|
||||
}) {
|
||||
const { npmName, npmVersion, authName, query } = options
|
||||
|
||||
const path = '/plugins/' + npmName + '/' + npmVersion + '/auth/' + authName
|
||||
|
||||
return this.getRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200,
|
||||
redirects: 0
|
||||
})
|
||||
}
|
||||
|
||||
updatePackageJSON (npmName: string, json: any) {
|
||||
const path = this.getPackageJSONPath(npmName)
|
||||
|
||||
return writeJSON(path, json)
|
||||
}
|
||||
|
||||
getPackageJSON (npmName: string): Promise<PluginPackageJSON> {
|
||||
const path = this.getPackageJSONPath(npmName)
|
||||
|
||||
return readJSON(path)
|
||||
}
|
||||
|
||||
private getPackageJSONPath (npmName: string) {
|
||||
return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json'))
|
||||
}
|
||||
}
|
80
packages/server-commands/src/server/redundancy-command.ts
Normal file
80
packages/server-commands/src/server/redundancy-command.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class RedundancyCommand extends AbstractCommand {
|
||||
|
||||
updateRedundancy (options: OverrideCommandOptions & {
|
||||
host: string
|
||||
redundancyAllowed: boolean
|
||||
}) {
|
||||
const { host, redundancyAllowed } = options
|
||||
const path = '/api/v1/server/redundancy/' + host
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { redundancyAllowed },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
listVideos (options: OverrideCommandOptions & {
|
||||
target: VideoRedundanciesTarget
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
}) {
|
||||
const path = '/api/v1/server/redundancy/videos'
|
||||
|
||||
const { target, start, count, sort } = options
|
||||
|
||||
return this.getRequestBody<ResultList<VideoRedundancy>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
|
||||
query: {
|
||||
start: start ?? 0,
|
||||
count: count ?? 5,
|
||||
sort: sort ?? 'name',
|
||||
target
|
||||
},
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
addVideo (options: OverrideCommandOptions & {
|
||||
videoId: number
|
||||
}) {
|
||||
const path = '/api/v1/server/redundancy/videos'
|
||||
const { videoId } = options
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { videoId },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeVideo (options: OverrideCommandOptions & {
|
||||
redundancyId: number
|
||||
}) {
|
||||
const { redundancyId } = options
|
||||
const path = '/api/v1/server/redundancy/videos/' + redundancyId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
451
packages/server-commands/src/server/server.ts
Normal file
451
packages/server-commands/src/server/server.ts
Normal file
|
@ -0,0 +1,451 @@
|
|||
import { ChildProcess, fork } from 'child_process'
|
||||
import { copy } from 'fs-extra/esm'
|
||||
import { join } from 'path'
|
||||
import { randomInt } from '@peertube/peertube-core-utils'
|
||||
import { Video, VideoChannel, VideoChannelSync, VideoCreateResult, VideoDetails } from '@peertube/peertube-models'
|
||||
import { parallelTests, root } from '@peertube/peertube-node-utils'
|
||||
import { BulkCommand } from '../bulk/index.js'
|
||||
import { CLICommand } from '../cli/index.js'
|
||||
import { CustomPagesCommand } from '../custom-pages/index.js'
|
||||
import { FeedCommand } from '../feeds/index.js'
|
||||
import { LogsCommand } from '../logs/index.js'
|
||||
import { AbusesCommand } from '../moderation/index.js'
|
||||
import { OverviewsCommand } from '../overviews/index.js'
|
||||
import { RunnerJobsCommand, RunnerRegistrationTokensCommand, RunnersCommand } from '../runners/index.js'
|
||||
import { SearchCommand } from '../search/index.js'
|
||||
import { SocketIOCommand } from '../socket/index.js'
|
||||
import {
|
||||
AccountsCommand,
|
||||
BlocklistCommand,
|
||||
LoginCommand,
|
||||
NotificationsCommand,
|
||||
RegistrationsCommand,
|
||||
SubscriptionsCommand,
|
||||
TwoFactorCommand,
|
||||
UsersCommand
|
||||
} from '../users/index.js'
|
||||
import {
|
||||
BlacklistCommand,
|
||||
CaptionsCommand,
|
||||
ChangeOwnershipCommand,
|
||||
ChannelsCommand,
|
||||
ChannelSyncsCommand,
|
||||
CommentsCommand,
|
||||
HistoryCommand,
|
||||
ImportsCommand,
|
||||
LiveCommand,
|
||||
PlaylistsCommand,
|
||||
ServicesCommand,
|
||||
StoryboardCommand,
|
||||
StreamingPlaylistsCommand,
|
||||
VideoPasswordsCommand,
|
||||
VideosCommand,
|
||||
VideoStatsCommand,
|
||||
VideoStudioCommand,
|
||||
VideoTokenCommand,
|
||||
ViewsCommand
|
||||
} from '../videos/index.js'
|
||||
import { ConfigCommand } from './config-command.js'
|
||||
import { ContactFormCommand } from './contact-form-command.js'
|
||||
import { DebugCommand } from './debug-command.js'
|
||||
import { FollowsCommand } from './follows-command.js'
|
||||
import { JobsCommand } from './jobs-command.js'
|
||||
import { MetricsCommand } from './metrics-command.js'
|
||||
import { PluginsCommand } from './plugins-command.js'
|
||||
import { RedundancyCommand } from './redundancy-command.js'
|
||||
import { ServersCommand } from './servers-command.js'
|
||||
import { StatsCommand } from './stats-command.js'
|
||||
|
||||
export type RunServerOptions = {
|
||||
hideLogs?: boolean
|
||||
nodeArgs?: string[]
|
||||
peertubeArgs?: string[]
|
||||
env?: { [ id: string ]: string }
|
||||
}
|
||||
|
||||
export class PeerTubeServer {
|
||||
app?: ChildProcess
|
||||
|
||||
url: string
|
||||
host?: string
|
||||
hostname?: string
|
||||
port?: number
|
||||
|
||||
rtmpPort?: number
|
||||
rtmpsPort?: number
|
||||
|
||||
parallel?: boolean
|
||||
internalServerNumber: number
|
||||
|
||||
serverNumber?: number
|
||||
customConfigFile?: string
|
||||
|
||||
store?: {
|
||||
client?: {
|
||||
id?: string
|
||||
secret?: string
|
||||
}
|
||||
|
||||
user?: {
|
||||
username: string
|
||||
password: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
channel?: VideoChannel
|
||||
videoChannelSync?: Partial<VideoChannelSync>
|
||||
|
||||
video?: Video
|
||||
videoCreated?: VideoCreateResult
|
||||
videoDetails?: VideoDetails
|
||||
|
||||
videos?: { id: number, uuid: string }[]
|
||||
}
|
||||
|
||||
accessToken?: string
|
||||
refreshToken?: string
|
||||
|
||||
bulk?: BulkCommand
|
||||
cli?: CLICommand
|
||||
customPage?: CustomPagesCommand
|
||||
feed?: FeedCommand
|
||||
logs?: LogsCommand
|
||||
abuses?: AbusesCommand
|
||||
overviews?: OverviewsCommand
|
||||
search?: SearchCommand
|
||||
contactForm?: ContactFormCommand
|
||||
debug?: DebugCommand
|
||||
follows?: FollowsCommand
|
||||
jobs?: JobsCommand
|
||||
metrics?: MetricsCommand
|
||||
plugins?: PluginsCommand
|
||||
redundancy?: RedundancyCommand
|
||||
stats?: StatsCommand
|
||||
config?: ConfigCommand
|
||||
socketIO?: SocketIOCommand
|
||||
accounts?: AccountsCommand
|
||||
blocklist?: BlocklistCommand
|
||||
subscriptions?: SubscriptionsCommand
|
||||
live?: LiveCommand
|
||||
services?: ServicesCommand
|
||||
blacklist?: BlacklistCommand
|
||||
captions?: CaptionsCommand
|
||||
changeOwnership?: ChangeOwnershipCommand
|
||||
playlists?: PlaylistsCommand
|
||||
history?: HistoryCommand
|
||||
imports?: ImportsCommand
|
||||
channelSyncs?: ChannelSyncsCommand
|
||||
streamingPlaylists?: StreamingPlaylistsCommand
|
||||
channels?: ChannelsCommand
|
||||
comments?: CommentsCommand
|
||||
notifications?: NotificationsCommand
|
||||
servers?: ServersCommand
|
||||
login?: LoginCommand
|
||||
users?: UsersCommand
|
||||
videoStudio?: VideoStudioCommand
|
||||
videos?: VideosCommand
|
||||
videoStats?: VideoStatsCommand
|
||||
views?: ViewsCommand
|
||||
twoFactor?: TwoFactorCommand
|
||||
videoToken?: VideoTokenCommand
|
||||
registrations?: RegistrationsCommand
|
||||
videoPasswords?: VideoPasswordsCommand
|
||||
|
||||
storyboard?: StoryboardCommand
|
||||
|
||||
runners?: RunnersCommand
|
||||
runnerRegistrationTokens?: RunnerRegistrationTokensCommand
|
||||
runnerJobs?: RunnerJobsCommand
|
||||
|
||||
constructor (options: { serverNumber: number } | { url: string }) {
|
||||
if ((options as any).url) {
|
||||
this.setUrl((options as any).url)
|
||||
} else {
|
||||
this.setServerNumber((options as any).serverNumber)
|
||||
}
|
||||
|
||||
this.store = {
|
||||
client: {
|
||||
id: null,
|
||||
secret: null
|
||||
},
|
||||
user: {
|
||||
username: null,
|
||||
password: null
|
||||
}
|
||||
}
|
||||
|
||||
this.assignCommands()
|
||||
}
|
||||
|
||||
setServerNumber (serverNumber: number) {
|
||||
this.serverNumber = serverNumber
|
||||
|
||||
this.parallel = parallelTests()
|
||||
|
||||
this.internalServerNumber = this.parallel ? this.randomServer() : this.serverNumber
|
||||
this.rtmpPort = this.parallel ? this.randomRTMP() : 1936
|
||||
this.rtmpsPort = this.parallel ? this.randomRTMP() : 1937
|
||||
this.port = 9000 + this.internalServerNumber
|
||||
|
||||
this.url = `http://127.0.0.1:${this.port}`
|
||||
this.host = `127.0.0.1:${this.port}`
|
||||
this.hostname = '127.0.0.1'
|
||||
}
|
||||
|
||||
setUrl (url: string) {
|
||||
const parsed = new URL(url)
|
||||
|
||||
this.url = url
|
||||
this.host = parsed.host
|
||||
this.hostname = parsed.hostname
|
||||
this.port = parseInt(parsed.port)
|
||||
}
|
||||
|
||||
getDirectoryPath (directoryName: string) {
|
||||
const testDirectory = 'test' + this.internalServerNumber
|
||||
|
||||
return join(root(), testDirectory, directoryName)
|
||||
}
|
||||
|
||||
async flushAndRun (configOverride?: object, options: RunServerOptions = {}) {
|
||||
await ServersCommand.flushTests(this.internalServerNumber)
|
||||
|
||||
return this.run(configOverride, options)
|
||||
}
|
||||
|
||||
async run (configOverrideArg?: any, options: RunServerOptions = {}) {
|
||||
// These actions are async so we need to be sure that they have both been done
|
||||
const serverRunString = {
|
||||
'HTTP server listening': false
|
||||
}
|
||||
const key = 'Database peertube_test' + this.internalServerNumber + ' is ready'
|
||||
serverRunString[key] = false
|
||||
|
||||
const regexps = {
|
||||
client_id: 'Client id: (.+)',
|
||||
client_secret: 'Client secret: (.+)',
|
||||
user_username: 'Username: (.+)',
|
||||
user_password: 'User password: (.+)'
|
||||
}
|
||||
|
||||
await this.assignCustomConfigFile()
|
||||
|
||||
const configOverride = this.buildConfigOverride()
|
||||
|
||||
if (configOverrideArg !== undefined) {
|
||||
Object.assign(configOverride, configOverrideArg)
|
||||
}
|
||||
|
||||
// Share the environment
|
||||
const env = { ...process.env }
|
||||
env['NODE_ENV'] = 'test'
|
||||
env['NODE_APP_INSTANCE'] = this.internalServerNumber.toString()
|
||||
env['NODE_CONFIG'] = JSON.stringify(configOverride)
|
||||
|
||||
if (options.env) {
|
||||
Object.assign(env, options.env)
|
||||
}
|
||||
|
||||
const execArgv = options.nodeArgs || []
|
||||
// FIXME: too slow :/
|
||||
// execArgv.push('--enable-source-maps')
|
||||
|
||||
const forkOptions = {
|
||||
silent: true,
|
||||
env,
|
||||
detached: false,
|
||||
execArgv
|
||||
}
|
||||
|
||||
const peertubeArgs = options.peertubeArgs || []
|
||||
|
||||
return new Promise<void>((res, rej) => {
|
||||
const self = this
|
||||
let aggregatedLogs = ''
|
||||
|
||||
this.app = fork(join(root(), 'dist', 'server.js'), peertubeArgs, forkOptions)
|
||||
|
||||
const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs))
|
||||
const onParentExit = () => {
|
||||
if (!this.app?.pid) return
|
||||
|
||||
try {
|
||||
process.kill(self.app.pid)
|
||||
} catch { /* empty */ }
|
||||
}
|
||||
|
||||
this.app.on('exit', onPeerTubeExit)
|
||||
process.on('exit', onParentExit)
|
||||
|
||||
this.app.stdout.on('data', function onStdout (data) {
|
||||
let dontContinue = false
|
||||
|
||||
const log: string = data.toString()
|
||||
aggregatedLogs += log
|
||||
|
||||
// Capture things if we want to
|
||||
for (const key of Object.keys(regexps)) {
|
||||
const regexp = regexps[key]
|
||||
const matches = log.match(regexp)
|
||||
if (matches !== null) {
|
||||
if (key === 'client_id') self.store.client.id = matches[1]
|
||||
else if (key === 'client_secret') self.store.client.secret = matches[1]
|
||||
else if (key === 'user_username') self.store.user.username = matches[1]
|
||||
else if (key === 'user_password') self.store.user.password = matches[1]
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all required sentences are here
|
||||
for (const key of Object.keys(serverRunString)) {
|
||||
if (log.includes(key)) serverRunString[key] = true
|
||||
if (serverRunString[key] === false) dontContinue = true
|
||||
}
|
||||
|
||||
// If no, there is maybe one thing not already initialized (client/user credentials generation...)
|
||||
if (dontContinue === true) return
|
||||
|
||||
if (options.hideLogs === false) {
|
||||
console.log(log)
|
||||
} else {
|
||||
process.removeListener('exit', onParentExit)
|
||||
self.app.stdout.removeListener('data', onStdout)
|
||||
self.app.removeListener('exit', onPeerTubeExit)
|
||||
}
|
||||
|
||||
res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
kill () {
|
||||
if (!this.app) return Promise.resolve()
|
||||
|
||||
process.kill(this.app.pid)
|
||||
|
||||
this.app = null
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
private randomServer () {
|
||||
const low = 2500
|
||||
const high = 10000
|
||||
|
||||
return randomInt(low, high)
|
||||
}
|
||||
|
||||
private randomRTMP () {
|
||||
const low = 1900
|
||||
const high = 2100
|
||||
|
||||
return randomInt(low, high)
|
||||
}
|
||||
|
||||
private async assignCustomConfigFile () {
|
||||
if (this.internalServerNumber === this.serverNumber) return
|
||||
|
||||
const basePath = join(root(), 'config')
|
||||
|
||||
const tmpConfigFile = join(basePath, `test-${this.internalServerNumber}.yaml`)
|
||||
await copy(join(basePath, `test-${this.serverNumber}.yaml`), tmpConfigFile)
|
||||
|
||||
this.customConfigFile = tmpConfigFile
|
||||
}
|
||||
|
||||
private buildConfigOverride () {
|
||||
if (!this.parallel) return {}
|
||||
|
||||
return {
|
||||
listen: {
|
||||
port: this.port
|
||||
},
|
||||
webserver: {
|
||||
port: this.port
|
||||
},
|
||||
database: {
|
||||
suffix: '_test' + this.internalServerNumber
|
||||
},
|
||||
storage: {
|
||||
tmp: this.getDirectoryPath('tmp') + '/',
|
||||
tmp_persistent: this.getDirectoryPath('tmp-persistent') + '/',
|
||||
bin: this.getDirectoryPath('bin') + '/',
|
||||
avatars: this.getDirectoryPath('avatars') + '/',
|
||||
web_videos: this.getDirectoryPath('web-videos') + '/',
|
||||
streaming_playlists: this.getDirectoryPath('streaming-playlists') + '/',
|
||||
redundancy: this.getDirectoryPath('redundancy') + '/',
|
||||
logs: this.getDirectoryPath('logs') + '/',
|
||||
previews: this.getDirectoryPath('previews') + '/',
|
||||
thumbnails: this.getDirectoryPath('thumbnails') + '/',
|
||||
storyboards: this.getDirectoryPath('storyboards') + '/',
|
||||
torrents: this.getDirectoryPath('torrents') + '/',
|
||||
captions: this.getDirectoryPath('captions') + '/',
|
||||
cache: this.getDirectoryPath('cache') + '/',
|
||||
plugins: this.getDirectoryPath('plugins') + '/',
|
||||
well_known: this.getDirectoryPath('well-known') + '/'
|
||||
},
|
||||
admin: {
|
||||
email: `admin${this.internalServerNumber}@example.com`
|
||||
},
|
||||
live: {
|
||||
rtmp: {
|
||||
port: this.rtmpPort
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private assignCommands () {
|
||||
this.bulk = new BulkCommand(this)
|
||||
this.cli = new CLICommand(this)
|
||||
this.customPage = new CustomPagesCommand(this)
|
||||
this.feed = new FeedCommand(this)
|
||||
this.logs = new LogsCommand(this)
|
||||
this.abuses = new AbusesCommand(this)
|
||||
this.overviews = new OverviewsCommand(this)
|
||||
this.search = new SearchCommand(this)
|
||||
this.contactForm = new ContactFormCommand(this)
|
||||
this.debug = new DebugCommand(this)
|
||||
this.follows = new FollowsCommand(this)
|
||||
this.jobs = new JobsCommand(this)
|
||||
this.metrics = new MetricsCommand(this)
|
||||
this.plugins = new PluginsCommand(this)
|
||||
this.redundancy = new RedundancyCommand(this)
|
||||
this.stats = new StatsCommand(this)
|
||||
this.config = new ConfigCommand(this)
|
||||
this.socketIO = new SocketIOCommand(this)
|
||||
this.accounts = new AccountsCommand(this)
|
||||
this.blocklist = new BlocklistCommand(this)
|
||||
this.subscriptions = new SubscriptionsCommand(this)
|
||||
this.live = new LiveCommand(this)
|
||||
this.services = new ServicesCommand(this)
|
||||
this.blacklist = new BlacklistCommand(this)
|
||||
this.captions = new CaptionsCommand(this)
|
||||
this.changeOwnership = new ChangeOwnershipCommand(this)
|
||||
this.playlists = new PlaylistsCommand(this)
|
||||
this.history = new HistoryCommand(this)
|
||||
this.imports = new ImportsCommand(this)
|
||||
this.channelSyncs = new ChannelSyncsCommand(this)
|
||||
this.streamingPlaylists = new StreamingPlaylistsCommand(this)
|
||||
this.channels = new ChannelsCommand(this)
|
||||
this.comments = new CommentsCommand(this)
|
||||
this.notifications = new NotificationsCommand(this)
|
||||
this.servers = new ServersCommand(this)
|
||||
this.login = new LoginCommand(this)
|
||||
this.users = new UsersCommand(this)
|
||||
this.videos = new VideosCommand(this)
|
||||
this.videoStudio = new VideoStudioCommand(this)
|
||||
this.videoStats = new VideoStatsCommand(this)
|
||||
this.views = new ViewsCommand(this)
|
||||
this.twoFactor = new TwoFactorCommand(this)
|
||||
this.videoToken = new VideoTokenCommand(this)
|
||||
this.registrations = new RegistrationsCommand(this)
|
||||
|
||||
this.storyboard = new StoryboardCommand(this)
|
||||
|
||||
this.runners = new RunnersCommand(this)
|
||||
this.runnerRegistrationTokens = new RunnerRegistrationTokensCommand(this)
|
||||
this.runnerJobs = new RunnerJobsCommand(this)
|
||||
this.videoPasswords = new VideoPasswordsCommand(this)
|
||||
}
|
||||
}
|
104
packages/server-commands/src/server/servers-command.ts
Normal file
104
packages/server-commands/src/server/servers-command.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
import { exec } from 'child_process'
|
||||
import { copy, ensureDir, remove } from 'fs-extra/esm'
|
||||
import { readdir, readFile } from 'fs/promises'
|
||||
import { basename, join } from 'path'
|
||||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { getFileSize, isGithubCI, root } from '@peertube/peertube-node-utils'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class ServersCommand extends AbstractCommand {
|
||||
|
||||
static flushTests (internalServerNumber: number) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
const suffix = ` -- ${internalServerNumber}`
|
||||
|
||||
return exec('npm run clean:server:test' + suffix, (err, _stdout, stderr) => {
|
||||
if (err || stderr) return rej(err || new Error(stderr))
|
||||
|
||||
return res()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ping (options: OverrideCommandOptions = {}) {
|
||||
return this.getRequestBody({
|
||||
...options,
|
||||
|
||||
path: '/api/v1/ping',
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
cleanupTests () {
|
||||
const promises: Promise<any>[] = []
|
||||
|
||||
const saveGithubLogsIfNeeded = async () => {
|
||||
if (!isGithubCI()) return
|
||||
|
||||
await ensureDir('artifacts')
|
||||
|
||||
const origin = this.buildDirectory('logs/peertube.log')
|
||||
const destname = `peertube-${this.server.internalServerNumber}.log`
|
||||
console.log('Saving logs %s.', destname)
|
||||
|
||||
await copy(origin, join('artifacts', destname))
|
||||
}
|
||||
|
||||
if (this.server.parallel) {
|
||||
const promise = saveGithubLogsIfNeeded()
|
||||
.then(() => ServersCommand.flushTests(this.server.internalServerNumber))
|
||||
|
||||
promises.push(promise)
|
||||
}
|
||||
|
||||
if (this.server.customConfigFile) {
|
||||
promises.push(remove(this.server.customConfigFile))
|
||||
}
|
||||
|
||||
return promises
|
||||
}
|
||||
|
||||
async waitUntilLog (str: string, count = 1, strictCount = true) {
|
||||
const logfile = this.buildDirectory('logs/peertube.log')
|
||||
|
||||
while (true) {
|
||||
const buf = await readFile(logfile)
|
||||
|
||||
const matches = buf.toString().match(new RegExp(str, 'g'))
|
||||
if (matches && matches.length === count) return
|
||||
if (matches && strictCount === false && matches.length >= count) return
|
||||
|
||||
await wait(1000)
|
||||
}
|
||||
}
|
||||
|
||||
buildDirectory (directory: string) {
|
||||
return join(root(), 'test' + this.server.internalServerNumber, directory)
|
||||
}
|
||||
|
||||
async countFiles (directory: string) {
|
||||
const files = await readdir(this.buildDirectory(directory))
|
||||
|
||||
return files.length
|
||||
}
|
||||
|
||||
buildWebVideoFilePath (fileUrl: string) {
|
||||
return this.buildDirectory(join('web-videos', basename(fileUrl)))
|
||||
}
|
||||
|
||||
buildFragmentedFilePath (videoUUID: string, fileUrl: string) {
|
||||
return this.buildDirectory(join('streaming-playlists', 'hls', videoUUID, basename(fileUrl)))
|
||||
}
|
||||
|
||||
getLogContent () {
|
||||
return readFile(this.buildDirectory('logs/peertube.log'))
|
||||
}
|
||||
|
||||
async getServerFileSize (subPath: string) {
|
||||
const path = this.server.servers.buildDirectory(subPath)
|
||||
|
||||
return getFileSize(path)
|
||||
}
|
||||
}
|
68
packages/server-commands/src/server/servers.ts
Normal file
68
packages/server-commands/src/server/servers.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
import { ensureDir } from 'fs-extra/esm'
|
||||
import { isGithubCI } from '@peertube/peertube-node-utils'
|
||||
import { PeerTubeServer, RunServerOptions } from './server.js'
|
||||
|
||||
async function createSingleServer (serverNumber: number, configOverride?: object, options: RunServerOptions = {}) {
|
||||
const server = new PeerTubeServer({ serverNumber })
|
||||
|
||||
await server.flushAndRun(configOverride, options)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
function createMultipleServers (totalServers: number, configOverride?: object, options: RunServerOptions = {}) {
|
||||
const serverPromises: Promise<PeerTubeServer>[] = []
|
||||
|
||||
for (let i = 1; i <= totalServers; i++) {
|
||||
serverPromises.push(createSingleServer(i, configOverride, options))
|
||||
}
|
||||
|
||||
return Promise.all(serverPromises)
|
||||
}
|
||||
|
||||
function killallServers (servers: PeerTubeServer[]) {
|
||||
return Promise.all(servers.map(s => s.kill()))
|
||||
}
|
||||
|
||||
async function cleanupTests (servers: PeerTubeServer[]) {
|
||||
await killallServers(servers)
|
||||
|
||||
if (isGithubCI()) {
|
||||
await ensureDir('artifacts')
|
||||
}
|
||||
|
||||
let p: Promise<any>[] = []
|
||||
for (const server of servers) {
|
||||
p = p.concat(server.servers.cleanupTests())
|
||||
}
|
||||
|
||||
return Promise.all(p)
|
||||
}
|
||||
|
||||
function getServerImportConfig (mode: 'youtube-dl' | 'yt-dlp') {
|
||||
return {
|
||||
import: {
|
||||
videos: {
|
||||
http: {
|
||||
youtube_dl_release: {
|
||||
url: mode === 'youtube-dl'
|
||||
? 'https://api.github.com/repos/ytdl-org/youtube-dl/releases'
|
||||
: 'https://api.github.com/repos/yt-dlp/yt-dlp/releases',
|
||||
|
||||
name: mode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
createSingleServer,
|
||||
createMultipleServers,
|
||||
cleanupTests,
|
||||
killallServers,
|
||||
getServerImportConfig
|
||||
}
|
25
packages/server-commands/src/server/stats-command.ts
Normal file
25
packages/server-commands/src/server/stats-command.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { HttpStatusCode, ServerStats } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class StatsCommand extends AbstractCommand {
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
useCache?: boolean // default false
|
||||
} = {}) {
|
||||
const { useCache = false } = options
|
||||
const path = '/api/v1/server/stats'
|
||||
|
||||
const query = {
|
||||
t: useCache ? undefined : new Date().getTime()
|
||||
}
|
||||
|
||||
return this.getRequestBody<ServerStats>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
225
packages/server-commands/src/shared/abstract-command.ts
Normal file
225
packages/server-commands/src/shared/abstract-command.ts
Normal file
|
@ -0,0 +1,225 @@
|
|||
import { isAbsolute } from 'path'
|
||||
import { HttpStatusCodeType } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import {
|
||||
makeDeleteRequest,
|
||||
makeGetRequest,
|
||||
makePostBodyRequest,
|
||||
makePutBodyRequest,
|
||||
makeUploadRequest,
|
||||
unwrapBody,
|
||||
unwrapText
|
||||
} from '../requests/requests.js'
|
||||
|
||||
import type { PeerTubeServer } from '../server/server.js'
|
||||
|
||||
export interface OverrideCommandOptions {
|
||||
token?: string
|
||||
expectedStatus?: HttpStatusCodeType
|
||||
}
|
||||
|
||||
interface InternalCommonCommandOptions extends OverrideCommandOptions {
|
||||
// Default to server.url
|
||||
url?: string
|
||||
|
||||
path: string
|
||||
// If we automatically send the server token if the token is not provided
|
||||
implicitToken: boolean
|
||||
defaultExpectedStatus: HttpStatusCodeType
|
||||
|
||||
// Common optional request parameters
|
||||
contentType?: string
|
||||
accept?: string
|
||||
redirects?: number
|
||||
range?: string
|
||||
host?: string
|
||||
headers?: { [ name: string ]: string }
|
||||
requestType?: string
|
||||
responseType?: string
|
||||
xForwardedFor?: string
|
||||
}
|
||||
|
||||
interface InternalGetCommandOptions extends InternalCommonCommandOptions {
|
||||
query?: { [ id: string ]: any }
|
||||
}
|
||||
|
||||
interface InternalDeleteCommandOptions extends InternalCommonCommandOptions {
|
||||
query?: { [ id: string ]: any }
|
||||
rawQuery?: string
|
||||
}
|
||||
|
||||
abstract class AbstractCommand {
|
||||
|
||||
constructor (
|
||||
protected server: PeerTubeServer
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
protected getRequestBody <T> (options: InternalGetCommandOptions) {
|
||||
return unwrapBody<T>(this.getRequest(options))
|
||||
}
|
||||
|
||||
protected getRequestText (options: InternalGetCommandOptions) {
|
||||
return unwrapText(this.getRequest(options))
|
||||
}
|
||||
|
||||
protected getRawRequest (options: Omit<InternalGetCommandOptions, 'path'>) {
|
||||
const { url, range } = options
|
||||
const { host, protocol, pathname } = new URL(url)
|
||||
|
||||
return this.getRequest({
|
||||
...options,
|
||||
|
||||
token: this.buildCommonRequestToken(options),
|
||||
defaultExpectedStatus: this.buildExpectedStatus(options),
|
||||
|
||||
url: `${protocol}//${host}`,
|
||||
path: pathname,
|
||||
range
|
||||
})
|
||||
}
|
||||
|
||||
protected getRequest (options: InternalGetCommandOptions) {
|
||||
const { query } = options
|
||||
|
||||
return makeGetRequest({
|
||||
...this.buildCommonRequestOptions(options),
|
||||
|
||||
query
|
||||
})
|
||||
}
|
||||
|
||||
protected deleteRequest (options: InternalDeleteCommandOptions) {
|
||||
const { query, rawQuery } = options
|
||||
|
||||
return makeDeleteRequest({
|
||||
...this.buildCommonRequestOptions(options),
|
||||
|
||||
query,
|
||||
rawQuery
|
||||
})
|
||||
}
|
||||
|
||||
protected putBodyRequest (options: InternalCommonCommandOptions & {
|
||||
fields?: { [ fieldName: string ]: any }
|
||||
headers?: { [name: string]: string }
|
||||
}) {
|
||||
const { fields, headers } = options
|
||||
|
||||
return makePutBodyRequest({
|
||||
...this.buildCommonRequestOptions(options),
|
||||
|
||||
fields,
|
||||
headers
|
||||
})
|
||||
}
|
||||
|
||||
protected postBodyRequest (options: InternalCommonCommandOptions & {
|
||||
fields?: { [ fieldName: string ]: any }
|
||||
headers?: { [name: string]: string }
|
||||
}) {
|
||||
const { fields, headers } = options
|
||||
|
||||
return makePostBodyRequest({
|
||||
...this.buildCommonRequestOptions(options),
|
||||
|
||||
fields,
|
||||
headers
|
||||
})
|
||||
}
|
||||
|
||||
protected postUploadRequest (options: InternalCommonCommandOptions & {
|
||||
fields?: { [ fieldName: string ]: any }
|
||||
attaches?: { [ fieldName: string ]: any }
|
||||
}) {
|
||||
const { fields, attaches } = options
|
||||
|
||||
return makeUploadRequest({
|
||||
...this.buildCommonRequestOptions(options),
|
||||
|
||||
method: 'POST',
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
}
|
||||
|
||||
protected putUploadRequest (options: InternalCommonCommandOptions & {
|
||||
fields?: { [ fieldName: string ]: any }
|
||||
attaches?: { [ fieldName: string ]: any }
|
||||
}) {
|
||||
const { fields, attaches } = options
|
||||
|
||||
return makeUploadRequest({
|
||||
...this.buildCommonRequestOptions(options),
|
||||
|
||||
method: 'PUT',
|
||||
fields,
|
||||
attaches
|
||||
})
|
||||
}
|
||||
|
||||
protected updateImageRequest (options: InternalCommonCommandOptions & {
|
||||
fixture: string
|
||||
fieldname: string
|
||||
}) {
|
||||
const filePath = isAbsolute(options.fixture)
|
||||
? options.fixture
|
||||
: buildAbsoluteFixturePath(options.fixture)
|
||||
|
||||
return this.postUploadRequest({
|
||||
...options,
|
||||
|
||||
fields: {},
|
||||
attaches: { [options.fieldname]: filePath }
|
||||
})
|
||||
}
|
||||
|
||||
protected buildCommonRequestOptions (options: InternalCommonCommandOptions) {
|
||||
const { url, path, redirects, contentType, accept, range, host, headers, requestType, xForwardedFor, responseType } = options
|
||||
|
||||
return {
|
||||
url: url ?? this.server.url,
|
||||
path,
|
||||
|
||||
token: this.buildCommonRequestToken(options),
|
||||
expectedStatus: this.buildExpectedStatus(options),
|
||||
|
||||
redirects,
|
||||
contentType,
|
||||
range,
|
||||
host,
|
||||
accept,
|
||||
headers,
|
||||
type: requestType,
|
||||
responseType,
|
||||
xForwardedFor
|
||||
}
|
||||
}
|
||||
|
||||
protected buildCommonRequestToken (options: Pick<InternalCommonCommandOptions, 'token' | 'implicitToken'>) {
|
||||
const { token } = options
|
||||
|
||||
const fallbackToken = options.implicitToken
|
||||
? this.server.accessToken
|
||||
: undefined
|
||||
|
||||
return token !== undefined ? token : fallbackToken
|
||||
}
|
||||
|
||||
protected buildExpectedStatus (options: Pick<InternalCommonCommandOptions, 'expectedStatus' | 'defaultExpectedStatus'>) {
|
||||
const { expectedStatus, defaultExpectedStatus } = options
|
||||
|
||||
return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus
|
||||
}
|
||||
|
||||
protected buildVideoPasswordHeader (videoPassword: string) {
|
||||
return videoPassword !== undefined && videoPassword !== null
|
||||
? { 'x-peertube-video-password': videoPassword }
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
AbstractCommand
|
||||
}
|
1
packages/server-commands/src/shared/index.ts
Normal file
1
packages/server-commands/src/shared/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './abstract-command.js'
|
1
packages/server-commands/src/socket/index.ts
Normal file
1
packages/server-commands/src/socket/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './socket-io-command.js'
|
24
packages/server-commands/src/socket/socket-io-command.ts
Normal file
24
packages/server-commands/src/socket/socket-io-command.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { io } from 'socket.io-client'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class SocketIOCommand extends AbstractCommand {
|
||||
|
||||
getUserNotificationSocket (options: OverrideCommandOptions = {}) {
|
||||
return io(this.server.url + '/user-notifications', {
|
||||
query: { accessToken: options.token ?? this.server.accessToken }
|
||||
})
|
||||
}
|
||||
|
||||
getLiveNotificationSocket () {
|
||||
return io(this.server.url + '/live-videos')
|
||||
}
|
||||
|
||||
getRunnersSocket (options: {
|
||||
runnerToken: string
|
||||
}) {
|
||||
return io(this.server.url + '/runners', {
|
||||
reconnection: false,
|
||||
auth: { runnerToken: options.runnerToken }
|
||||
})
|
||||
}
|
||||
}
|
76
packages/server-commands/src/users/accounts-command.ts
Normal file
76
packages/server-commands/src/users/accounts-command.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { Account, AccountVideoRate, ActorFollow, HttpStatusCode, ResultList, VideoRateType } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class AccountsCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
sort?: string // default -createdAt
|
||||
} = {}) {
|
||||
const { sort = '-createdAt' } = options
|
||||
const path = '/api/v1/accounts'
|
||||
|
||||
return this.getRequestBody<ResultList<Account>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { sort },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
accountName: string
|
||||
}) {
|
||||
const path = '/api/v1/accounts/' + options.accountName
|
||||
|
||||
return this.getRequestBody<Account>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listRatings (options: OverrideCommandOptions & {
|
||||
accountName: string
|
||||
rating?: VideoRateType
|
||||
}) {
|
||||
const { rating, accountName } = options
|
||||
const path = '/api/v1/accounts/' + accountName + '/ratings'
|
||||
|
||||
const query = { rating }
|
||||
|
||||
return this.getRequestBody<ResultList<AccountVideoRate>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listFollowers (options: OverrideCommandOptions & {
|
||||
accountName: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
}) {
|
||||
const { accountName, start, count, sort, search } = options
|
||||
const path = '/api/v1/accounts/' + accountName + '/followers'
|
||||
|
||||
const query = { start, count, sort, search }
|
||||
|
||||
return this.getRequestBody<ResultList<ActorFollow>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
15
packages/server-commands/src/users/accounts.ts
Normal file
15
packages/server-commands/src/users/accounts.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { PeerTubeServer } from '../server/server.js'
|
||||
|
||||
async function setDefaultAccountAvatar (serversArg: PeerTubeServer | PeerTubeServer[], token?: string) {
|
||||
const servers = Array.isArray(serversArg)
|
||||
? serversArg
|
||||
: [ serversArg ]
|
||||
|
||||
for (const server of servers) {
|
||||
await server.users.updateMyAvatar({ fixture: 'avatar.png', token })
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
setDefaultAccountAvatar
|
||||
}
|
165
packages/server-commands/src/users/blocklist-command.ts
Normal file
165
packages/server-commands/src/users/blocklist-command.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { AccountBlock, BlockStatus, HttpStatusCode, ResultList, ServerBlock } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
type ListBlocklistOptions = OverrideCommandOptions & {
|
||||
start: number
|
||||
count: number
|
||||
|
||||
sort?: string // default -createdAt
|
||||
|
||||
search?: string
|
||||
}
|
||||
|
||||
export class BlocklistCommand extends AbstractCommand {
|
||||
|
||||
listMyAccountBlocklist (options: ListBlocklistOptions) {
|
||||
const path = '/api/v1/users/me/blocklist/accounts'
|
||||
|
||||
return this.listBlocklist<AccountBlock>(options, path)
|
||||
}
|
||||
|
||||
listMyServerBlocklist (options: ListBlocklistOptions) {
|
||||
const path = '/api/v1/users/me/blocklist/servers'
|
||||
|
||||
return this.listBlocklist<ServerBlock>(options, path)
|
||||
}
|
||||
|
||||
listServerAccountBlocklist (options: ListBlocklistOptions) {
|
||||
const path = '/api/v1/server/blocklist/accounts'
|
||||
|
||||
return this.listBlocklist<AccountBlock>(options, path)
|
||||
}
|
||||
|
||||
listServerServerBlocklist (options: ListBlocklistOptions) {
|
||||
const path = '/api/v1/server/blocklist/servers'
|
||||
|
||||
return this.listBlocklist<ServerBlock>(options, path)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getStatus (options: OverrideCommandOptions & {
|
||||
accounts?: string[]
|
||||
hosts?: string[]
|
||||
}) {
|
||||
const { accounts, hosts } = options
|
||||
|
||||
const path = '/api/v1/blocklist/status'
|
||||
|
||||
return this.getRequestBody<BlockStatus>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
accounts,
|
||||
hosts
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
addToMyBlocklist (options: OverrideCommandOptions & {
|
||||
account?: string
|
||||
server?: string
|
||||
}) {
|
||||
const { account, server } = options
|
||||
|
||||
const path = account
|
||||
? '/api/v1/users/me/blocklist/accounts'
|
||||
: '/api/v1/users/me/blocklist/servers'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {
|
||||
accountName: account,
|
||||
host: server
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
addToServerBlocklist (options: OverrideCommandOptions & {
|
||||
account?: string
|
||||
server?: string
|
||||
}) {
|
||||
const { account, server } = options
|
||||
|
||||
const path = account
|
||||
? '/api/v1/server/blocklist/accounts'
|
||||
: '/api/v1/server/blocklist/servers'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {
|
||||
accountName: account,
|
||||
host: server
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
removeFromMyBlocklist (options: OverrideCommandOptions & {
|
||||
account?: string
|
||||
server?: string
|
||||
}) {
|
||||
const { account, server } = options
|
||||
|
||||
const path = account
|
||||
? '/api/v1/users/me/blocklist/accounts/' + account
|
||||
: '/api/v1/users/me/blocklist/servers/' + server
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeFromServerBlocklist (options: OverrideCommandOptions & {
|
||||
account?: string
|
||||
server?: string
|
||||
}) {
|
||||
const { account, server } = options
|
||||
|
||||
const path = account
|
||||
? '/api/v1/server/blocklist/accounts/' + account
|
||||
: '/api/v1/server/blocklist/servers/' + server
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
private listBlocklist <T> (options: ListBlocklistOptions, path: string) {
|
||||
const { start, count, search, sort = '-createdAt' } = options
|
||||
|
||||
return this.getRequestBody<ResultList<T>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { start, count, sort, search },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
}
|
10
packages/server-commands/src/users/index.ts
Normal file
10
packages/server-commands/src/users/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
export * from './accounts-command.js'
|
||||
export * from './accounts.js'
|
||||
export * from './blocklist-command.js'
|
||||
export * from './login.js'
|
||||
export * from './login-command.js'
|
||||
export * from './notifications-command.js'
|
||||
export * from './registrations-command.js'
|
||||
export * from './subscriptions-command.js'
|
||||
export * from './two-factor-command.js'
|
||||
export * from './users-command.js'
|
159
packages/server-commands/src/users/login-command.ts
Normal file
159
packages/server-commands/src/users/login-command.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { HttpStatusCode, PeerTubeProblemDocument } from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
type LoginOptions = OverrideCommandOptions & {
|
||||
client?: { id?: string, secret?: string }
|
||||
user?: { username: string, password?: string }
|
||||
otpToken?: string
|
||||
}
|
||||
|
||||
export class LoginCommand extends AbstractCommand {
|
||||
|
||||
async login (options: LoginOptions = {}) {
|
||||
const res = await this._login(options)
|
||||
|
||||
return this.unwrapLoginBody(res.body)
|
||||
}
|
||||
|
||||
async loginAndGetResponse (options: LoginOptions = {}) {
|
||||
const res = await this._login(options)
|
||||
|
||||
return {
|
||||
res,
|
||||
body: this.unwrapLoginBody(res.body)
|
||||
}
|
||||
}
|
||||
|
||||
getAccessToken (arg1?: { username: string, password?: string }): Promise<string>
|
||||
getAccessToken (arg1: string, password?: string): Promise<string>
|
||||
async getAccessToken (arg1?: { username: string, password?: string } | string, password?: string) {
|
||||
let user: { username: string, password?: string }
|
||||
|
||||
if (!arg1) user = this.server.store.user
|
||||
else if (typeof arg1 === 'object') user = arg1
|
||||
else user = { username: arg1, password }
|
||||
|
||||
try {
|
||||
const body = await this.login({ user })
|
||||
|
||||
return body.access_token
|
||||
} catch (err) {
|
||||
throw new Error(`Cannot authenticate. Please check your username/password. (${err})`)
|
||||
}
|
||||
}
|
||||
|
||||
loginUsingExternalToken (options: OverrideCommandOptions & {
|
||||
username: string
|
||||
externalAuthToken: string
|
||||
}) {
|
||||
const { username, externalAuthToken } = options
|
||||
const path = '/api/v1/users/token'
|
||||
|
||||
const body = {
|
||||
client_id: this.server.store.client.id,
|
||||
client_secret: this.server.store.client.secret,
|
||||
username,
|
||||
response_type: 'code',
|
||||
grant_type: 'password',
|
||||
scope: 'upload',
|
||||
externalAuthToken
|
||||
}
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
requestType: 'form',
|
||||
fields: body,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
logout (options: OverrideCommandOptions & {
|
||||
token: string
|
||||
}) {
|
||||
const path = '/api/v1/users/revoke-token'
|
||||
|
||||
return unwrapBody<{ redirectUrl: string }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
requestType: 'form',
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
refreshToken (options: OverrideCommandOptions & {
|
||||
refreshToken: string
|
||||
}) {
|
||||
const path = '/api/v1/users/token'
|
||||
|
||||
const body = {
|
||||
client_id: this.server.store.client.id,
|
||||
client_secret: this.server.store.client.secret,
|
||||
refresh_token: options.refreshToken,
|
||||
response_type: 'code',
|
||||
grant_type: 'refresh_token'
|
||||
}
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
requestType: 'form',
|
||||
fields: body,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getClient (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/oauth-clients/local'
|
||||
|
||||
return this.getRequestBody<{ client_id: string, client_secret: string }>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
host: this.server.host,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
private _login (options: LoginOptions) {
|
||||
const { client = this.server.store.client, user = this.server.store.user, otpToken } = options
|
||||
const path = '/api/v1/users/token'
|
||||
|
||||
const body = {
|
||||
client_id: client.id,
|
||||
client_secret: client.secret,
|
||||
username: user.username,
|
||||
password: user.password ?? 'password',
|
||||
response_type: 'code',
|
||||
grant_type: 'password',
|
||||
scope: 'upload'
|
||||
}
|
||||
|
||||
const headers = otpToken
|
||||
? { 'x-peertube-otp': otpToken }
|
||||
: {}
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
headers,
|
||||
requestType: 'form',
|
||||
fields: body,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
private unwrapLoginBody (body: any) {
|
||||
return body as { access_token: string, refresh_token: string } & PeerTubeProblemDocument
|
||||
}
|
||||
}
|
19
packages/server-commands/src/users/login.ts
Normal file
19
packages/server-commands/src/users/login.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { PeerTubeServer } from '../server/server.js'
|
||||
|
||||
function setAccessTokensToServers (servers: PeerTubeServer[]) {
|
||||
const tasks: Promise<any>[] = []
|
||||
|
||||
for (const server of servers) {
|
||||
const p = server.login.getAccessToken()
|
||||
.then(t => { server.accessToken = t })
|
||||
tasks.push(p)
|
||||
}
|
||||
|
||||
return Promise.all(tasks)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
setAccessTokensToServers
|
||||
}
|
85
packages/server-commands/src/users/notifications-command.ts
Normal file
85
packages/server-commands/src/users/notifications-command.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { HttpStatusCode, ResultList, UserNotification, UserNotificationSetting } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class NotificationsCommand extends AbstractCommand {
|
||||
|
||||
updateMySettings (options: OverrideCommandOptions & {
|
||||
settings: UserNotificationSetting
|
||||
}) {
|
||||
const path = '/api/v1/users/me/notification-settings'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.settings,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
unread?: boolean
|
||||
sort?: string
|
||||
}) {
|
||||
const { start, count, unread, sort = '-createdAt' } = options
|
||||
const path = '/api/v1/users/me/notifications'
|
||||
|
||||
return this.getRequestBody<ResultList<UserNotification>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
start,
|
||||
count,
|
||||
sort,
|
||||
unread
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
markAsRead (options: OverrideCommandOptions & {
|
||||
ids: number[]
|
||||
}) {
|
||||
const { ids } = options
|
||||
const path = '/api/v1/users/me/notifications/read'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { ids },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
markAsReadAll (options: OverrideCommandOptions) {
|
||||
const path = '/api/v1/users/me/notifications/read-all'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async getLatest (options: OverrideCommandOptions = {}) {
|
||||
const { total, data } = await this.list({
|
||||
...options,
|
||||
start: 0,
|
||||
count: 1,
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
if (total === 0) return undefined
|
||||
|
||||
return data[0]
|
||||
}
|
||||
}
|
157
packages/server-commands/src/users/registrations-command.ts
Normal file
157
packages/server-commands/src/users/registrations-command.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
ResultList,
|
||||
UserRegistration,
|
||||
UserRegistrationRequest,
|
||||
UserRegistrationUpdateState
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class RegistrationsCommand extends AbstractCommand {
|
||||
|
||||
register (options: OverrideCommandOptions & Partial<UserRegistrationRequest> & Pick<UserRegistrationRequest, 'username'>) {
|
||||
const { password = 'password', email = options.username + '@example.com' } = options
|
||||
const path = '/api/v1/users/register'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {
|
||||
...pick(options, [ 'username', 'displayName', 'channel' ]),
|
||||
|
||||
password,
|
||||
email
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
requestRegistration (
|
||||
options: OverrideCommandOptions & Partial<UserRegistrationRequest> & Pick<UserRegistrationRequest, 'username' | 'registrationReason'>
|
||||
) {
|
||||
const { password = 'password', email = options.username + '@example.com' } = options
|
||||
const path = '/api/v1/users/registrations/request'
|
||||
|
||||
return unwrapBody<UserRegistration>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {
|
||||
...pick(options, [ 'username', 'displayName', 'channel', 'registrationReason' ]),
|
||||
|
||||
password,
|
||||
email
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
accept (options: OverrideCommandOptions & { id: number } & UserRegistrationUpdateState) {
|
||||
const { id } = options
|
||||
const path = '/api/v1/users/registrations/' + id + '/accept'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'moderationResponse', 'preventEmailDelivery' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
reject (options: OverrideCommandOptions & { id: number } & UserRegistrationUpdateState) {
|
||||
const { id } = options
|
||||
const path = '/api/v1/users/registrations/' + id + '/reject'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'moderationResponse', 'preventEmailDelivery' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
id: number
|
||||
}) {
|
||||
const { id } = options
|
||||
const path = '/api/v1/users/registrations/' + id
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
} = {}) {
|
||||
const path = '/api/v1/users/registrations'
|
||||
|
||||
return this.getRequestBody<ResultList<UserRegistration>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'search' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
askSendVerifyEmail (options: OverrideCommandOptions & {
|
||||
email: string
|
||||
}) {
|
||||
const { email } = options
|
||||
const path = '/api/v1/users/registrations/ask-send-verify-email'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { email },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
verifyEmail (options: OverrideCommandOptions & {
|
||||
registrationId: number
|
||||
verificationString: string
|
||||
}) {
|
||||
const { registrationId, verificationString } = options
|
||||
const path = '/api/v1/users/registrations/' + registrationId + '/verify-email'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {
|
||||
verificationString
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
83
packages/server-commands/src/users/subscriptions-command.ts
Normal file
83
packages/server-commands/src/users/subscriptions-command.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { HttpStatusCode, ResultList, VideoChannel } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class SubscriptionsCommand extends AbstractCommand {
|
||||
|
||||
add (options: OverrideCommandOptions & {
|
||||
targetUri: string
|
||||
}) {
|
||||
const path = '/api/v1/users/me/subscriptions'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { uri: options.targetUri },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
sort?: string // default -createdAt
|
||||
search?: string
|
||||
} = {}) {
|
||||
const { sort = '-createdAt', search } = options
|
||||
const path = '/api/v1/users/me/subscriptions'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoChannel>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
sort,
|
||||
search
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
uri: string
|
||||
}) {
|
||||
const path = '/api/v1/users/me/subscriptions/' + options.uri
|
||||
|
||||
return this.getRequestBody<VideoChannel>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
remove (options: OverrideCommandOptions & {
|
||||
uri: string
|
||||
}) {
|
||||
const path = '/api/v1/users/me/subscriptions/' + options.uri
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
exist (options: OverrideCommandOptions & {
|
||||
uris: string[]
|
||||
}) {
|
||||
const path = '/api/v1/users/me/subscriptions/exist'
|
||||
|
||||
return this.getRequestBody<{ [id: string ]: boolean }>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { 'uris[]': options.uris },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
92
packages/server-commands/src/users/two-factor-command.ts
Normal file
92
packages/server-commands/src/users/two-factor-command.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { TOTP } from 'otpauth'
|
||||
import { HttpStatusCode, TwoFactorEnableResult } from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class TwoFactorCommand extends AbstractCommand {
|
||||
|
||||
static buildOTP (options: {
|
||||
secret: string
|
||||
}) {
|
||||
const { secret } = options
|
||||
|
||||
return new TOTP({
|
||||
issuer: 'PeerTube',
|
||||
algorithm: 'SHA1',
|
||||
digits: 6,
|
||||
period: 30,
|
||||
secret
|
||||
})
|
||||
}
|
||||
|
||||
request (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
currentPassword?: string
|
||||
}) {
|
||||
const { currentPassword, userId } = options
|
||||
|
||||
const path = '/api/v1/users/' + userId + '/two-factor/request'
|
||||
|
||||
return unwrapBody<TwoFactorEnableResult>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { currentPassword },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
confirmRequest (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
requestToken: string
|
||||
otpToken: string
|
||||
}) {
|
||||
const { userId, requestToken, otpToken } = options
|
||||
|
||||
const path = '/api/v1/users/' + userId + '/two-factor/confirm-request'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { requestToken, otpToken },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
disable (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
currentPassword?: string
|
||||
}) {
|
||||
const { userId, currentPassword } = options
|
||||
const path = '/api/v1/users/' + userId + '/two-factor/disable'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { currentPassword },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async requestAndConfirm (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
currentPassword?: string
|
||||
}) {
|
||||
const { userId, currentPassword } = options
|
||||
|
||||
const { otpRequest } = await this.request({ userId, currentPassword })
|
||||
|
||||
await this.confirmRequest({
|
||||
userId,
|
||||
requestToken: otpRequest.requestToken,
|
||||
otpToken: TwoFactorCommand.buildOTP({ secret: otpRequest.secret }).generate()
|
||||
})
|
||||
|
||||
return otpRequest
|
||||
}
|
||||
}
|
389
packages/server-commands/src/users/users-command.ts
Normal file
389
packages/server-commands/src/users/users-command.ts
Normal file
|
@ -0,0 +1,389 @@
|
|||
import { omit, pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
MyUser,
|
||||
ResultList,
|
||||
ScopedToken,
|
||||
User,
|
||||
UserAdminFlagType,
|
||||
UserCreateResult,
|
||||
UserRole,
|
||||
UserRoleType,
|
||||
UserUpdate,
|
||||
UserUpdateMe,
|
||||
UserVideoQuota,
|
||||
UserVideoRate
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class UsersCommand extends AbstractCommand {
|
||||
|
||||
askResetPassword (options: OverrideCommandOptions & {
|
||||
email: string
|
||||
}) {
|
||||
const { email } = options
|
||||
const path = '/api/v1/users/ask-reset-password'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { email },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
resetPassword (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
verificationString: string
|
||||
password: string
|
||||
}) {
|
||||
const { userId, verificationString, password } = options
|
||||
const path = '/api/v1/users/' + userId + '/reset-password'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { password, verificationString },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
askSendVerifyEmail (options: OverrideCommandOptions & {
|
||||
email: string
|
||||
}) {
|
||||
const { email } = options
|
||||
const path = '/api/v1/users/ask-send-verify-email'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { email },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
verifyEmail (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
verificationString: string
|
||||
isPendingEmail?: boolean // default false
|
||||
}) {
|
||||
const { userId, verificationString, isPendingEmail = false } = options
|
||||
const path = '/api/v1/users/' + userId + '/verify-email'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {
|
||||
verificationString,
|
||||
isPendingEmail
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
banUser (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
reason?: string
|
||||
}) {
|
||||
const { userId, reason } = options
|
||||
const path = '/api/v1/users' + '/' + userId + '/block'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { reason },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
unbanUser (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
}) {
|
||||
const { userId } = options
|
||||
const path = '/api/v1/users' + '/' + userId + '/unblock'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getMyScopedTokens (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/users/scoped-tokens'
|
||||
|
||||
return this.getRequestBody<ScopedToken>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
renewMyScopedTokens (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/users/scoped-tokens'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
create (options: OverrideCommandOptions & {
|
||||
username: string
|
||||
password?: string
|
||||
videoQuota?: number
|
||||
videoQuotaDaily?: number
|
||||
role?: UserRoleType
|
||||
adminFlags?: UserAdminFlagType
|
||||
}) {
|
||||
const {
|
||||
username,
|
||||
adminFlags,
|
||||
password = 'password',
|
||||
videoQuota,
|
||||
videoQuotaDaily,
|
||||
role = UserRole.USER
|
||||
} = options
|
||||
|
||||
const path = '/api/v1/users'
|
||||
|
||||
return unwrapBody<{ user: UserCreateResult }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {
|
||||
username,
|
||||
password,
|
||||
role,
|
||||
adminFlags,
|
||||
email: username + '@example.com',
|
||||
videoQuota,
|
||||
videoQuotaDaily
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})).then(res => res.user)
|
||||
}
|
||||
|
||||
async generate (username: string, role?: UserRoleType) {
|
||||
const password = 'password'
|
||||
const user = await this.create({ username, password, role })
|
||||
|
||||
const token = await this.server.login.getAccessToken({ username, password })
|
||||
|
||||
const me = await this.getMyInfo({ token })
|
||||
|
||||
return {
|
||||
token,
|
||||
userId: user.id,
|
||||
userChannelId: me.videoChannels[0].id,
|
||||
userChannelName: me.videoChannels[0].name,
|
||||
password
|
||||
}
|
||||
}
|
||||
|
||||
async generateUserAndToken (username: string, role?: UserRoleType) {
|
||||
const password = 'password'
|
||||
await this.create({ username, password, role })
|
||||
|
||||
return this.server.login.getAccessToken({ username, password })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getMyInfo (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/users/me'
|
||||
|
||||
return this.getRequestBody<MyUser>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getMyQuotaUsed (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/users/me/video-quota-used'
|
||||
|
||||
return this.getRequestBody<UserVideoQuota>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getMyRating (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const { videoId } = options
|
||||
const path = '/api/v1/users/me/videos/' + videoId + '/rating'
|
||||
|
||||
return this.getRequestBody<UserVideoRate>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
deleteMe (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/users/me'
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
updateMe (options: OverrideCommandOptions & UserUpdateMe) {
|
||||
const path = '/api/v1/users/me'
|
||||
|
||||
const toSend: UserUpdateMe = omit(options, [ 'expectedStatus', 'token' ])
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: toSend,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
updateMyAvatar (options: OverrideCommandOptions & {
|
||||
fixture: string
|
||||
}) {
|
||||
const { fixture } = options
|
||||
const path = '/api/v1/users/me/avatar/pick'
|
||||
|
||||
return this.updateImageRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fixture,
|
||||
fieldname: 'avatarfile',
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
withStats?: boolean // default false
|
||||
}) {
|
||||
const { userId, withStats } = options
|
||||
const path = '/api/v1/users/' + userId
|
||||
|
||||
return this.getRequestBody<User>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { withStats },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
blocked?: boolean
|
||||
} = {}) {
|
||||
const path = '/api/v1/users'
|
||||
|
||||
return this.getRequestBody<ResultList<User>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'search', 'blocked' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
remove (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
}) {
|
||||
const { userId } = options
|
||||
const path = '/api/v1/users/' + userId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
userId: number
|
||||
email?: string
|
||||
emailVerified?: boolean
|
||||
videoQuota?: number
|
||||
videoQuotaDaily?: number
|
||||
password?: string
|
||||
adminFlags?: UserAdminFlagType
|
||||
pluginAuth?: string
|
||||
role?: UserRoleType
|
||||
}) {
|
||||
const path = '/api/v1/users/' + options.userId
|
||||
|
||||
const toSend: UserUpdate = {}
|
||||
if (options.password !== undefined && options.password !== null) toSend.password = options.password
|
||||
if (options.email !== undefined && options.email !== null) toSend.email = options.email
|
||||
if (options.emailVerified !== undefined && options.emailVerified !== null) toSend.emailVerified = options.emailVerified
|
||||
if (options.videoQuota !== undefined && options.videoQuota !== null) toSend.videoQuota = options.videoQuota
|
||||
if (options.videoQuotaDaily !== undefined && options.videoQuotaDaily !== null) toSend.videoQuotaDaily = options.videoQuotaDaily
|
||||
if (options.role !== undefined && options.role !== null) toSend.role = options.role
|
||||
if (options.adminFlags !== undefined && options.adminFlags !== null) toSend.adminFlags = options.adminFlags
|
||||
if (options.pluginAuth !== undefined) toSend.pluginAuth = options.pluginAuth
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: toSend,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
74
packages/server-commands/src/videos/blacklist-command.ts
Normal file
74
packages/server-commands/src/videos/blacklist-command.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { HttpStatusCode, ResultList, VideoBlacklist, VideoBlacklistType_Type } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class BlacklistCommand extends AbstractCommand {
|
||||
|
||||
add (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
reason?: string
|
||||
unfederate?: boolean
|
||||
}) {
|
||||
const { videoId, reason, unfederate } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/blacklist'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { reason, unfederate },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
reason?: string
|
||||
}) {
|
||||
const { videoId, reason } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/blacklist'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { reason },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
remove (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const { videoId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/blacklist'
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
sort?: string
|
||||
type?: VideoBlacklistType_Type
|
||||
} = {}) {
|
||||
const { sort, type } = options
|
||||
const path = '/api/v1/videos/blacklist/'
|
||||
|
||||
const query = { sort, type }
|
||||
|
||||
return this.getRequestBody<ResultList<VideoBlacklist>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
67
packages/server-commands/src/videos/captions-command.ts
Normal file
67
packages/server-commands/src/videos/captions-command.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { HttpStatusCode, ResultList, VideoCaption } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class CaptionsCommand extends AbstractCommand {
|
||||
|
||||
add (options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
language: string
|
||||
fixture: string
|
||||
mimeType?: string
|
||||
}) {
|
||||
const { videoId, language, fixture, mimeType } = options
|
||||
|
||||
const path = '/api/v1/videos/' + videoId + '/captions/' + language
|
||||
|
||||
const captionfile = buildAbsoluteFixturePath(fixture)
|
||||
const captionfileAttach = mimeType
|
||||
? [ captionfile, { contentType: mimeType } ]
|
||||
: captionfile
|
||||
|
||||
return this.putUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: {},
|
||||
attaches: {
|
||||
captionfile: captionfileAttach
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
videoPassword?: string
|
||||
}) {
|
||||
const { videoId, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/captions'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoCaption>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
headers: this.buildVideoPasswordHeader(videoPassword),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
language: string
|
||||
}) {
|
||||
const { videoId, language } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/captions/' + language
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { HttpStatusCode, ResultList, VideoChangeOwnership } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class ChangeOwnershipCommand extends AbstractCommand {
|
||||
|
||||
create (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
username: string
|
||||
}) {
|
||||
const { videoId, username } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/give-ownership'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { username },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/videos/ownership'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoChangeOwnership>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { sort: '-createdAt' },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
accept (options: OverrideCommandOptions & {
|
||||
ownershipId: number
|
||||
channelId: number
|
||||
}) {
|
||||
const { ownershipId, channelId } = options
|
||||
const path = '/api/v1/videos/ownership/' + ownershipId + '/accept'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { channelId },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
refuse (options: OverrideCommandOptions & {
|
||||
ownershipId: number
|
||||
}) {
|
||||
const { ownershipId } = options
|
||||
const path = '/api/v1/videos/ownership/' + ownershipId + '/refuse'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
55
packages/server-commands/src/videos/channel-syncs-command.ts
Normal file
55
packages/server-commands/src/videos/channel-syncs-command.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { HttpStatusCode, ResultList, VideoChannelSync, VideoChannelSyncCreate } from '@peertube/peertube-models'
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class ChannelSyncsCommand extends AbstractCommand {
|
||||
private static readonly API_PATH = '/api/v1/video-channel-syncs'
|
||||
|
||||
listByAccount (options: OverrideCommandOptions & {
|
||||
accountName: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
}) {
|
||||
const { accountName, sort = 'createdAt' } = options
|
||||
|
||||
const path = `/api/v1/accounts/${accountName}/video-channel-syncs`
|
||||
|
||||
return this.getRequestBody<ResultList<VideoChannelSync>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { sort, ...pick(options, [ 'start', 'count' ]) },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async create (options: OverrideCommandOptions & {
|
||||
attributes: VideoChannelSyncCreate
|
||||
}) {
|
||||
return unwrapBody<{ videoChannelSync: VideoChannelSync }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path: ChannelSyncsCommand.API_PATH,
|
||||
fields: options.attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
channelSyncId: number
|
||||
}) {
|
||||
const path = `${ChannelSyncsCommand.API_PATH}/${options.channelSyncId}`
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
202
packages/server-commands/src/videos/channels-command.ts
Normal file
202
packages/server-commands/src/videos/channels-command.ts
Normal file
|
@ -0,0 +1,202 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
ActorFollow,
|
||||
HttpStatusCode,
|
||||
ResultList,
|
||||
VideoChannel,
|
||||
VideoChannelCreate,
|
||||
VideoChannelCreateResult,
|
||||
VideoChannelUpdate,
|
||||
VideosImportInChannelCreate
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class ChannelsCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
withStats?: boolean
|
||||
} = {}) {
|
||||
const path = '/api/v1/video-channels'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoChannel>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'withStats' ]),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listByAccount (options: OverrideCommandOptions & {
|
||||
accountName: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
withStats?: boolean
|
||||
search?: string
|
||||
}) {
|
||||
const { accountName, sort = 'createdAt' } = options
|
||||
const path = '/api/v1/accounts/' + accountName + '/video-channels'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoChannel>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async create (options: OverrideCommandOptions & {
|
||||
attributes: Partial<VideoChannelCreate>
|
||||
}) {
|
||||
const path = '/api/v1/video-channels/'
|
||||
|
||||
// Default attributes
|
||||
const defaultAttributes = {
|
||||
displayName: 'my super video channel',
|
||||
description: 'my super channel description',
|
||||
support: 'my super channel support'
|
||||
}
|
||||
const attributes = { ...defaultAttributes, ...options.attributes }
|
||||
|
||||
const body = await unwrapBody<{ videoChannel: VideoChannelCreateResult }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
return body.videoChannel
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
channelName: string
|
||||
attributes: VideoChannelUpdate
|
||||
}) {
|
||||
const { channelName, attributes } = options
|
||||
const path = '/api/v1/video-channels/' + channelName
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
channelName: string
|
||||
}) {
|
||||
const path = '/api/v1/video-channels/' + options.channelName
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
channelName: string
|
||||
}) {
|
||||
const path = '/api/v1/video-channels/' + options.channelName
|
||||
|
||||
return this.getRequestBody<VideoChannel>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
updateImage (options: OverrideCommandOptions & {
|
||||
fixture: string
|
||||
channelName: string | number
|
||||
type: 'avatar' | 'banner'
|
||||
}) {
|
||||
const { channelName, fixture, type } = options
|
||||
|
||||
const path = `/api/v1/video-channels/${channelName}/${type}/pick`
|
||||
|
||||
return this.updateImageRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fixture,
|
||||
fieldname: type + 'file',
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
deleteImage (options: OverrideCommandOptions & {
|
||||
channelName: string | number
|
||||
type: 'avatar' | 'banner'
|
||||
}) {
|
||||
const { channelName, type } = options
|
||||
|
||||
const path = `/api/v1/video-channels/${channelName}/${type}`
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
listFollowers (options: OverrideCommandOptions & {
|
||||
channelName: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
}) {
|
||||
const { channelName, start, count, sort, search } = options
|
||||
const path = '/api/v1/video-channels/' + channelName + '/followers'
|
||||
|
||||
const query = { start, count, sort, search }
|
||||
|
||||
return this.getRequestBody<ResultList<ActorFollow>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
importVideos (options: OverrideCommandOptions & VideosImportInChannelCreate & {
|
||||
channelName: string
|
||||
}) {
|
||||
const { channelName, externalChannelUrl, videoChannelSyncId } = options
|
||||
|
||||
const path = `/api/v1/video-channels/${channelName}/import-videos`
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { externalChannelUrl, videoChannelSyncId },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
29
packages/server-commands/src/videos/channels.ts
Normal file
29
packages/server-commands/src/videos/channels.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { PeerTubeServer } from '../server/server.js'
|
||||
|
||||
function setDefaultVideoChannel (servers: PeerTubeServer[]) {
|
||||
const tasks: Promise<any>[] = []
|
||||
|
||||
for (const server of servers) {
|
||||
const p = server.users.getMyInfo()
|
||||
.then(user => { server.store.channel = user.videoChannels[0] })
|
||||
|
||||
tasks.push(p)
|
||||
}
|
||||
|
||||
return Promise.all(tasks)
|
||||
}
|
||||
|
||||
async function setDefaultChannelAvatar (serversArg: PeerTubeServer | PeerTubeServer[], channelName: string = 'root_channel') {
|
||||
const servers = Array.isArray(serversArg)
|
||||
? serversArg
|
||||
: [ serversArg ]
|
||||
|
||||
for (const server of servers) {
|
||||
await server.channels.updateImage({ channelName, fixture: 'avatar.png', type: 'avatar' })
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
setDefaultVideoChannel,
|
||||
setDefaultChannelAvatar
|
||||
}
|
159
packages/server-commands/src/videos/comments-command.ts
Normal file
159
packages/server-commands/src/videos/comments-command.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ResultList, VideoComment, VideoCommentThreads, VideoCommentThreadTree } from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class CommentsCommand extends AbstractCommand {
|
||||
|
||||
private lastVideoId: number | string
|
||||
private lastThreadId: number
|
||||
private lastReplyId: number
|
||||
|
||||
listForAdmin (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
isLocal?: boolean
|
||||
onLocalVideo?: boolean
|
||||
search?: string
|
||||
searchAccount?: string
|
||||
searchVideo?: string
|
||||
} = {}) {
|
||||
const { sort = '-createdAt' } = options
|
||||
const path = '/api/v1/videos/comments'
|
||||
|
||||
const query = { sort, ...pick(options, [ 'start', 'count', 'isLocal', 'onLocalVideo', 'search', 'searchAccount', 'searchVideo' ]) }
|
||||
|
||||
return this.getRequestBody<ResultList<VideoComment>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listThreads (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
videoPassword?: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
}) {
|
||||
const { start, count, sort, videoId, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
||||
|
||||
return this.getRequestBody<VideoCommentThreads>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { start, count, sort },
|
||||
headers: this.buildVideoPasswordHeader(videoPassword),
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getThread (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
threadId: number
|
||||
}) {
|
||||
const { videoId, threadId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comment-threads/' + threadId
|
||||
|
||||
return this.getRequestBody<VideoCommentThreadTree>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async createThread (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
videoPassword?: string
|
||||
}) {
|
||||
const { videoId, text, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comment-threads'
|
||||
|
||||
const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { text },
|
||||
headers: this.buildVideoPasswordHeader(videoPassword),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
this.lastThreadId = body.comment?.id
|
||||
this.lastVideoId = videoId
|
||||
|
||||
return body.comment
|
||||
}
|
||||
|
||||
async addReply (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
toCommentId: number
|
||||
text: string
|
||||
videoPassword?: string
|
||||
}) {
|
||||
const { videoId, toCommentId, text, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId
|
||||
|
||||
const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { text },
|
||||
headers: this.buildVideoPasswordHeader(videoPassword),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
this.lastReplyId = body.comment?.id
|
||||
|
||||
return body.comment
|
||||
}
|
||||
|
||||
async addReplyToLastReply (options: OverrideCommandOptions & {
|
||||
text: string
|
||||
}) {
|
||||
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastReplyId })
|
||||
}
|
||||
|
||||
async addReplyToLastThread (options: OverrideCommandOptions & {
|
||||
text: string
|
||||
}) {
|
||||
return this.addReply({ ...options, videoId: this.lastVideoId, toCommentId: this.lastThreadId })
|
||||
}
|
||||
|
||||
async findCommentId (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
text: string
|
||||
}) {
|
||||
const { videoId, text } = options
|
||||
const { data } = await this.listThreads({ videoId, count: 25, sort: '-createdAt' })
|
||||
|
||||
return data.find(c => c.text === text).id
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
commentId: number
|
||||
}) {
|
||||
const { videoId, commentId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/comments/' + commentId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
54
packages/server-commands/src/videos/history-command.ts
Normal file
54
packages/server-commands/src/videos/history-command.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { HttpStatusCode, ResultList, Video } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class HistoryCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
search?: string
|
||||
} = {}) {
|
||||
const { search } = options
|
||||
const path = '/api/v1/users/me/history/videos'
|
||||
|
||||
return this.getRequestBody<ResultList<Video>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
search
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
removeElement (options: OverrideCommandOptions & {
|
||||
videoId: number
|
||||
}) {
|
||||
const { videoId } = options
|
||||
const path = '/api/v1/users/me/history/videos/' + videoId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeAll (options: OverrideCommandOptions & {
|
||||
beforeDate?: string
|
||||
} = {}) {
|
||||
const { beforeDate } = options
|
||||
const path = '/api/v1/users/me/history/videos/remove'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { beforeDate },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
76
packages/server-commands/src/videos/imports-command.ts
Normal file
76
packages/server-commands/src/videos/imports-command.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
|
||||
import { HttpStatusCode, ResultList, VideoImport, VideoImportCreate } from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class ImportsCommand extends AbstractCommand {
|
||||
|
||||
importVideo (options: OverrideCommandOptions & {
|
||||
attributes: (VideoImportCreate | { torrentfile?: string, previewfile?: string, thumbnailfile?: string })
|
||||
}) {
|
||||
const { attributes } = options
|
||||
const path = '/api/v1/videos/imports'
|
||||
|
||||
let attaches: any = {}
|
||||
if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile }
|
||||
if (attributes.thumbnailfile) attaches = { thumbnailfile: attributes.thumbnailfile }
|
||||
if (attributes.previewfile) attaches = { previewfile: attributes.previewfile }
|
||||
|
||||
return unwrapBody<VideoImport>(this.postUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
attaches,
|
||||
fields: options.attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
importId: number
|
||||
}) {
|
||||
const path = '/api/v1/videos/imports/' + options.importId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
cancel (options: OverrideCommandOptions & {
|
||||
importId: number
|
||||
}) {
|
||||
const path = '/api/v1/videos/imports/' + options.importId + '/cancel'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
getMyVideoImports (options: OverrideCommandOptions & {
|
||||
sort?: string
|
||||
targetUrl?: string
|
||||
videoChannelSyncId?: number
|
||||
search?: string
|
||||
} = {}) {
|
||||
const { sort, targetUrl, videoChannelSyncId, search } = options
|
||||
const path = '/api/v1/users/me/videos/imports'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoImport>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { sort, targetUrl, videoChannelSyncId, search },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
22
packages/server-commands/src/videos/index.ts
Normal file
22
packages/server-commands/src/videos/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
export * from './blacklist-command.js'
|
||||
export * from './captions-command.js'
|
||||
export * from './change-ownership-command.js'
|
||||
export * from './channels.js'
|
||||
export * from './channels-command.js'
|
||||
export * from './channel-syncs-command.js'
|
||||
export * from './comments-command.js'
|
||||
export * from './history-command.js'
|
||||
export * from './imports-command.js'
|
||||
export * from './live-command.js'
|
||||
export * from './live.js'
|
||||
export * from './playlists-command.js'
|
||||
export * from './services-command.js'
|
||||
export * from './storyboard-command.js'
|
||||
export * from './streaming-playlists-command.js'
|
||||
export * from './comments-command.js'
|
||||
export * from './video-studio-command.js'
|
||||
export * from './video-token-command.js'
|
||||
export * from './views-command.js'
|
||||
export * from './videos-command.js'
|
||||
export * from './video-passwords-command.js'
|
||||
export * from './video-stats-command.js'
|
339
packages/server-commands/src/videos/live-command.ts
Normal file
339
packages/server-commands/src/videos/live-command.ts
Normal file
|
@ -0,0 +1,339 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { readdir } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { omit, wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
LiveVideo,
|
||||
LiveVideoCreate,
|
||||
LiveVideoSession,
|
||||
LiveVideoUpdate,
|
||||
ResultList,
|
||||
VideoCreateResult,
|
||||
VideoDetails,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
VideoState,
|
||||
VideoStateType
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { ObjectStorageCommand, PeerTubeServer } from '../server/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
import { sendRTMPStream, testFfmpegStreamError } from './live.js'
|
||||
|
||||
export class LiveCommand extends AbstractCommand {
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/live'
|
||||
|
||||
return this.getRequestBody<LiveVideo>({
|
||||
...options,
|
||||
|
||||
path: path + '/' + options.videoId,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
listSessions (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = `/api/v1/videos/live/${options.videoId}/sessions`
|
||||
|
||||
return this.getRequestBody<ResultList<LiveVideoSession>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async findLatestSession (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const { data: sessions } = await this.listSessions(options)
|
||||
|
||||
return sessions[sessions.length - 1]
|
||||
}
|
||||
|
||||
getReplaySession (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = `/api/v1/videos/${options.videoId}/live-session`
|
||||
|
||||
return this.getRequestBody<LiveVideoSession>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
fields: LiveVideoUpdate
|
||||
}) {
|
||||
const { videoId, fields } = options
|
||||
const path = '/api/v1/videos/live'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path: path + '/' + videoId,
|
||||
fields,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async create (options: OverrideCommandOptions & {
|
||||
fields: LiveVideoCreate
|
||||
}) {
|
||||
const { fields } = options
|
||||
const path = '/api/v1/videos/live'
|
||||
|
||||
const attaches: any = {}
|
||||
if (fields.thumbnailfile) attaches.thumbnailfile = fields.thumbnailfile
|
||||
if (fields.previewfile) attaches.previewfile = fields.previewfile
|
||||
|
||||
const body = await unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
attaches,
|
||||
fields: omit(fields, [ 'thumbnailfile', 'previewfile' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
return body.video
|
||||
}
|
||||
|
||||
async quickCreate (options: OverrideCommandOptions & {
|
||||
saveReplay: boolean
|
||||
permanentLive: boolean
|
||||
privacy?: VideoPrivacyType
|
||||
videoPasswords?: string[]
|
||||
}) {
|
||||
const { saveReplay, permanentLive, privacy = VideoPrivacy.PUBLIC, videoPasswords } = options
|
||||
|
||||
const replaySettings = privacy === VideoPrivacy.PASSWORD_PROTECTED
|
||||
? { privacy: VideoPrivacy.PRIVATE }
|
||||
: { privacy }
|
||||
|
||||
const { uuid } = await this.create({
|
||||
...options,
|
||||
|
||||
fields: {
|
||||
name: 'live',
|
||||
permanentLive,
|
||||
saveReplay,
|
||||
replaySettings,
|
||||
channelId: this.server.store.channel.id,
|
||||
privacy,
|
||||
videoPasswords
|
||||
}
|
||||
})
|
||||
|
||||
const video = await this.server.videos.getWithToken({ id: uuid })
|
||||
const live = await this.get({ videoId: uuid })
|
||||
|
||||
return { video, live }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async sendRTMPStreamInVideo (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
fixtureName?: string
|
||||
copyCodecs?: boolean
|
||||
}) {
|
||||
const { videoId, fixtureName, copyCodecs } = options
|
||||
const videoLive = await this.get({ videoId })
|
||||
|
||||
return sendRTMPStream({ rtmpBaseUrl: videoLive.rtmpUrl, streamKey: videoLive.streamKey, fixtureName, copyCodecs })
|
||||
}
|
||||
|
||||
async runAndTestStreamError (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
shouldHaveError: boolean
|
||||
}) {
|
||||
const command = await this.sendRTMPStreamInVideo(options)
|
||||
|
||||
return testFfmpegStreamError(command, options.shouldHaveError)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
waitUntilPublished (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const { videoId } = options
|
||||
return this.waitUntilState({ videoId, state: VideoState.PUBLISHED })
|
||||
}
|
||||
|
||||
waitUntilWaiting (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const { videoId } = options
|
||||
return this.waitUntilState({ videoId, state: VideoState.WAITING_FOR_LIVE })
|
||||
}
|
||||
|
||||
waitUntilEnded (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const { videoId } = options
|
||||
return this.waitUntilState({ videoId, state: VideoState.LIVE_ENDED })
|
||||
}
|
||||
|
||||
async waitUntilSegmentGeneration (options: OverrideCommandOptions & {
|
||||
server: PeerTubeServer
|
||||
videoUUID: string
|
||||
playlistNumber: number
|
||||
segment: number
|
||||
objectStorage?: ObjectStorageCommand
|
||||
objectStorageBaseUrl?: string
|
||||
}) {
|
||||
const {
|
||||
server,
|
||||
objectStorage,
|
||||
playlistNumber,
|
||||
segment,
|
||||
videoUUID,
|
||||
objectStorageBaseUrl
|
||||
} = options
|
||||
|
||||
const segmentName = `${playlistNumber}-00000${segment}.ts`
|
||||
const baseUrl = objectStorage
|
||||
? join(objectStorageBaseUrl || objectStorage.getMockPlaylistBaseUrl(), 'hls')
|
||||
: server.url + '/static/streaming-playlists/hls'
|
||||
|
||||
let error = true
|
||||
|
||||
while (error) {
|
||||
try {
|
||||
// Check fragment exists
|
||||
await this.getRawRequest({
|
||||
...options,
|
||||
|
||||
url: `${baseUrl}/${videoUUID}/${segmentName}`,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
const video = await server.videos.get({ id: videoUUID })
|
||||
const hlsPlaylist = video.streamingPlaylists[0]
|
||||
|
||||
// Check SHA generation
|
||||
const shaBody = await server.streamingPlaylists.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url, withRetry: !!objectStorage })
|
||||
if (!shaBody[segmentName]) {
|
||||
throw new Error('Segment SHA does not exist')
|
||||
}
|
||||
|
||||
// Check fragment is in m3u8 playlist
|
||||
const subPlaylist = await server.streamingPlaylists.get({ url: `${baseUrl}/${video.uuid}/${playlistNumber}.m3u8` })
|
||||
if (!subPlaylist.includes(segmentName)) throw new Error('Fragment does not exist in playlist')
|
||||
|
||||
error = false
|
||||
} catch {
|
||||
error = true
|
||||
await wait(100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async waitUntilReplacedByReplay (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
let video: VideoDetails
|
||||
|
||||
do {
|
||||
video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
|
||||
|
||||
await wait(500)
|
||||
} while (video.isLive === true || video.state.id !== VideoState.PUBLISHED)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getSegmentFile (options: OverrideCommandOptions & {
|
||||
videoUUID: string
|
||||
playlistNumber: number
|
||||
segment: number
|
||||
objectStorage?: ObjectStorageCommand
|
||||
}) {
|
||||
const { playlistNumber, segment, videoUUID, objectStorage } = options
|
||||
|
||||
const segmentName = `${playlistNumber}-00000${segment}.ts`
|
||||
const baseUrl = objectStorage
|
||||
? objectStorage.getMockPlaylistBaseUrl()
|
||||
: `${this.server.url}/static/streaming-playlists/hls`
|
||||
|
||||
const url = `${baseUrl}/${videoUUID}/${segmentName}`
|
||||
|
||||
return this.getRawRequest({
|
||||
...options,
|
||||
|
||||
url,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getPlaylistFile (options: OverrideCommandOptions & {
|
||||
videoUUID: string
|
||||
playlistName: string
|
||||
objectStorage?: ObjectStorageCommand
|
||||
}) {
|
||||
const { playlistName, videoUUID, objectStorage } = options
|
||||
|
||||
const baseUrl = objectStorage
|
||||
? objectStorage.getMockPlaylistBaseUrl()
|
||||
: `${this.server.url}/static/streaming-playlists/hls`
|
||||
|
||||
const url = `${baseUrl}/${videoUUID}/${playlistName}`
|
||||
|
||||
return this.getRawRequest({
|
||||
...options,
|
||||
|
||||
url,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async countPlaylists (options: OverrideCommandOptions & {
|
||||
videoUUID: string
|
||||
}) {
|
||||
const basePath = this.server.servers.buildDirectory('streaming-playlists')
|
||||
const hlsPath = join(basePath, 'hls', options.videoUUID)
|
||||
|
||||
const files = await readdir(hlsPath)
|
||||
|
||||
return files.filter(f => f.endsWith('.m3u8')).length
|
||||
}
|
||||
|
||||
private async waitUntilState (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
state: VideoStateType
|
||||
}) {
|
||||
let video: VideoDetails
|
||||
|
||||
do {
|
||||
video = await this.server.videos.getWithToken({ token: options.token, id: options.videoId })
|
||||
|
||||
await wait(500)
|
||||
} while (video.state.id !== options.state)
|
||||
}
|
||||
}
|
129
packages/server-commands/src/videos/live.ts
Normal file
129
packages/server-commands/src/videos/live.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { VideoDetails, VideoInclude, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg'
|
||||
import truncate from 'lodash-es/truncate.js'
|
||||
import { PeerTubeServer } from '../server/server.js'
|
||||
|
||||
function sendRTMPStream (options: {
|
||||
rtmpBaseUrl: string
|
||||
streamKey: string
|
||||
fixtureName?: string // default video_short.mp4
|
||||
copyCodecs?: boolean // default false
|
||||
}) {
|
||||
const { rtmpBaseUrl, streamKey, fixtureName = 'video_short.mp4', copyCodecs = false } = options
|
||||
|
||||
const fixture = buildAbsoluteFixturePath(fixtureName)
|
||||
|
||||
const command = ffmpeg(fixture)
|
||||
command.inputOption('-stream_loop -1')
|
||||
command.inputOption('-re')
|
||||
|
||||
if (copyCodecs) {
|
||||
command.outputOption('-c copy')
|
||||
} else {
|
||||
command.outputOption('-c:v libx264')
|
||||
command.outputOption('-g 120')
|
||||
command.outputOption('-x264-params "no-scenecut=1"')
|
||||
command.outputOption('-r 60')
|
||||
}
|
||||
|
||||
command.outputOption('-f flv')
|
||||
|
||||
const rtmpUrl = rtmpBaseUrl + '/' + streamKey
|
||||
command.output(rtmpUrl)
|
||||
|
||||
command.on('error', err => {
|
||||
if (err?.message?.includes('Exiting normally')) return
|
||||
|
||||
if (process.env.DEBUG) console.error(err)
|
||||
})
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
command.on('stderr', data => console.log(data))
|
||||
command.on('stdout', data => console.log(data))
|
||||
}
|
||||
|
||||
command.run()
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) {
|
||||
return new Promise<void>((res, rej) => {
|
||||
command.on('error', err => {
|
||||
return rej(err)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
res()
|
||||
}, successAfterMS)
|
||||
})
|
||||
}
|
||||
|
||||
async function testFfmpegStreamError (command: FfmpegCommand, shouldHaveError: boolean) {
|
||||
let error: Error
|
||||
|
||||
try {
|
||||
await waitFfmpegUntilError(command, 45000)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
|
||||
await stopFfmpeg(command)
|
||||
|
||||
if (shouldHaveError && !error) throw new Error('Ffmpeg did not have an error')
|
||||
if (!shouldHaveError && error) throw error
|
||||
}
|
||||
|
||||
async function stopFfmpeg (command: FfmpegCommand) {
|
||||
command.kill('SIGINT')
|
||||
|
||||
await wait(500)
|
||||
}
|
||||
|
||||
async function waitUntilLivePublishedOnAllServers (servers: PeerTubeServer[], videoId: string) {
|
||||
for (const server of servers) {
|
||||
await server.live.waitUntilPublished({ videoId })
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntilLiveWaitingOnAllServers (servers: PeerTubeServer[], videoId: string) {
|
||||
for (const server of servers) {
|
||||
await server.live.waitUntilWaiting({ videoId })
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntilLiveReplacedByReplayOnAllServers (servers: PeerTubeServer[], videoId: string) {
|
||||
for (const server of servers) {
|
||||
await server.live.waitUntilReplacedByReplay({ videoId })
|
||||
}
|
||||
}
|
||||
|
||||
async function findExternalSavedVideo (server: PeerTubeServer, liveDetails: VideoDetails) {
|
||||
const include = VideoInclude.BLACKLISTED
|
||||
const privacyOneOf = [ VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.PUBLIC, VideoPrivacy.UNLISTED ]
|
||||
|
||||
const { data } = await server.videos.list({ token: server.accessToken, sort: '-publishedAt', include, privacyOneOf })
|
||||
|
||||
const videoNameSuffix = ` - ${new Date(liveDetails.publishedAt).toLocaleString()}`
|
||||
const truncatedVideoName = truncate(liveDetails.name, {
|
||||
length: 120 - videoNameSuffix.length
|
||||
})
|
||||
const toFind = truncatedVideoName + videoNameSuffix
|
||||
|
||||
return data.find(v => v.name === toFind)
|
||||
}
|
||||
|
||||
export {
|
||||
sendRTMPStream,
|
||||
waitFfmpegUntilError,
|
||||
testFfmpegStreamError,
|
||||
stopFfmpeg,
|
||||
|
||||
waitUntilLivePublishedOnAllServers,
|
||||
waitUntilLiveReplacedByReplayOnAllServers,
|
||||
waitUntilLiveWaitingOnAllServers,
|
||||
|
||||
findExternalSavedVideo
|
||||
}
|
281
packages/server-commands/src/videos/playlists-command.ts
Normal file
281
packages/server-commands/src/videos/playlists-command.ts
Normal file
|
@ -0,0 +1,281 @@
|
|||
import { omit, pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
BooleanBothQuery,
|
||||
HttpStatusCode,
|
||||
ResultList,
|
||||
VideoExistInPlaylist,
|
||||
VideoPlaylist,
|
||||
VideoPlaylistCreate,
|
||||
VideoPlaylistCreateResult,
|
||||
VideoPlaylistElement,
|
||||
VideoPlaylistElementCreate,
|
||||
VideoPlaylistElementCreateResult,
|
||||
VideoPlaylistElementUpdate,
|
||||
VideoPlaylistReorder,
|
||||
VideoPlaylistType_Type,
|
||||
VideoPlaylistUpdate
|
||||
} from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class PlaylistsCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
playlistType?: VideoPlaylistType_Type
|
||||
}) {
|
||||
const path = '/api/v1/video-playlists'
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'playlistType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<VideoPlaylist>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listByChannel (options: OverrideCommandOptions & {
|
||||
handle: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
playlistType?: VideoPlaylistType_Type
|
||||
}) {
|
||||
const path = '/api/v1/video-channels/' + options.handle + '/video-playlists'
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'playlistType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<VideoPlaylist>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listByAccount (options: OverrideCommandOptions & {
|
||||
handle: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
playlistType?: VideoPlaylistType_Type
|
||||
}) {
|
||||
const path = '/api/v1/accounts/' + options.handle + '/video-playlists'
|
||||
const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ])
|
||||
|
||||
return this.getRequestBody<ResultList<VideoPlaylist>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
playlistId: number | string
|
||||
}) {
|
||||
const { playlistId } = options
|
||||
const path = '/api/v1/video-playlists/' + playlistId
|
||||
|
||||
return this.getRequestBody<VideoPlaylist>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listVideos (options: OverrideCommandOptions & {
|
||||
playlistId: number | string
|
||||
start?: number
|
||||
count?: number
|
||||
query?: { nsfw?: BooleanBothQuery }
|
||||
}) {
|
||||
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
|
||||
const query = options.query ?? {}
|
||||
|
||||
return this.getRequestBody<ResultList<VideoPlaylistElement>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: {
|
||||
...query,
|
||||
start: options.start,
|
||||
count: options.count
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
playlistId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/video-playlists/' + options.playlistId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async create (options: OverrideCommandOptions & {
|
||||
attributes: VideoPlaylistCreate
|
||||
}) {
|
||||
const path = '/api/v1/video-playlists'
|
||||
|
||||
const fields = omit(options.attributes, [ 'thumbnailfile' ])
|
||||
|
||||
const attaches = options.attributes.thumbnailfile
|
||||
? { thumbnailfile: options.attributes.thumbnailfile }
|
||||
: {}
|
||||
|
||||
const body = await unwrapBody<{ videoPlaylist: VideoPlaylistCreateResult }>(this.postUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields,
|
||||
attaches,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
return body.videoPlaylist
|
||||
}
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
attributes: VideoPlaylistUpdate
|
||||
playlistId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/video-playlists/' + options.playlistId
|
||||
|
||||
const fields = omit(options.attributes, [ 'thumbnailfile' ])
|
||||
|
||||
const attaches = options.attributes.thumbnailfile
|
||||
? { thumbnailfile: options.attributes.thumbnailfile }
|
||||
: {}
|
||||
|
||||
return this.putUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields,
|
||||
attaches,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async addElement (options: OverrideCommandOptions & {
|
||||
playlistId: number | string
|
||||
attributes: VideoPlaylistElementCreate | { videoId: string }
|
||||
}) {
|
||||
const attributes = {
|
||||
...options.attributes,
|
||||
|
||||
videoId: await this.server.videos.getId({ ...options, uuid: options.attributes.videoId })
|
||||
}
|
||||
|
||||
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos'
|
||||
|
||||
const body = await unwrapBody<{ videoPlaylistElement: VideoPlaylistElementCreateResult }>(this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
return body.videoPlaylistElement
|
||||
}
|
||||
|
||||
updateElement (options: OverrideCommandOptions & {
|
||||
playlistId: number | string
|
||||
elementId: number | string
|
||||
attributes: VideoPlaylistElementUpdate
|
||||
}) {
|
||||
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeElement (options: OverrideCommandOptions & {
|
||||
playlistId: number | string
|
||||
elementId: number
|
||||
}) {
|
||||
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.elementId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
reorderElements (options: OverrideCommandOptions & {
|
||||
playlistId: number | string
|
||||
attributes: VideoPlaylistReorder
|
||||
}) {
|
||||
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/reorder'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
getPrivacies (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/video-playlists/privacies'
|
||||
|
||||
return this.getRequestBody<{ [ id: number ]: string }>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
videosExist (options: OverrideCommandOptions & {
|
||||
videoIds: number[]
|
||||
}) {
|
||||
const { videoIds } = options
|
||||
const path = '/api/v1/users/me/video-playlists/videos-exist'
|
||||
|
||||
return this.getRequestBody<VideoExistInPlaylist>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { videoIds },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
29
packages/server-commands/src/videos/services-command.ts
Normal file
29
packages/server-commands/src/videos/services-command.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class ServicesCommand extends AbstractCommand {
|
||||
|
||||
getOEmbed (options: OverrideCommandOptions & {
|
||||
oembedUrl: string
|
||||
format?: string
|
||||
maxHeight?: number
|
||||
maxWidth?: number
|
||||
}) {
|
||||
const path = '/services/oembed'
|
||||
const query = {
|
||||
url: options.oembedUrl,
|
||||
format: options.format,
|
||||
maxheight: options.maxHeight,
|
||||
maxwidth: options.maxWidth
|
||||
}
|
||||
|
||||
return this.getRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
19
packages/server-commands/src/videos/storyboard-command.ts
Normal file
19
packages/server-commands/src/videos/storyboard-command.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { HttpStatusCode, Storyboard } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class StoryboardCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.id + '/storyboards'
|
||||
|
||||
return this.getRequestBody<{ storyboards: Storyboard[] }>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import { wait } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { unwrapBody, unwrapBodyOrDecodeToJSON, unwrapTextOrDecode } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class StreamingPlaylistsCommand extends AbstractCommand {
|
||||
|
||||
async get (options: OverrideCommandOptions & {
|
||||
url: string
|
||||
|
||||
videoFileToken?: string
|
||||
reinjectVideoFileToken?: boolean
|
||||
|
||||
withRetry?: boolean // default false
|
||||
currentRetry?: number
|
||||
}): Promise<string> {
|
||||
const { videoFileToken, reinjectVideoFileToken, expectedStatus, withRetry = false, currentRetry = 1 } = options
|
||||
|
||||
try {
|
||||
const result = await unwrapTextOrDecode(this.getRawRequest({
|
||||
...options,
|
||||
|
||||
url: options.url,
|
||||
query: {
|
||||
videoFileToken,
|
||||
reinjectVideoFileToken
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
// master.m3u8 could be empty
|
||||
if (!result && (!expectedStatus || expectedStatus === HttpStatusCode.OK_200)) {
|
||||
throw new Error('Empty result')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
if (!withRetry || currentRetry > 10) throw err
|
||||
|
||||
await wait(250)
|
||||
|
||||
return this.get({
|
||||
...options,
|
||||
|
||||
withRetry,
|
||||
currentRetry: currentRetry + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async getFragmentedSegment (options: OverrideCommandOptions & {
|
||||
url: string
|
||||
range?: string
|
||||
|
||||
withRetry?: boolean // default false
|
||||
currentRetry?: number
|
||||
}) {
|
||||
const { withRetry = false, currentRetry = 1 } = options
|
||||
|
||||
try {
|
||||
const result = await unwrapBody<Buffer>(this.getRawRequest({
|
||||
...options,
|
||||
|
||||
url: options.url,
|
||||
range: options.range,
|
||||
implicitToken: false,
|
||||
responseType: 'application/octet-stream',
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
if (!withRetry || currentRetry > 10) throw err
|
||||
|
||||
await wait(250)
|
||||
|
||||
return this.getFragmentedSegment({
|
||||
...options,
|
||||
|
||||
withRetry,
|
||||
currentRetry: currentRetry + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async getSegmentSha256 (options: OverrideCommandOptions & {
|
||||
url: string
|
||||
|
||||
withRetry?: boolean // default false
|
||||
currentRetry?: number
|
||||
}) {
|
||||
const { withRetry = false, currentRetry = 1 } = options
|
||||
|
||||
try {
|
||||
const result = await unwrapBodyOrDecodeToJSON<{ [ id: string ]: string }>(this.getRawRequest({
|
||||
...options,
|
||||
|
||||
url: options.url,
|
||||
contentType: 'application/json',
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
if (!withRetry || currentRetry > 10) throw err
|
||||
|
||||
await wait(250)
|
||||
|
||||
return this.getSegmentSha256({
|
||||
...options,
|
||||
|
||||
withRetry,
|
||||
currentRetry: currentRetry + 1
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { HttpStatusCode, ResultList, VideoPassword } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class VideoPasswordsCommand extends AbstractCommand {
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
}) {
|
||||
const { start, count, sort, videoId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/passwords'
|
||||
|
||||
return this.getRequestBody<ResultList<VideoPassword>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { start, count, sort },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
updateAll (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
passwords: string[]
|
||||
}) {
|
||||
const { videoId, passwords } = options
|
||||
const path = `/api/v1/videos/${videoId}/passwords`
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
path,
|
||||
fields: { passwords },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
remove (options: OverrideCommandOptions & {
|
||||
id: number
|
||||
videoId: number | string
|
||||
}) {
|
||||
const { id, videoId } = options
|
||||
const path = `/api/v1/videos/${videoId}/passwords/${id}`
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
62
packages/server-commands/src/videos/video-stats-command.ts
Normal file
62
packages/server-commands/src/videos/video-stats-command.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
VideoStatsOverall,
|
||||
VideoStatsRetention,
|
||||
VideoStatsTimeserie,
|
||||
VideoStatsTimeserieMetric
|
||||
} from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class VideoStatsCommand extends AbstractCommand {
|
||||
|
||||
getOverallStats (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/stats/overall'
|
||||
|
||||
return this.getRequestBody<VideoStatsOverall>({
|
||||
...options,
|
||||
path,
|
||||
|
||||
query: pick(options, [ 'startDate', 'endDate' ]),
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getTimeserieStats (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
metric: VideoStatsTimeserieMetric
|
||||
startDate?: Date
|
||||
endDate?: Date
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/stats/timeseries/' + options.metric
|
||||
|
||||
return this.getRequestBody<VideoStatsTimeserie>({
|
||||
...options,
|
||||
path,
|
||||
|
||||
query: pick(options, [ 'startDate', 'endDate' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getRetentionStats (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/stats/retention'
|
||||
|
||||
return this.getRequestBody<VideoStatsRetention>({
|
||||
...options,
|
||||
path,
|
||||
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
}
|
67
packages/server-commands/src/videos/video-studio-command.ts
Normal file
67
packages/server-commands/src/videos/video-studio-command.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { HttpStatusCode, VideoStudioTask } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class VideoStudioCommand extends AbstractCommand {
|
||||
|
||||
static getComplexTask (): VideoStudioTask[] {
|
||||
return [
|
||||
// Total duration: 2
|
||||
{
|
||||
name: 'cut',
|
||||
options: {
|
||||
start: 1,
|
||||
end: 3
|
||||
}
|
||||
},
|
||||
|
||||
// Total duration: 7
|
||||
{
|
||||
name: 'add-outro',
|
||||
options: {
|
||||
file: 'video_short.webm'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
name: 'add-watermark',
|
||||
options: {
|
||||
file: 'custom-thumbnail.png'
|
||||
}
|
||||
},
|
||||
|
||||
// Total duration: 9
|
||||
{
|
||||
name: 'add-intro',
|
||||
options: {
|
||||
file: 'video_very_short_240p.mp4'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
createEditionTasks (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
tasks: VideoStudioTask[]
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/studio/edit'
|
||||
const attaches: { [id: string]: any } = {}
|
||||
|
||||
for (let i = 0; i < options.tasks.length; i++) {
|
||||
const task = options.tasks[i]
|
||||
|
||||
if (task.name === 'add-intro' || task.name === 'add-outro' || task.name === 'add-watermark') {
|
||||
attaches[`tasks[${i}][options][file]`] = task.options.file
|
||||
}
|
||||
}
|
||||
|
||||
return this.postUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
attaches,
|
||||
fields: { tasks: options.tasks },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
34
packages/server-commands/src/videos/video-token-command.ts
Normal file
34
packages/server-commands/src/videos/video-token-command.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
|
||||
|
||||
import { HttpStatusCode, VideoToken } from '@peertube/peertube-models'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class VideoTokenCommand extends AbstractCommand {
|
||||
|
||||
create (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
videoPassword?: string
|
||||
}) {
|
||||
const { videoId, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/token'
|
||||
|
||||
return unwrapBody<VideoToken>(this.postBodyRequest({
|
||||
...options,
|
||||
headers: this.buildVideoPasswordHeader(videoPassword),
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
async getVideoFileToken (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
videoPassword?: string
|
||||
}) {
|
||||
const { files } = await this.create(options)
|
||||
|
||||
return files.token
|
||||
}
|
||||
}
|
831
packages/server-commands/src/videos/videos-command.ts
Normal file
831
packages/server-commands/src/videos/videos-command.ts
Normal file
|
@ -0,0 +1,831 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
|
||||
|
||||
import { expect } from 'chai'
|
||||
import { createReadStream } from 'fs'
|
||||
import { stat } from 'fs/promises'
|
||||
import got, { Response as GotResponse } from 'got'
|
||||
import validator from 'validator'
|
||||
import { getAllPrivacies, omit, pick, wait } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
ResultList,
|
||||
UserVideoRateType,
|
||||
Video,
|
||||
VideoCreate,
|
||||
VideoCreateResult,
|
||||
VideoDetails,
|
||||
VideoFileMetadata,
|
||||
VideoInclude,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
VideosCommonQuery,
|
||||
VideoSource,
|
||||
VideoTranscodingCreate
|
||||
} from '@peertube/peertube-models'
|
||||
import { buildAbsoluteFixturePath, buildUUID } from '@peertube/peertube-node-utils'
|
||||
import { unwrapBody } from '../requests/index.js'
|
||||
import { waitJobs } from '../server/jobs.js'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export type VideoEdit = Partial<Omit<VideoCreate, 'thumbnailfile' | 'previewfile'>> & {
|
||||
fixture?: string
|
||||
thumbnailfile?: string
|
||||
previewfile?: string
|
||||
}
|
||||
|
||||
export class VideosCommand extends AbstractCommand {
|
||||
|
||||
getCategories (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/videos/categories'
|
||||
|
||||
return this.getRequestBody<{ [id: number]: string }>({
|
||||
...options,
|
||||
path,
|
||||
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getLicences (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/videos/licences'
|
||||
|
||||
return this.getRequestBody<{ [id: number]: string }>({
|
||||
...options,
|
||||
path,
|
||||
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getLanguages (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/videos/languages'
|
||||
|
||||
return this.getRequestBody<{ [id: string]: string }>({
|
||||
...options,
|
||||
path,
|
||||
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getPrivacies (options: OverrideCommandOptions = {}) {
|
||||
const path = '/api/v1/videos/privacies'
|
||||
|
||||
return this.getRequestBody<{ [id in VideoPrivacyType]: string }>({
|
||||
...options,
|
||||
path,
|
||||
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getDescription (options: OverrideCommandOptions & {
|
||||
descriptionPath: string
|
||||
}) {
|
||||
return this.getRequestBody<{ description: string }>({
|
||||
...options,
|
||||
path: options.descriptionPath,
|
||||
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getFileMetadata (options: OverrideCommandOptions & {
|
||||
url: string
|
||||
}) {
|
||||
return unwrapBody<VideoFileMetadata>(this.getRawRequest({
|
||||
...options,
|
||||
|
||||
url: options.url,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
}))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
rate (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
rating: UserVideoRateType
|
||||
videoPassword?: string
|
||||
}) {
|
||||
const { id, rating, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + id + '/rate'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: { rating },
|
||||
headers: this.buildVideoPasswordHeader(videoPassword),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
get (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.id
|
||||
|
||||
return this.getRequestBody<VideoDetails>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getWithToken (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
}) {
|
||||
return this.get({
|
||||
...options,
|
||||
|
||||
token: this.buildCommonRequestToken({ ...options, implicitToken: true })
|
||||
})
|
||||
}
|
||||
|
||||
getWithPassword (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
password?: string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.id
|
||||
|
||||
return this.getRequestBody<VideoDetails>({
|
||||
...options,
|
||||
headers:{
|
||||
'x-peertube-video-password': options.password
|
||||
},
|
||||
path,
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
getSource (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.id + '/source'
|
||||
|
||||
return this.getRequestBody<VideoSource>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
async getId (options: OverrideCommandOptions & {
|
||||
uuid: number | string
|
||||
}) {
|
||||
const { uuid } = options
|
||||
|
||||
if (validator.default.isUUID('' + uuid) === false) return uuid as number
|
||||
|
||||
const { id } = await this.get({ ...options, id: uuid })
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
async listFiles (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
}) {
|
||||
const video = await this.get(options)
|
||||
|
||||
const files = video.files || []
|
||||
const hlsFiles = video.streamingPlaylists[0]?.files || []
|
||||
|
||||
return files.concat(hlsFiles)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
listMyVideos (options: OverrideCommandOptions & {
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
search?: string
|
||||
isLive?: boolean
|
||||
channelId?: number
|
||||
} = {}) {
|
||||
const path = '/api/v1/users/me/videos'
|
||||
|
||||
return this.getRequestBody<ResultList<Video>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listMySubscriptionVideos (options: OverrideCommandOptions & VideosCommonQuery = {}) {
|
||||
const { sort = '-createdAt' } = options
|
||||
const path = '/api/v1/users/me/subscriptions/videos'
|
||||
|
||||
return this.getRequestBody<ResultList<Video>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { sort, ...this.buildListQuery(options) },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
list (options: OverrideCommandOptions & VideosCommonQuery = {}) {
|
||||
const path = '/api/v1/videos'
|
||||
|
||||
const query = this.buildListQuery(options)
|
||||
|
||||
return this.getRequestBody<ResultList<Video>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { sort: 'name', ...query },
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listWithToken (options: OverrideCommandOptions & VideosCommonQuery = {}) {
|
||||
return this.list({
|
||||
...options,
|
||||
|
||||
token: this.buildCommonRequestToken({ ...options, implicitToken: true })
|
||||
})
|
||||
}
|
||||
|
||||
listAllForAdmin (options: OverrideCommandOptions & VideosCommonQuery = {}) {
|
||||
const include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED | VideoInclude.BLOCKED_OWNER
|
||||
const nsfw = 'both'
|
||||
const privacyOneOf = getAllPrivacies()
|
||||
|
||||
return this.list({
|
||||
...options,
|
||||
|
||||
include,
|
||||
nsfw,
|
||||
privacyOneOf,
|
||||
|
||||
token: this.buildCommonRequestToken({ ...options, implicitToken: true })
|
||||
})
|
||||
}
|
||||
|
||||
listByAccount (options: OverrideCommandOptions & VideosCommonQuery & {
|
||||
handle: string
|
||||
}) {
|
||||
const { handle, search } = options
|
||||
const path = '/api/v1/accounts/' + handle + '/videos'
|
||||
|
||||
return this.getRequestBody<ResultList<Video>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { search, ...this.buildListQuery(options) },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
listByChannel (options: OverrideCommandOptions & VideosCommonQuery & {
|
||||
handle: string
|
||||
}) {
|
||||
const { handle } = options
|
||||
const path = '/api/v1/video-channels/' + handle + '/videos'
|
||||
|
||||
return this.getRequestBody<ResultList<Video>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: this.buildListQuery(options),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async find (options: OverrideCommandOptions & {
|
||||
name: string
|
||||
}) {
|
||||
const { data } = await this.list(options)
|
||||
|
||||
return data.find(v => v.name === options.name)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
update (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
attributes?: VideoEdit
|
||||
}) {
|
||||
const { id, attributes = {} } = options
|
||||
const path = '/api/v1/videos/' + id
|
||||
|
||||
// Upload request
|
||||
if (attributes.thumbnailfile || attributes.previewfile) {
|
||||
const attaches: any = {}
|
||||
if (attributes.thumbnailfile) attaches.thumbnailfile = attributes.thumbnailfile
|
||||
if (attributes.previewfile) attaches.previewfile = attributes.previewfile
|
||||
|
||||
return this.putUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.attributes,
|
||||
attaches: {
|
||||
thumbnailfile: attributes.thumbnailfile,
|
||||
previewfile: attributes.previewfile
|
||||
},
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: options.attributes,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
remove (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.id
|
||||
|
||||
return unwrapBody(this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
}))
|
||||
}
|
||||
|
||||
async removeAll () {
|
||||
const { data } = await this.list()
|
||||
|
||||
for (const v of data) {
|
||||
await this.remove({ id: v.id })
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async upload (options: OverrideCommandOptions & {
|
||||
attributes?: VideoEdit
|
||||
mode?: 'legacy' | 'resumable' // default legacy
|
||||
waitTorrentGeneration?: boolean // default true
|
||||
completedExpectedStatus?: HttpStatusCodeType
|
||||
} = {}) {
|
||||
const { mode = 'legacy', waitTorrentGeneration = true } = options
|
||||
let defaultChannelId = 1
|
||||
|
||||
try {
|
||||
const { videoChannels } = await this.server.users.getMyInfo({ token: options.token })
|
||||
defaultChannelId = videoChannels[0].id
|
||||
} catch (e) { /* empty */ }
|
||||
|
||||
// Override default attributes
|
||||
const attributes = {
|
||||
name: 'my super video',
|
||||
category: 5,
|
||||
licence: 4,
|
||||
language: 'zh',
|
||||
channelId: defaultChannelId,
|
||||
nsfw: true,
|
||||
waitTranscoding: false,
|
||||
description: 'my super description',
|
||||
support: 'my super support text',
|
||||
tags: [ 'tag' ],
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
commentsEnabled: true,
|
||||
downloadEnabled: true,
|
||||
fixture: 'video_short.webm',
|
||||
|
||||
...options.attributes
|
||||
}
|
||||
|
||||
const created = mode === 'legacy'
|
||||
? await this.buildLegacyUpload({ ...options, attributes })
|
||||
: await this.buildResumeUpload({ ...options, path: '/api/v1/videos/upload-resumable', attributes })
|
||||
|
||||
// Wait torrent generation
|
||||
const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 })
|
||||
if (expectedStatus === HttpStatusCode.OK_200 && waitTorrentGeneration) {
|
||||
let video: VideoDetails
|
||||
|
||||
do {
|
||||
video = await this.getWithToken({ ...options, id: created.uuid })
|
||||
|
||||
await wait(50)
|
||||
} while (!video.files[0].torrentUrl)
|
||||
}
|
||||
|
||||
return created
|
||||
}
|
||||
|
||||
async buildLegacyUpload (options: OverrideCommandOptions & {
|
||||
attributes: VideoEdit
|
||||
}): Promise<VideoCreateResult> {
|
||||
const path = '/api/v1/videos/upload'
|
||||
|
||||
return unwrapBody<{ video: VideoCreateResult }>(this.postUploadRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: this.buildUploadFields(options.attributes),
|
||||
attaches: this.buildUploadAttaches(options.attributes),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})).then(body => body.video || body as any)
|
||||
}
|
||||
|
||||
async buildResumeUpload (options: OverrideCommandOptions & {
|
||||
path: string
|
||||
attributes: { fixture?: string } & { [id: string]: any }
|
||||
completedExpectedStatus?: HttpStatusCodeType // When the upload is finished
|
||||
}): Promise<VideoCreateResult> {
|
||||
const { path, attributes, expectedStatus = HttpStatusCode.OK_200, completedExpectedStatus } = options
|
||||
|
||||
let size = 0
|
||||
let videoFilePath: string
|
||||
let mimetype = 'video/mp4'
|
||||
|
||||
if (attributes.fixture) {
|
||||
videoFilePath = buildAbsoluteFixturePath(attributes.fixture)
|
||||
size = (await stat(videoFilePath)).size
|
||||
|
||||
if (videoFilePath.endsWith('.mkv')) {
|
||||
mimetype = 'video/x-matroska'
|
||||
} else if (videoFilePath.endsWith('.webm')) {
|
||||
mimetype = 'video/webm'
|
||||
}
|
||||
}
|
||||
|
||||
// Do not check status automatically, we'll check it manually
|
||||
const initializeSessionRes = await this.prepareResumableUpload({
|
||||
...options,
|
||||
|
||||
path,
|
||||
expectedStatus: null,
|
||||
attributes,
|
||||
size,
|
||||
mimetype
|
||||
})
|
||||
const initStatus = initializeSessionRes.status
|
||||
|
||||
if (videoFilePath && initStatus === HttpStatusCode.CREATED_201) {
|
||||
const locationHeader = initializeSessionRes.header['location']
|
||||
expect(locationHeader).to.not.be.undefined
|
||||
|
||||
const pathUploadId = locationHeader.split('?')[1]
|
||||
|
||||
const result = await this.sendResumableChunks({
|
||||
...options,
|
||||
|
||||
path,
|
||||
pathUploadId,
|
||||
videoFilePath,
|
||||
size,
|
||||
expectedStatus: completedExpectedStatus
|
||||
})
|
||||
|
||||
if (result.statusCode === HttpStatusCode.OK_200) {
|
||||
await this.endResumableUpload({
|
||||
...options,
|
||||
|
||||
expectedStatus: HttpStatusCode.NO_CONTENT_204,
|
||||
path,
|
||||
pathUploadId
|
||||
})
|
||||
}
|
||||
|
||||
return result.body?.video || result.body as any
|
||||
}
|
||||
|
||||
const expectedInitStatus = expectedStatus === HttpStatusCode.OK_200
|
||||
? HttpStatusCode.CREATED_201
|
||||
: expectedStatus
|
||||
|
||||
expect(initStatus).to.equal(expectedInitStatus)
|
||||
|
||||
return initializeSessionRes.body.video || initializeSessionRes.body
|
||||
}
|
||||
|
||||
async prepareResumableUpload (options: OverrideCommandOptions & {
|
||||
path: string
|
||||
attributes: { fixture?: string } & { [id: string]: any }
|
||||
size: number
|
||||
mimetype: string
|
||||
|
||||
originalName?: string
|
||||
lastModified?: number
|
||||
}) {
|
||||
const { path, attributes, originalName, lastModified, size, mimetype } = options
|
||||
|
||||
const attaches = this.buildUploadAttaches(omit(options.attributes, [ 'fixture' ]))
|
||||
|
||||
const uploadOptions = {
|
||||
...options,
|
||||
|
||||
path,
|
||||
headers: {
|
||||
'X-Upload-Content-Type': mimetype,
|
||||
'X-Upload-Content-Length': size.toString()
|
||||
},
|
||||
fields: {
|
||||
filename: attributes.fixture,
|
||||
originalName,
|
||||
lastModified,
|
||||
|
||||
...this.buildUploadFields(options.attributes)
|
||||
},
|
||||
|
||||
// Fixture will be sent later
|
||||
attaches: this.buildUploadAttaches(omit(options.attributes, [ 'fixture' ])),
|
||||
implicitToken: true,
|
||||
|
||||
defaultExpectedStatus: null
|
||||
}
|
||||
|
||||
if (Object.keys(attaches).length === 0) return this.postBodyRequest(uploadOptions)
|
||||
|
||||
return this.postUploadRequest(uploadOptions)
|
||||
}
|
||||
|
||||
sendResumableChunks (options: OverrideCommandOptions & {
|
||||
pathUploadId: string
|
||||
path: string
|
||||
videoFilePath: string
|
||||
size: number
|
||||
contentLength?: number
|
||||
contentRangeBuilder?: (start: number, chunk: any) => string
|
||||
digestBuilder?: (chunk: any) => string
|
||||
}) {
|
||||
const {
|
||||
path,
|
||||
pathUploadId,
|
||||
videoFilePath,
|
||||
size,
|
||||
contentLength,
|
||||
contentRangeBuilder,
|
||||
digestBuilder,
|
||||
expectedStatus = HttpStatusCode.OK_200
|
||||
} = options
|
||||
|
||||
let start = 0
|
||||
|
||||
const token = this.buildCommonRequestToken({ ...options, implicitToken: true })
|
||||
|
||||
const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 })
|
||||
const server = this.server
|
||||
return new Promise<GotResponse<{ video: VideoCreateResult }>>((resolve, reject) => {
|
||||
readable.on('data', async function onData (chunk) {
|
||||
try {
|
||||
readable.pause()
|
||||
|
||||
const byterangeStart = start + chunk.length - 1
|
||||
|
||||
const headers = {
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Content-Range': contentRangeBuilder
|
||||
? contentRangeBuilder(start, chunk)
|
||||
: `bytes ${start}-${byterangeStart}/${size}`,
|
||||
'Content-Length': contentLength ? contentLength + '' : chunk.length + ''
|
||||
}
|
||||
|
||||
if (digestBuilder) {
|
||||
Object.assign(headers, { digest: digestBuilder(chunk) })
|
||||
}
|
||||
|
||||
const res = await got<{ video: VideoCreateResult }>({
|
||||
url: new URL(path + '?' + pathUploadId, server.url).toString(),
|
||||
method: 'put',
|
||||
headers,
|
||||
body: chunk,
|
||||
responseType: 'json',
|
||||
throwHttpErrors: false
|
||||
})
|
||||
|
||||
start += chunk.length
|
||||
|
||||
// Last request, check final status
|
||||
if (byterangeStart + 1 === size) {
|
||||
if (res.statusCode === expectedStatus) {
|
||||
return resolve(res)
|
||||
}
|
||||
|
||||
if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) {
|
||||
readable.off('data', onData)
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
const message = `Incorrect transient behaviour sending intermediary chunks. Status code is ${res.statusCode} instead of ${expectedStatus}`
|
||||
return reject(new Error(message))
|
||||
}
|
||||
}
|
||||
|
||||
readable.resume()
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
endResumableUpload (options: OverrideCommandOptions & {
|
||||
path: string
|
||||
pathUploadId: string
|
||||
}) {
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path: options.path,
|
||||
rawQuery: options.pathUploadId,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
quickUpload (options: OverrideCommandOptions & {
|
||||
name: string
|
||||
nsfw?: boolean
|
||||
privacy?: VideoPrivacyType
|
||||
fixture?: string
|
||||
videoPasswords?: string[]
|
||||
}) {
|
||||
const attributes: VideoEdit = { name: options.name }
|
||||
if (options.nsfw) attributes.nsfw = options.nsfw
|
||||
if (options.privacy) attributes.privacy = options.privacy
|
||||
if (options.fixture) attributes.fixture = options.fixture
|
||||
if (options.videoPasswords) attributes.videoPasswords = options.videoPasswords
|
||||
|
||||
return this.upload({ ...options, attributes })
|
||||
}
|
||||
|
||||
async randomUpload (options: OverrideCommandOptions & {
|
||||
wait?: boolean // default true
|
||||
additionalParams?: VideoEdit & { prefixName?: string }
|
||||
} = {}) {
|
||||
const { wait = true, additionalParams } = options
|
||||
const prefixName = additionalParams?.prefixName || ''
|
||||
const name = prefixName + buildUUID()
|
||||
|
||||
const attributes = { name, ...additionalParams }
|
||||
|
||||
const result = await this.upload({ ...options, attributes })
|
||||
|
||||
if (wait) await waitJobs([ this.server ])
|
||||
|
||||
return { ...result, name }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
replaceSourceFile (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
fixture: string
|
||||
completedExpectedStatus?: HttpStatusCodeType
|
||||
}) {
|
||||
return this.buildResumeUpload({
|
||||
...options,
|
||||
|
||||
path: '/api/v1/videos/' + options.videoId + '/source/replace-resumable',
|
||||
attributes: { fixture: options.fixture }
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
removeHLSPlaylist (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/hls'
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeHLSFile (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
fileId: number
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/hls/' + options.fileId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeAllWebVideoFiles (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/web-videos'
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
removeWebVideoFile (options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
fileId: number
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/web-videos/' + options.fileId
|
||||
|
||||
return this.deleteRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
runTranscoding (options: OverrideCommandOptions & VideoTranscodingCreate & {
|
||||
videoId: number | string
|
||||
}) {
|
||||
const path = '/api/v1/videos/' + options.videoId + '/transcoding'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
fields: pick(options, [ 'transcodingType', 'forceTranscoding' ]),
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private buildListQuery (options: VideosCommonQuery) {
|
||||
return pick(options, [
|
||||
'start',
|
||||
'count',
|
||||
'sort',
|
||||
'nsfw',
|
||||
'isLive',
|
||||
'categoryOneOf',
|
||||
'licenceOneOf',
|
||||
'languageOneOf',
|
||||
'privacyOneOf',
|
||||
'tagsOneOf',
|
||||
'tagsAllOf',
|
||||
'isLocal',
|
||||
'include',
|
||||
'skipCount'
|
||||
])
|
||||
}
|
||||
|
||||
private buildUploadFields (attributes: VideoEdit) {
|
||||
return omit(attributes, [ 'fixture', 'thumbnailfile', 'previewfile' ])
|
||||
}
|
||||
|
||||
private buildUploadAttaches (attributes: VideoEdit) {
|
||||
const attaches: { [ name: string ]: string } = {}
|
||||
|
||||
for (const key of [ 'thumbnailfile', 'previewfile' ]) {
|
||||
if (attributes[key]) attaches[key] = buildAbsoluteFixturePath(attributes[key])
|
||||
}
|
||||
|
||||
if (attributes.fixture) attaches.videofile = buildAbsoluteFixturePath(attributes.fixture)
|
||||
|
||||
return attaches
|
||||
}
|
||||
}
|
51
packages/server-commands/src/videos/views-command.ts
Normal file
51
packages/server-commands/src/videos/views-command.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */
|
||||
import { HttpStatusCode, VideoViewEvent } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class ViewsCommand extends AbstractCommand {
|
||||
|
||||
view (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
currentTime: number
|
||||
viewEvent?: VideoViewEvent
|
||||
xForwardedFor?: string
|
||||
}) {
|
||||
const { id, xForwardedFor, viewEvent, currentTime } = options
|
||||
const path = '/api/v1/videos/' + id + '/views'
|
||||
|
||||
return this.postBodyRequest({
|
||||
...options,
|
||||
|
||||
path,
|
||||
xForwardedFor,
|
||||
fields: {
|
||||
currentTime,
|
||||
viewEvent
|
||||
},
|
||||
implicitToken: false,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
|
||||
async simulateView (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
xForwardedFor?: string
|
||||
}) {
|
||||
await this.view({ ...options, currentTime: 0 })
|
||||
await this.view({ ...options, currentTime: 5 })
|
||||
}
|
||||
|
||||
async simulateViewer (options: OverrideCommandOptions & {
|
||||
id: number | string
|
||||
currentTimes: number[]
|
||||
xForwardedFor?: string
|
||||
}) {
|
||||
let viewEvent: VideoViewEvent = 'seek'
|
||||
|
||||
for (const currentTime of options.currentTimes) {
|
||||
await this.view({ ...options, currentTime, viewEvent })
|
||||
|
||||
viewEvent = undefined
|
||||
}
|
||||
}
|
||||
}
|
14
packages/server-commands/tsconfig.json
Normal file
14
packages/server-commands/tsconfig.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "src",
|
||||
"tsBuildInfoFile": "./dist/.tsbuildinfo"
|
||||
},
|
||||
"references": [
|
||||
{ "path": "../models" },
|
||||
{ "path": "../core-utils" },
|
||||
{ "path": "../typescript-utils" }
|
||||
]
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue