From bf54587a3e2ad9c2c186828f2a5682b91ee2cc00 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 17 Dec 2021 09:29:23 +0100 Subject: shared/ typescript types dir server-commands --- shared/extra-utils/bulk/bulk-command.ts | 20 - shared/extra-utils/bulk/index.ts | 1 - shared/extra-utils/cli/cli-command.ts | 27 - shared/extra-utils/cli/index.ts | 1 - .../custom-pages/custom-pages-command.ts | 33 - shared/extra-utils/custom-pages/index.ts | 1 - shared/extra-utils/feeds/feeds-command.ts | 44 -- shared/extra-utils/feeds/index.ts | 1 - shared/extra-utils/index.ts | 16 +- shared/extra-utils/logs/index.ts | 1 - shared/extra-utils/logs/logs-command.ts | 44 -- shared/extra-utils/miscs/checks.ts | 58 -- shared/extra-utils/miscs/generate.ts | 75 -- shared/extra-utils/miscs/index.ts | 5 - shared/extra-utils/miscs/sql-command.ts | 141 ---- shared/extra-utils/miscs/tests.ts | 101 --- shared/extra-utils/miscs/webtorrent.ts | 46 -- shared/extra-utils/mock-servers/index.ts | 5 - shared/extra-utils/mock-servers/mock-429.ts | 33 - shared/extra-utils/mock-servers/mock-email.ts | 63 -- .../mock-servers/mock-instances-index.ts | 46 -- .../mock-servers/mock-joinpeertube-versions.ts | 34 - .../mock-servers/mock-object-storage.ts | 41 -- .../mock-servers/mock-plugin-blocklist.ts | 36 - shared/extra-utils/mock-servers/mock-proxy.ts | 25 - shared/extra-utils/mock-servers/utils.ts | 33 - shared/extra-utils/moderation/abuses-command.ts | 228 ------ shared/extra-utils/moderation/index.ts | 1 - shared/extra-utils/overviews/index.ts | 1 - shared/extra-utils/overviews/overviews-command.ts | 23 - shared/extra-utils/requests/check-api-params.ts | 48 -- shared/extra-utils/requests/index.ts | 3 - shared/extra-utils/requests/requests.ts | 208 ------ shared/extra-utils/search/index.ts | 1 - shared/extra-utils/search/search-command.ts | 98 --- shared/extra-utils/server/config-command.ts | 353 --------- shared/extra-utils/server/contact-form-command.ts | 31 - shared/extra-utils/server/debug-command.ts | 33 - shared/extra-utils/server/directories.ts | 34 - shared/extra-utils/server/follows-command.ts | 139 ---- shared/extra-utils/server/follows.ts | 20 - shared/extra-utils/server/index.ts | 17 - shared/extra-utils/server/jobs-command.ts | 61 -- shared/extra-utils/server/jobs.ts | 84 --- .../extra-utils/server/object-storage-command.ts | 77 -- shared/extra-utils/server/plugins-command.ts | 257 ------- shared/extra-utils/server/plugins.ts | 18 - shared/extra-utils/server/redundancy-command.ts | 80 --- shared/extra-utils/server/server.ts | 392 ---------- shared/extra-utils/server/servers-command.ts | 92 --- shared/extra-utils/server/servers.ts | 49 -- shared/extra-utils/server/stats-command.ts | 25 - shared/extra-utils/server/tracker.ts | 27 - shared/extra-utils/shared/abstract-command.ts | 211 ------ shared/extra-utils/shared/index.ts | 1 - shared/extra-utils/socket/index.ts | 1 - shared/extra-utils/socket/socket-io-command.ts | 15 - shared/extra-utils/users/accounts-command.ts | 78 -- shared/extra-utils/users/actors.ts | 73 -- shared/extra-utils/users/blocklist-command.ts | 162 ----- shared/extra-utils/users/index.ts | 9 - shared/extra-utils/users/login-command.ts | 132 ---- shared/extra-utils/users/login.ts | 19 - shared/extra-utils/users/notifications-command.ts | 86 --- shared/extra-utils/users/notifications.ts | 795 --------------------- shared/extra-utils/users/subscriptions-command.ts | 99 --- shared/extra-utils/users/users-command.ts | 416 ----------- shared/extra-utils/videos/blacklist-command.ts | 76 -- shared/extra-utils/videos/captions-command.ts | 65 -- shared/extra-utils/videos/captions.ts | 21 - .../extra-utils/videos/change-ownership-command.ts | 68 -- shared/extra-utils/videos/channels-command.ts | 178 ----- shared/extra-utils/videos/channels.ts | 18 - shared/extra-utils/videos/comments-command.ts | 152 ---- shared/extra-utils/videos/history-command.ts | 58 -- shared/extra-utils/videos/imports-command.ts | 47 -- shared/extra-utils/videos/index.ts | 19 - shared/extra-utils/videos/live-command.ts | 155 ---- shared/extra-utils/videos/live.ts | 137 ---- shared/extra-utils/videos/playlists-command.ts | 280 -------- shared/extra-utils/videos/playlists.ts | 25 - shared/extra-utils/videos/services-command.ts | 29 - .../videos/streaming-playlists-command.ts | 44 -- shared/extra-utils/videos/streaming-playlists.ts | 77 -- shared/extra-utils/videos/videos-command.ts | 679 ------------------ shared/extra-utils/videos/videos.ts | 104 --- shared/server-commands/bulk/bulk-command.ts | 20 + shared/server-commands/bulk/index.ts | 1 + shared/server-commands/cli/cli-command.ts | 27 + shared/server-commands/cli/index.ts | 1 + .../custom-pages/custom-pages-command.ts | 33 + shared/server-commands/custom-pages/index.ts | 1 + shared/server-commands/feeds/feeds-command.ts | 44 ++ shared/server-commands/feeds/index.ts | 1 + shared/server-commands/index.ts | 15 + shared/server-commands/logs/index.ts | 1 + shared/server-commands/logs/logs-command.ts | 44 ++ shared/server-commands/miscs/checks.ts | 58 ++ shared/server-commands/miscs/generate.ts | 75 ++ shared/server-commands/miscs/index.ts | 5 + shared/server-commands/miscs/sql-command.ts | 141 ++++ shared/server-commands/miscs/tests.ts | 101 +++ shared/server-commands/miscs/webtorrent.ts | 46 ++ shared/server-commands/mock-servers/index.ts | 5 + shared/server-commands/mock-servers/mock-429.ts | 33 + shared/server-commands/mock-servers/mock-email.ts | 63 ++ .../mock-servers/mock-instances-index.ts | 46 ++ .../mock-servers/mock-joinpeertube-versions.ts | 34 + .../mock-servers/mock-object-storage.ts | 41 ++ .../mock-servers/mock-plugin-blocklist.ts | 36 + shared/server-commands/mock-servers/mock-proxy.ts | 25 + shared/server-commands/mock-servers/utils.ts | 33 + .../server-commands/moderation/abuses-command.ts | 228 ++++++ shared/server-commands/moderation/index.ts | 1 + shared/server-commands/overviews/index.ts | 1 + .../server-commands/overviews/overviews-command.ts | 23 + .../server-commands/requests/check-api-params.ts | 48 ++ shared/server-commands/requests/index.ts | 3 + shared/server-commands/requests/requests.ts | 208 ++++++ shared/server-commands/search/index.ts | 1 + shared/server-commands/search/search-command.ts | 98 +++ shared/server-commands/server/config-command.ts | 353 +++++++++ .../server-commands/server/contact-form-command.ts | 31 + shared/server-commands/server/debug-command.ts | 33 + shared/server-commands/server/directories.ts | 34 + shared/server-commands/server/follows-command.ts | 139 ++++ shared/server-commands/server/follows.ts | 20 + shared/server-commands/server/index.ts | 17 + shared/server-commands/server/jobs-command.ts | 61 ++ shared/server-commands/server/jobs.ts | 84 +++ .../server/object-storage-command.ts | 77 ++ shared/server-commands/server/plugins-command.ts | 257 +++++++ shared/server-commands/server/plugins.ts | 18 + .../server-commands/server/redundancy-command.ts | 80 +++ shared/server-commands/server/server.ts | 392 ++++++++++ shared/server-commands/server/servers-command.ts | 92 +++ shared/server-commands/server/servers.ts | 49 ++ shared/server-commands/server/stats-command.ts | 25 + shared/server-commands/server/tracker.ts | 27 + shared/server-commands/shared/abstract-command.ts | 211 ++++++ shared/server-commands/shared/index.ts | 1 + shared/server-commands/socket/index.ts | 1 + shared/server-commands/socket/socket-io-command.ts | 15 + shared/server-commands/users/accounts-command.ts | 78 ++ shared/server-commands/users/actors.ts | 73 ++ shared/server-commands/users/blocklist-command.ts | 162 +++++ shared/server-commands/users/index.ts | 9 + shared/server-commands/users/login-command.ts | 132 ++++ shared/server-commands/users/login.ts | 19 + .../server-commands/users/notifications-command.ts | 86 +++ shared/server-commands/users/notifications.ts | 795 +++++++++++++++++++++ .../server-commands/users/subscriptions-command.ts | 99 +++ shared/server-commands/users/users-command.ts | 416 +++++++++++ shared/server-commands/videos/blacklist-command.ts | 76 ++ shared/server-commands/videos/captions-command.ts | 65 ++ shared/server-commands/videos/captions.ts | 21 + .../videos/change-ownership-command.ts | 68 ++ shared/server-commands/videos/channels-command.ts | 178 +++++ shared/server-commands/videos/channels.ts | 18 + shared/server-commands/videos/comments-command.ts | 152 ++++ shared/server-commands/videos/history-command.ts | 58 ++ shared/server-commands/videos/imports-command.ts | 47 ++ shared/server-commands/videos/index.ts | 19 + shared/server-commands/videos/live-command.ts | 155 ++++ shared/server-commands/videos/live.ts | 137 ++++ shared/server-commands/videos/playlists-command.ts | 280 ++++++++ shared/server-commands/videos/playlists.ts | 25 + shared/server-commands/videos/services-command.ts | 29 + .../videos/streaming-playlists-command.ts | 44 ++ .../server-commands/videos/streaming-playlists.ts | 77 ++ shared/server-commands/videos/videos-command.ts | 679 ++++++++++++++++++ shared/server-commands/videos/videos.ts | 104 +++ shared/tsconfig.types.json | 5 +- shared/typescript-utils/index.ts | 1 + shared/typescript-utils/types.ts | 45 ++ 175 files changed, 7710 insertions(+), 7660 deletions(-) delete mode 100644 shared/extra-utils/bulk/bulk-command.ts delete mode 100644 shared/extra-utils/bulk/index.ts delete mode 100644 shared/extra-utils/cli/cli-command.ts delete mode 100644 shared/extra-utils/cli/index.ts delete mode 100644 shared/extra-utils/custom-pages/custom-pages-command.ts delete mode 100644 shared/extra-utils/custom-pages/index.ts delete mode 100644 shared/extra-utils/feeds/feeds-command.ts delete mode 100644 shared/extra-utils/feeds/index.ts delete mode 100644 shared/extra-utils/logs/index.ts delete mode 100644 shared/extra-utils/logs/logs-command.ts delete mode 100644 shared/extra-utils/miscs/checks.ts delete mode 100644 shared/extra-utils/miscs/generate.ts delete mode 100644 shared/extra-utils/miscs/index.ts delete mode 100644 shared/extra-utils/miscs/sql-command.ts delete mode 100644 shared/extra-utils/miscs/tests.ts delete mode 100644 shared/extra-utils/miscs/webtorrent.ts delete mode 100644 shared/extra-utils/mock-servers/index.ts delete mode 100644 shared/extra-utils/mock-servers/mock-429.ts delete mode 100644 shared/extra-utils/mock-servers/mock-email.ts delete mode 100644 shared/extra-utils/mock-servers/mock-instances-index.ts delete mode 100644 shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts delete mode 100644 shared/extra-utils/mock-servers/mock-object-storage.ts delete mode 100644 shared/extra-utils/mock-servers/mock-plugin-blocklist.ts delete mode 100644 shared/extra-utils/mock-servers/mock-proxy.ts delete mode 100644 shared/extra-utils/mock-servers/utils.ts delete mode 100644 shared/extra-utils/moderation/abuses-command.ts delete mode 100644 shared/extra-utils/moderation/index.ts delete mode 100644 shared/extra-utils/overviews/index.ts delete mode 100644 shared/extra-utils/overviews/overviews-command.ts delete mode 100644 shared/extra-utils/requests/check-api-params.ts delete mode 100644 shared/extra-utils/requests/index.ts delete mode 100644 shared/extra-utils/requests/requests.ts delete mode 100644 shared/extra-utils/search/index.ts delete mode 100644 shared/extra-utils/search/search-command.ts delete mode 100644 shared/extra-utils/server/config-command.ts delete mode 100644 shared/extra-utils/server/contact-form-command.ts delete mode 100644 shared/extra-utils/server/debug-command.ts delete mode 100644 shared/extra-utils/server/directories.ts delete mode 100644 shared/extra-utils/server/follows-command.ts delete mode 100644 shared/extra-utils/server/follows.ts delete mode 100644 shared/extra-utils/server/index.ts delete mode 100644 shared/extra-utils/server/jobs-command.ts delete mode 100644 shared/extra-utils/server/jobs.ts delete mode 100644 shared/extra-utils/server/object-storage-command.ts delete mode 100644 shared/extra-utils/server/plugins-command.ts delete mode 100644 shared/extra-utils/server/plugins.ts delete mode 100644 shared/extra-utils/server/redundancy-command.ts delete mode 100644 shared/extra-utils/server/server.ts delete mode 100644 shared/extra-utils/server/servers-command.ts delete mode 100644 shared/extra-utils/server/servers.ts delete mode 100644 shared/extra-utils/server/stats-command.ts delete mode 100644 shared/extra-utils/server/tracker.ts delete mode 100644 shared/extra-utils/shared/abstract-command.ts delete mode 100644 shared/extra-utils/shared/index.ts delete mode 100644 shared/extra-utils/socket/index.ts delete mode 100644 shared/extra-utils/socket/socket-io-command.ts delete mode 100644 shared/extra-utils/users/accounts-command.ts delete mode 100644 shared/extra-utils/users/actors.ts delete mode 100644 shared/extra-utils/users/blocklist-command.ts delete mode 100644 shared/extra-utils/users/index.ts delete mode 100644 shared/extra-utils/users/login-command.ts delete mode 100644 shared/extra-utils/users/login.ts delete mode 100644 shared/extra-utils/users/notifications-command.ts delete mode 100644 shared/extra-utils/users/notifications.ts delete mode 100644 shared/extra-utils/users/subscriptions-command.ts delete mode 100644 shared/extra-utils/users/users-command.ts delete mode 100644 shared/extra-utils/videos/blacklist-command.ts delete mode 100644 shared/extra-utils/videos/captions-command.ts delete mode 100644 shared/extra-utils/videos/captions.ts delete mode 100644 shared/extra-utils/videos/change-ownership-command.ts delete mode 100644 shared/extra-utils/videos/channels-command.ts delete mode 100644 shared/extra-utils/videos/channels.ts delete mode 100644 shared/extra-utils/videos/comments-command.ts delete mode 100644 shared/extra-utils/videos/history-command.ts delete mode 100644 shared/extra-utils/videos/imports-command.ts delete mode 100644 shared/extra-utils/videos/index.ts delete mode 100644 shared/extra-utils/videos/live-command.ts delete mode 100644 shared/extra-utils/videos/live.ts delete mode 100644 shared/extra-utils/videos/playlists-command.ts delete mode 100644 shared/extra-utils/videos/playlists.ts delete mode 100644 shared/extra-utils/videos/services-command.ts delete mode 100644 shared/extra-utils/videos/streaming-playlists-command.ts delete mode 100644 shared/extra-utils/videos/streaming-playlists.ts delete mode 100644 shared/extra-utils/videos/videos-command.ts delete mode 100644 shared/extra-utils/videos/videos.ts create mode 100644 shared/server-commands/bulk/bulk-command.ts create mode 100644 shared/server-commands/bulk/index.ts create mode 100644 shared/server-commands/cli/cli-command.ts create mode 100644 shared/server-commands/cli/index.ts create mode 100644 shared/server-commands/custom-pages/custom-pages-command.ts create mode 100644 shared/server-commands/custom-pages/index.ts create mode 100644 shared/server-commands/feeds/feeds-command.ts create mode 100644 shared/server-commands/feeds/index.ts create mode 100644 shared/server-commands/index.ts create mode 100644 shared/server-commands/logs/index.ts create mode 100644 shared/server-commands/logs/logs-command.ts create mode 100644 shared/server-commands/miscs/checks.ts create mode 100644 shared/server-commands/miscs/generate.ts create mode 100644 shared/server-commands/miscs/index.ts create mode 100644 shared/server-commands/miscs/sql-command.ts create mode 100644 shared/server-commands/miscs/tests.ts create mode 100644 shared/server-commands/miscs/webtorrent.ts create mode 100644 shared/server-commands/mock-servers/index.ts create mode 100644 shared/server-commands/mock-servers/mock-429.ts create mode 100644 shared/server-commands/mock-servers/mock-email.ts create mode 100644 shared/server-commands/mock-servers/mock-instances-index.ts create mode 100644 shared/server-commands/mock-servers/mock-joinpeertube-versions.ts create mode 100644 shared/server-commands/mock-servers/mock-object-storage.ts create mode 100644 shared/server-commands/mock-servers/mock-plugin-blocklist.ts create mode 100644 shared/server-commands/mock-servers/mock-proxy.ts create mode 100644 shared/server-commands/mock-servers/utils.ts create mode 100644 shared/server-commands/moderation/abuses-command.ts create mode 100644 shared/server-commands/moderation/index.ts create mode 100644 shared/server-commands/overviews/index.ts create mode 100644 shared/server-commands/overviews/overviews-command.ts create mode 100644 shared/server-commands/requests/check-api-params.ts create mode 100644 shared/server-commands/requests/index.ts create mode 100644 shared/server-commands/requests/requests.ts create mode 100644 shared/server-commands/search/index.ts create mode 100644 shared/server-commands/search/search-command.ts create mode 100644 shared/server-commands/server/config-command.ts create mode 100644 shared/server-commands/server/contact-form-command.ts create mode 100644 shared/server-commands/server/debug-command.ts create mode 100644 shared/server-commands/server/directories.ts create mode 100644 shared/server-commands/server/follows-command.ts create mode 100644 shared/server-commands/server/follows.ts create mode 100644 shared/server-commands/server/index.ts create mode 100644 shared/server-commands/server/jobs-command.ts create mode 100644 shared/server-commands/server/jobs.ts create mode 100644 shared/server-commands/server/object-storage-command.ts create mode 100644 shared/server-commands/server/plugins-command.ts create mode 100644 shared/server-commands/server/plugins.ts create mode 100644 shared/server-commands/server/redundancy-command.ts create mode 100644 shared/server-commands/server/server.ts create mode 100644 shared/server-commands/server/servers-command.ts create mode 100644 shared/server-commands/server/servers.ts create mode 100644 shared/server-commands/server/stats-command.ts create mode 100644 shared/server-commands/server/tracker.ts create mode 100644 shared/server-commands/shared/abstract-command.ts create mode 100644 shared/server-commands/shared/index.ts create mode 100644 shared/server-commands/socket/index.ts create mode 100644 shared/server-commands/socket/socket-io-command.ts create mode 100644 shared/server-commands/users/accounts-command.ts create mode 100644 shared/server-commands/users/actors.ts create mode 100644 shared/server-commands/users/blocklist-command.ts create mode 100644 shared/server-commands/users/index.ts create mode 100644 shared/server-commands/users/login-command.ts create mode 100644 shared/server-commands/users/login.ts create mode 100644 shared/server-commands/users/notifications-command.ts create mode 100644 shared/server-commands/users/notifications.ts create mode 100644 shared/server-commands/users/subscriptions-command.ts create mode 100644 shared/server-commands/users/users-command.ts create mode 100644 shared/server-commands/videos/blacklist-command.ts create mode 100644 shared/server-commands/videos/captions-command.ts create mode 100644 shared/server-commands/videos/captions.ts create mode 100644 shared/server-commands/videos/change-ownership-command.ts create mode 100644 shared/server-commands/videos/channels-command.ts create mode 100644 shared/server-commands/videos/channels.ts create mode 100644 shared/server-commands/videos/comments-command.ts create mode 100644 shared/server-commands/videos/history-command.ts create mode 100644 shared/server-commands/videos/imports-command.ts create mode 100644 shared/server-commands/videos/index.ts create mode 100644 shared/server-commands/videos/live-command.ts create mode 100644 shared/server-commands/videos/live.ts create mode 100644 shared/server-commands/videos/playlists-command.ts create mode 100644 shared/server-commands/videos/playlists.ts create mode 100644 shared/server-commands/videos/services-command.ts create mode 100644 shared/server-commands/videos/streaming-playlists-command.ts create mode 100644 shared/server-commands/videos/streaming-playlists.ts create mode 100644 shared/server-commands/videos/videos-command.ts create mode 100644 shared/server-commands/videos/videos.ts create mode 100644 shared/typescript-utils/index.ts create mode 100644 shared/typescript-utils/types.ts (limited to 'shared') diff --git a/shared/extra-utils/bulk/bulk-command.ts b/shared/extra-utils/bulk/bulk-command.ts deleted file mode 100644 index b5c5673ce..000000000 --- a/shared/extra-utils/bulk/bulk-command.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BulkRemoveCommentsOfBody, HttpStatusCode } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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 - }) - } -} diff --git a/shared/extra-utils/bulk/index.ts b/shared/extra-utils/bulk/index.ts deleted file mode 100644 index 391597243..000000000 --- a/shared/extra-utils/bulk/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './bulk-command' diff --git a/shared/extra-utils/cli/cli-command.ts b/shared/extra-utils/cli/cli-command.ts deleted file mode 100644 index ab9738174..000000000 --- a/shared/extra-utils/cli/cli-command.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { exec } from 'child_process' -import { AbstractCommand } from '../shared' - -export class CLICommand extends AbstractCommand { - - static exec (command: string) { - return new Promise((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}`) - } -} diff --git a/shared/extra-utils/cli/index.ts b/shared/extra-utils/cli/index.ts deleted file mode 100644 index 91b5abfbe..000000000 --- a/shared/extra-utils/cli/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './cli-command' diff --git a/shared/extra-utils/custom-pages/custom-pages-command.ts b/shared/extra-utils/custom-pages/custom-pages-command.ts deleted file mode 100644 index cd869a8de..000000000 --- a/shared/extra-utils/custom-pages/custom-pages-command.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { CustomPage, HttpStatusCode } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class CustomPagesCommand extends AbstractCommand { - - getInstanceHomepage (options: OverrideCommandOptions = {}) { - const path = '/api/v1/custom-pages/homepage/instance' - - return this.getRequestBody({ - ...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 - }) - } -} diff --git a/shared/extra-utils/custom-pages/index.ts b/shared/extra-utils/custom-pages/index.ts deleted file mode 100644 index 58aed04f2..000000000 --- a/shared/extra-utils/custom-pages/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './custom-pages-command' diff --git a/shared/extra-utils/feeds/feeds-command.ts b/shared/extra-utils/feeds/feeds-command.ts deleted file mode 100644 index 3c95f9536..000000000 --- a/shared/extra-utils/feeds/feeds-command.ts +++ /dev/null @@ -1,44 +0,0 @@ - -import { HttpStatusCode } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -type FeedType = 'videos' | 'video-comments' | 'subscriptions' - -export class FeedCommand extends AbstractCommand { - - getXML (options: OverrideCommandOptions & { - feed: FeedType - format?: string - }) { - const { feed, format } = options - const path = '/feeds/' + feed + '.xml' - - return this.getRequestText({ - ...options, - - path, - query: format ? { format } : undefined, - accept: 'application/xml', - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - getJSON (options: OverrideCommandOptions & { - feed: FeedType - query?: { [ id: string ]: any } - }) { - const { feed, query } = options - const path = '/feeds/' + feed + '.json' - - return this.getRequestText({ - ...options, - - path, - query, - accept: 'application/json', - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/feeds/index.ts b/shared/extra-utils/feeds/index.ts deleted file mode 100644 index 662a22b6f..000000000 --- a/shared/extra-utils/feeds/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './feeds-command' diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts index 4b3636d06..5710c5ab5 100644 --- a/shared/extra-utils/index.ts +++ b/shared/extra-utils/index.ts @@ -1,15 +1 @@ -export * from './bulk' -export * from './cli' -export * from './custom-pages' -export * from './feeds' -export * from './logs' -export * from './miscs' -export * from './mock-servers' -export * from './moderation' -export * from './overviews' -export * from './requests' -export * from './search' -export * from './server' -export * from './socket' -export * from './users' -export * from './videos' +export * from './ffprobe' diff --git a/shared/extra-utils/logs/index.ts b/shared/extra-utils/logs/index.ts deleted file mode 100644 index 69452d7f0..000000000 --- a/shared/extra-utils/logs/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './logs-command' diff --git a/shared/extra-utils/logs/logs-command.ts b/shared/extra-utils/logs/logs-command.ts deleted file mode 100644 index 7b5c66c0c..000000000 --- a/shared/extra-utils/logs/logs-command.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { HttpStatusCode } from '@shared/models' -import { LogLevel } from '../../models/server/log-level.type' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class LogsCommand extends AbstractCommand { - - getLogs (options: OverrideCommandOptions & { - startDate: Date - endDate?: Date - level?: LogLevel - tagsOneOf?: string[] - }) { - const { startDate, endDate, tagsOneOf, level } = options - const path = '/api/v1/server/logs' - - return this.getRequestBody({ - ...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 - }) - } - -} diff --git a/shared/extra-utils/miscs/checks.ts b/shared/extra-utils/miscs/checks.ts deleted file mode 100644 index 589928997..000000000 --- a/shared/extra-utils/miscs/checks.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ - -import { expect } from 'chai' -import { pathExists, readFile } from 'fs-extra' -import { join } from 'path' -import { root } from '@shared/core-utils' -import { HttpStatusCode } from '@shared/models' -import { makeGetRequest } from '../requests' -import { PeerTubeServer } from '../server' - -// Default interval -> 5 minutes -function dateIsValid (dateString: string, interval = 300000) { - const dateToCheck = new Date(dateString) - const now = new Date() - - return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval -} - -function expectStartWith (str: string, start: string) { - expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.true -} - -async function expectLogDoesNotContain (server: PeerTubeServer, str: string) { - const content = await server.servers.getLogContent() - - expect(content.toString()).to.not.contain(str) -} - -async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') { - const res = await makeGetRequest({ - url, - path: imagePath, - expectedStatus: HttpStatusCode.OK_200 - }) - - const body = res.body - - const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension)) - const minLength = body.length - ((30 * body.length) / 100) - const maxLength = body.length + ((30 * body.length) / 100) - - expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture') - expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture') -} - -async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) { - const base = server.servers.buildDirectory(directory) - - expect(await pathExists(join(base, filePath))).to.equal(exist) -} - -export { - dateIsValid, - testImage, - expectLogDoesNotContain, - testFileExistsOrNot, - expectStartWith -} diff --git a/shared/extra-utils/miscs/generate.ts b/shared/extra-utils/miscs/generate.ts deleted file mode 100644 index 93673a063..000000000 --- a/shared/extra-utils/miscs/generate.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { expect } from 'chai' -import ffmpeg from 'fluent-ffmpeg' -import { ensureDir, pathExists } from 'fs-extra' -import { dirname } from 'path' -import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '@shared/extra-utils/ffprobe' -import { getMaxBitrate } from '@shared/core-utils' -import { buildAbsoluteFixturePath } from './tests' - -async function ensureHasTooBigBitrate (fixturePath: string) { - const bitrate = await getVideoFileBitrate(fixturePath) - const dataResolution = await getVideoFileResolution(fixturePath) - const fps = await getVideoFileFPS(fixturePath) - - const maxBitrate = getMaxBitrate({ ...dataResolution, fps }) - expect(bitrate).to.be.above(maxBitrate) -} - -async function generateHighBitrateVideo () { - const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) - - await ensureDir(dirname(tempFixturePath)) - - const exists = await pathExists(tempFixturePath) - if (!exists) { - console.log('Generating high bitrate video.') - - // Generate a random, high bitrate video on the fly, so we don't have to include - // a large file in the repo. The video needs to have a certain minimum length so - // that FFmpeg properly applies bitrate limits. - // https://stackoverflow.com/a/15795112 - return new Promise((res, rej) => { - ffmpeg() - .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ]) - .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) - .outputOptions([ '-maxrate 10M', '-bufsize 10M' ]) - .output(tempFixturePath) - .on('error', rej) - .on('end', () => res(tempFixturePath)) - .run() - }) - } - - await ensureHasTooBigBitrate(tempFixturePath) - - return tempFixturePath -} - -async function generateVideoWithFramerate (fps = 60) { - const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true) - - await ensureDir(dirname(tempFixturePath)) - - const exists = await pathExists(tempFixturePath) - if (!exists) { - console.log('Generating video with framerate %d.', fps) - - return new Promise((res, rej) => { - ffmpeg() - .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ]) - .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) - .outputOptions([ `-r ${fps}` ]) - .output(tempFixturePath) - .on('error', rej) - .on('end', () => res(tempFixturePath)) - .run() - }) - } - - return tempFixturePath -} - -export { - generateHighBitrateVideo, - generateVideoWithFramerate -} diff --git a/shared/extra-utils/miscs/index.ts b/shared/extra-utils/miscs/index.ts deleted file mode 100644 index 4474661de..000000000 --- a/shared/extra-utils/miscs/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './checks' -export * from './generate' -export * from './sql-command' -export * from './tests' -export * from './webtorrent' diff --git a/shared/extra-utils/miscs/sql-command.ts b/shared/extra-utils/miscs/sql-command.ts deleted file mode 100644 index bedb3349b..000000000 --- a/shared/extra-utils/miscs/sql-command.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { QueryTypes, Sequelize } from 'sequelize' -import { AbstractCommand } from '../shared/abstract-command' - -export class SQLCommand extends AbstractCommand { - private sequelize: Sequelize - - deleteAll (table: string) { - const seq = this.getSequelize() - - const options = { type: QueryTypes.DELETE } - - return seq.query(`DELETE FROM "${table}"`, options) - } - - async getCount (table: string) { - const seq = this.getSequelize() - - const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } - - const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options) - if (total === null) return 0 - - return parseInt(total, 10) - } - - setActorField (to: string, field: string, value: string) { - const seq = this.getSequelize() - - const options = { type: QueryTypes.UPDATE } - - return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options) - } - - setVideoField (uuid: string, field: string, value: string) { - const seq = this.getSequelize() - - const options = { type: QueryTypes.UPDATE } - - return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) - } - - setPlaylistField (uuid: string, field: string, value: string) { - const seq = this.getSequelize() - - const options = { type: QueryTypes.UPDATE } - - return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) - } - - async countVideoViewsOf (uuid: string) { - const seq = this.getSequelize() - - const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' + - `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'` - - const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } - const [ { total } ] = await seq.query<{ total: number }>(query, options) - - if (!total) return 0 - - return parseInt(total + '', 10) - } - - getActorImage (filename: string) { - return this.selectQuery(`SELECT * FROM "actorImage" WHERE filename = '${filename}'`) - .then(rows => rows[0]) - } - - selectQuery (query: string) { - const seq = this.getSequelize() - const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } - - return seq.query(query, options) - } - - updateQuery (query: string) { - const seq = this.getSequelize() - const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE } - - return seq.query(query, options) - } - - setPluginField (pluginName: string, field: string, value: string) { - const seq = this.getSequelize() - - const options = { type: QueryTypes.UPDATE } - - return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options) - } - - setPluginVersion (pluginName: string, newVersion: string) { - return this.setPluginField(pluginName, 'version', newVersion) - } - - setPluginLatestVersion (pluginName: string, newVersion: string) { - return this.setPluginField(pluginName, 'latestVersion', newVersion) - } - - setActorFollowScores (newScore: number) { - const seq = this.getSequelize() - - const options = { type: QueryTypes.UPDATE } - - return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options) - } - - setTokenField (accessToken: string, field: string, value: string) { - const seq = this.getSequelize() - - const options = { type: QueryTypes.UPDATE } - - return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options) - } - - async cleanup () { - if (!this.sequelize) return - - await this.sequelize.close() - this.sequelize = undefined - } - - private getSequelize () { - if (this.sequelize) return this.sequelize - - const dbname = 'peertube_test' + this.server.internalServerNumber - const username = 'peertube' - const password = 'peertube' - const host = 'localhost' - const port = 5432 - - this.sequelize = new Sequelize(dbname, username, password, { - dialect: 'postgres', - host, - port, - logging: false - }) - - return this.sequelize - } - -} diff --git a/shared/extra-utils/miscs/tests.ts b/shared/extra-utils/miscs/tests.ts deleted file mode 100644 index 658fe5fd3..000000000 --- a/shared/extra-utils/miscs/tests.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { stat } from 'fs-extra' -import { basename, isAbsolute, join, resolve } from 'path' - -const FIXTURE_URLS = { - peertube_long: 'https://peertube2.cpy.re/videos/watch/122d093a-1ede-43bd-bd34-59d2931ffc5e', - peertube_short: 'https://peertube2.cpy.re/w/3fbif9S3WmtTP8gGsC5HBd', - - youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM', - - /** - * The video is used to check format-selection correctness wrt. HDR, - * which brings its own set of oddities outside of a MediaSource. - * - * The video needs to have the following format_ids: - * (which you can check by using `youtube-dl -F`): - * - (webm vp9) - * - (mp4 avc1) - * - (webm vp9.2 HDR) - */ - youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4', - - // eslint-disable-next-line max-len - magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Flazy-static%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4', - - badVideo: 'https://download.cpy.re/peertube/bad_video.mp4', - goodVideo: 'https://download.cpy.re/peertube/good_video.mp4', - goodVideo720: 'https://download.cpy.re/peertube/good_video_720.mp4', - - file4K: 'https://download.cpy.re/peertube/4k_file.txt' -} - -function parallelTests () { - return process.env.MOCHA_PARALLEL === 'true' -} - -function isGithubCI () { - return !!process.env.GITHUB_WORKSPACE -} - -function areHttpImportTestsDisabled () { - const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true' - - if (disabled) console.log('DISABLE_HTTP_IMPORT_TESTS env set to "true" so import tests are disabled') - - return disabled -} - -function areObjectStorageTestsDisabled () { - const disabled = process.env.ENABLE_OBJECT_STORAGE_TESTS !== 'true' - - if (disabled) console.log('ENABLE_OBJECT_STORAGE_TESTS env is not set to "true" so object storage tests are disabled') - - return disabled -} - -function buildAbsoluteFixturePath (path: string, customCIPath = false) { - if (isAbsolute(path)) return path - - if (customCIPath && process.env.GITHUB_WORKSPACE) { - return join(process.env.GITHUB_WORKSPACE, 'fixtures', path) - } - - return join(root(), 'server', 'tests', 'fixtures', path) -} - -function root () { - // We are in /miscs - let root = join(__dirname, '..', '..', '..') - - if (basename(root) === 'dist') root = resolve(root, '..') - - return root -} - -function wait (milliseconds: number) { - return new Promise(resolve => setTimeout(resolve, milliseconds)) -} - -async function getFileSize (path: string) { - const stats = await stat(path) - - return stats.size -} - -function buildRequestStub (): any { - return { } -} - -export { - FIXTURE_URLS, - - parallelTests, - isGithubCI, - areHttpImportTestsDisabled, - buildAbsoluteFixturePath, - getFileSize, - buildRequestStub, - areObjectStorageTestsDisabled, - wait, - root -} diff --git a/shared/extra-utils/miscs/webtorrent.ts b/shared/extra-utils/miscs/webtorrent.ts deleted file mode 100644 index 0683f8893..000000000 --- a/shared/extra-utils/miscs/webtorrent.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { readFile } from 'fs-extra' -import parseTorrent from 'parse-torrent' -import { basename, join } from 'path' -import * as WebTorrent from 'webtorrent' -import { VideoFile } from '@shared/models' -import { PeerTubeServer } from '../server' - -let webtorrent: WebTorrent.Instance - -function webtorrentAdd (torrentId: string, refreshWebTorrent = false) { - const WebTorrent = require('webtorrent') - - if (webtorrent && refreshWebTorrent) webtorrent.destroy() - if (!webtorrent || refreshWebTorrent) webtorrent = new WebTorrent() - - webtorrent.on('error', err => console.error('Error in webtorrent', err)) - - return new Promise(res => { - const torrent = webtorrent.add(torrentId, res) - - torrent.on('error', err => console.error('Error in webtorrent torrent', err)) - torrent.on('warning', warn => { - const msg = typeof warn === 'string' - ? warn - : warn.message - - if (msg.includes('Unsupported')) return - - console.error('Warning in webtorrent torrent', warn) - }) - }) -} - -async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) { - const torrentName = basename(file.torrentUrl) - const torrentPath = server.servers.buildDirectory(join('torrents', torrentName)) - - const data = await readFile(torrentPath) - - return parseTorrent(data) -} - -export { - webtorrentAdd, - parseTorrentVideo -} diff --git a/shared/extra-utils/mock-servers/index.ts b/shared/extra-utils/mock-servers/index.ts deleted file mode 100644 index 93c00c788..000000000 --- a/shared/extra-utils/mock-servers/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './mock-email' -export * from './mock-instances-index' -export * from './mock-joinpeertube-versions' -export * from './mock-plugin-blocklist' -export * from './mock-object-storage' diff --git a/shared/extra-utils/mock-servers/mock-429.ts b/shared/extra-utils/mock-servers/mock-429.ts deleted file mode 100644 index 9e0d1281a..000000000 --- a/shared/extra-utils/mock-servers/mock-429.ts +++ /dev/null @@ -1,33 +0,0 @@ -import express from 'express' -import { Server } from 'http' -import { getPort, randomListen, terminateServer } from './utils' - -export class Mock429 { - private server: Server - private responseSent = false - - async initialize () { - const app = express() - - app.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { - - if (!this.responseSent) { - this.responseSent = true - - // Retry after 5 seconds - res.header('retry-after', '2') - return res.sendStatus(429) - } - - return res.sendStatus(200) - }) - - this.server = await randomListen(app) - - return getPort(this.server) - } - - terminate () { - return terminateServer(this.server) - } -} diff --git a/shared/extra-utils/mock-servers/mock-email.ts b/shared/extra-utils/mock-servers/mock-email.ts deleted file mode 100644 index f646c1621..000000000 --- a/shared/extra-utils/mock-servers/mock-email.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ChildProcess } from 'child_process' -import MailDev from '@peertube/maildev' -import { randomInt } from '@shared/core-utils' -import { parallelTests } from '../miscs' - -class MockSmtpServer { - - private static instance: MockSmtpServer - private started = false - private emailChildProcess: ChildProcess - private emails: object[] - - private constructor () { } - - collectEmails (emailsCollection: object[]) { - return new Promise((res, rej) => { - const port = parallelTests() ? randomInt(1000, 2000) : 1025 - this.emails = emailsCollection - - if (this.started) { - return res(undefined) - } - - const maildev = new MailDev({ - ip: '127.0.0.1', - smtp: port, - disableWeb: true, - silent: true - }) - - maildev.on('new', email => { - this.emails.push(email) - }) - - maildev.listen(err => { - if (err) return rej(err) - - this.started = true - - return res(port) - }) - }) - } - - kill () { - if (!this.emailChildProcess) return - - process.kill(this.emailChildProcess.pid) - - this.emailChildProcess = null - MockSmtpServer.instance = null - } - - static get Instance () { - return this.instance || (this.instance = new this()) - } -} - -// --------------------------------------------------------------------------- - -export { - MockSmtpServer -} diff --git a/shared/extra-utils/mock-servers/mock-instances-index.ts b/shared/extra-utils/mock-servers/mock-instances-index.ts deleted file mode 100644 index 92b12d6f3..000000000 --- a/shared/extra-utils/mock-servers/mock-instances-index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import express from 'express' -import { Server } from 'http' -import { getPort, randomListen, terminateServer } from './utils' - -export class MockInstancesIndex { - private server: Server - - private readonly indexInstances: { host: string, createdAt: string }[] = [] - - async initialize () { - const app = express() - - app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { - if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url) - - return next() - }) - - app.get('/api/v1/instances/hosts', (req: express.Request, res: express.Response) => { - const since = req.query.since - - const filtered = this.indexInstances.filter(i => { - if (!since) return true - - return i.createdAt > since - }) - - return res.json({ - total: filtered.length, - data: filtered - }) - }) - - this.server = await randomListen(app) - - return getPort(this.server) - } - - addInstance (host: string) { - this.indexInstances.push({ host, createdAt: new Date().toISOString() }) - } - - terminate () { - return terminateServer(this.server) - } -} diff --git a/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts b/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts deleted file mode 100644 index e7906ea56..000000000 --- a/shared/extra-utils/mock-servers/mock-joinpeertube-versions.ts +++ /dev/null @@ -1,34 +0,0 @@ -import express from 'express' -import { Server } from 'http' -import { getPort, randomListen } from './utils' - -export class MockJoinPeerTubeVersions { - private server: Server - private latestVersion: string - - async initialize () { - const app = express() - - app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { - if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url) - - return next() - }) - - app.get('/versions.json', (req: express.Request, res: express.Response) => { - return res.json({ - peertube: { - latestVersion: this.latestVersion - } - }) - }) - - this.server = await randomListen(app) - - return getPort(this.server) - } - - setLatestVersion (latestVersion: string) { - this.latestVersion = latestVersion - } -} diff --git a/shared/extra-utils/mock-servers/mock-object-storage.ts b/shared/extra-utils/mock-servers/mock-object-storage.ts deleted file mode 100644 index d135c2631..000000000 --- a/shared/extra-utils/mock-servers/mock-object-storage.ts +++ /dev/null @@ -1,41 +0,0 @@ -import express from 'express' -import got, { RequestError } from 'got' -import { Server } from 'http' -import { pipeline } from 'stream' -import { ObjectStorageCommand } from '../server' -import { getPort, randomListen, terminateServer } from './utils' - -export class MockObjectStorage { - private server: Server - - async initialize () { - const app = express() - - app.get('/:bucketName/:path(*)', (req: express.Request, res: express.Response, next: express.NextFunction) => { - const url = `http://${req.params.bucketName}.${ObjectStorageCommand.getEndpointHost()}/${req.params.path}` - - if (process.env.DEBUG) { - console.log('Receiving request on mocked server %s.', req.url) - console.log('Proxifying request to %s', url) - } - - return pipeline( - got.stream(url, { throwHttpErrors: false }), - res, - (err: RequestError) => { - if (!err) return - - console.error('Pipeline failed.', err) - } - ) - }) - - this.server = await randomListen(app) - - return getPort(this.server) - } - - terminate () { - return terminateServer(this.server) - } -} diff --git a/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts b/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts deleted file mode 100644 index f8a271cba..000000000 --- a/shared/extra-utils/mock-servers/mock-plugin-blocklist.ts +++ /dev/null @@ -1,36 +0,0 @@ -import express, { Request, Response } from 'express' -import { Server } from 'http' -import { getPort, randomListen, terminateServer } from './utils' - -type BlocklistResponse = { - data: { - value: string - action?: 'add' | 'remove' - updatedAt?: string - }[] -} - -export class MockBlocklist { - private body: BlocklistResponse - private server: Server - - async initialize () { - const app = express() - - app.get('/blocklist', (req: Request, res: Response) => { - return res.json(this.body) - }) - - this.server = await randomListen(app) - - return getPort(this.server) - } - - replace (body: BlocklistResponse) { - this.body = body - } - - terminate () { - return terminateServer(this.server) - } -} diff --git a/shared/extra-utils/mock-servers/mock-proxy.ts b/shared/extra-utils/mock-servers/mock-proxy.ts deleted file mode 100644 index 75ac79055..000000000 --- a/shared/extra-utils/mock-servers/mock-proxy.ts +++ /dev/null @@ -1,25 +0,0 @@ - -import { createServer, Server } from 'http' -import proxy from 'proxy' -import { getPort, terminateServer } from './utils' - -class MockProxy { - private server: Server - - initialize () { - return new Promise(res => { - this.server = proxy(createServer()) - this.server.listen(0, () => res(getPort(this.server))) - }) - } - - terminate () { - return terminateServer(this.server) - } -} - -// --------------------------------------------------------------------------- - -export { - MockProxy -} diff --git a/shared/extra-utils/mock-servers/utils.ts b/shared/extra-utils/mock-servers/utils.ts deleted file mode 100644 index 235642439..000000000 --- a/shared/extra-utils/mock-servers/utils.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Express } from 'express' -import { Server } from 'http' -import { AddressInfo } from 'net' - -function randomListen (app: Express) { - return new Promise(res => { - const server = app.listen(0, () => res(server)) - }) -} - -function getPort (server: Server) { - const address = server.address() as AddressInfo - - return address.port -} - -function terminateServer (server: Server) { - if (!server) return Promise.resolve() - - return new Promise((res, rej) => { - server.close(err => { - if (err) return rej(err) - - return res() - }) - }) -} - -export { - randomListen, - getPort, - terminateServer -} diff --git a/shared/extra-utils/moderation/abuses-command.ts b/shared/extra-utils/moderation/abuses-command.ts deleted file mode 100644 index 0db32ba46..000000000 --- a/shared/extra-utils/moderation/abuses-command.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { pick } from '@shared/core-utils' -import { - AbuseFilter, - AbuseMessage, - AbusePredefinedReasonsString, - AbuseState, - AbuseUpdate, - AbuseVideoIs, - AdminAbuse, - HttpStatusCode, - ResultList, - UserAbuse -} from '@shared/models' -import { unwrapBody } from '../requests/requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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?: AbuseState - 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>({ - ...options, - - path, - query, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - getUserList (options: OverrideCommandOptions & { - start?: number - count?: number - sort?: string - - id?: number - search?: string - state?: AbuseState - }) { - 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>({ - ...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>({ - ...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 - }) - } - -} diff --git a/shared/extra-utils/moderation/index.ts b/shared/extra-utils/moderation/index.ts deleted file mode 100644 index b37643956..000000000 --- a/shared/extra-utils/moderation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './abuses-command' diff --git a/shared/extra-utils/overviews/index.ts b/shared/extra-utils/overviews/index.ts deleted file mode 100644 index e19551907..000000000 --- a/shared/extra-utils/overviews/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './overviews-command' diff --git a/shared/extra-utils/overviews/overviews-command.ts b/shared/extra-utils/overviews/overviews-command.ts deleted file mode 100644 index 06b4892d2..000000000 --- a/shared/extra-utils/overviews/overviews-command.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { HttpStatusCode, VideosOverview } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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({ - ...options, - - path, - query, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/requests/check-api-params.ts b/shared/extra-utils/requests/check-api-params.ts deleted file mode 100644 index 26ba1e913..000000000 --- a/shared/extra-utils/requests/check-api-params.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { HttpStatusCode } from '@shared/models' -import { makeGetRequest } from './requests' - -function checkBadStartPagination (url: string, path: string, token?: string, query = {}) { - return makeGetRequest({ - url, - path, - token, - query: { ...query, start: 'hello' }, - expectedStatus: HttpStatusCode.BAD_REQUEST_400 - }) -} - -async function checkBadCountPagination (url: string, path: string, token?: string, query = {}) { - await makeGetRequest({ - url, - path, - token, - query: { ...query, count: 'hello' }, - expectedStatus: HttpStatusCode.BAD_REQUEST_400 - }) - - await makeGetRequest({ - url, - path, - token, - query: { ...query, count: 2000 }, - expectedStatus: HttpStatusCode.BAD_REQUEST_400 - }) -} - -function checkBadSortPagination (url: string, path: string, token?: string, query = {}) { - return makeGetRequest({ - url, - path, - token, - query: { ...query, sort: 'hello' }, - expectedStatus: HttpStatusCode.BAD_REQUEST_400 - }) -} - -// --------------------------------------------------------------------------- - -export { - checkBadStartPagination, - checkBadCountPagination, - checkBadSortPagination -} diff --git a/shared/extra-utils/requests/index.ts b/shared/extra-utils/requests/index.ts deleted file mode 100644 index 501163f92..000000000 --- a/shared/extra-utils/requests/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Don't include activitypub that import stuff from server -export * from './check-api-params' -export * from './requests' diff --git a/shared/extra-utils/requests/requests.ts b/shared/extra-utils/requests/requests.ts deleted file mode 100644 index b6b9024ed..000000000 --- a/shared/extra-utils/requests/requests.ts +++ /dev/null @@ -1,208 +0,0 @@ -/* eslint-disable @typescript-eslint/no-floating-promises */ - -import { decode } from 'querystring' -import request from 'supertest' -import { URL } from 'url' -import { HttpStatusCode } from '@shared/models' -import { buildAbsoluteFixturePath } from '../miscs/tests' - -export type CommonRequestParams = { - url: string - path?: string - contentType?: string - range?: string - redirects?: number - accept?: string - host?: string - token?: string - headers?: { [ name: string ]: string } - type?: string - xForwardedFor?: string - expectedStatus?: HttpStatusCode -} - -function makeRawRequest (url: string, expectedStatus?: HttpStatusCode, range?: string) { - const { host, protocol, pathname } = new URL(url) - - return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, expectedStatus, range }) -} - -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 = HttpStatusCode.OK_200) { - return makeGetRequest({ - url, - path, - expectedStatus: 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?: HttpStatusCode -}) { - 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 (test: request.Test): Promise { - return test.then(res => res.body) -} - -function unwrapText (test: request.Test): Promise { - return test.then(res => res.text) -} - -function unwrapBodyOrDecodeToJSON (test: request.Test): Promise { - return test.then(res => { - if (res.body instanceof Buffer) { - return JSON.parse(new TextDecoder().decode(res.body)) - } - - return res.body - }) -} - -function unwrapTextOrDecode (test: request.Test): Promise { - 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.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.expectedStatus) req.expect(options.expectedStatus) - 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 -} - -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]) - } - } -} diff --git a/shared/extra-utils/search/index.ts b/shared/extra-utils/search/index.ts deleted file mode 100644 index 48dbe8ae9..000000000 --- a/shared/extra-utils/search/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './search-command' diff --git a/shared/extra-utils/search/search-command.ts b/shared/extra-utils/search/search-command.ts deleted file mode 100644 index 0fbbcd6ef..000000000 --- a/shared/extra-utils/search/search-command.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - HttpStatusCode, - ResultList, - Video, - VideoChannel, - VideoChannelsSearchQuery, - VideoPlaylist, - VideoPlaylistsSearchQuery, - VideosSearchQuery -} from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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>({ - ...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>({ - ...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: search, - sort: sort ?? '-publishedAt' - } - }) - } - - advancedVideoSearch (options: OverrideCommandOptions & { - search: VideosSearchQuery - }) { - const { search } = options - const path = '/api/v1/search/videos' - - return this.getRequestBody>({ - ...options, - - path, - query: search, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/server/config-command.ts b/shared/extra-utils/server/config-command.ts deleted file mode 100644 index 89ae8eb4f..000000000 --- a/shared/extra-utils/server/config-command.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { merge } from 'lodash' -import { DeepPartial } from '@shared/typescript-utils' -import { About, HttpStatusCode, ServerConfig } from '@shared/models' -import { CustomConfig } from '../../models/server/custom-config.model' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class ConfigCommand extends AbstractCommand { - - static getCustomConfigResolutions (enabled: boolean) { - return { - '144p': enabled, - '240p': enabled, - '360p': enabled, - '480p': enabled, - '720p': enabled, - '1080p': enabled, - '1440p': enabled, - '2160p': enabled - } - } - - enableImports () { - return this.updateExistingSubConfig({ - newConfig: { - import: { - videos: { - http: { - enabled: true - }, - - torrent: { - enabled: true - } - } - } - } - }) - } - - enableLive (options: { - allowReplay?: boolean - transcoding?: boolean - } = {}) { - return this.updateExistingSubConfig({ - newConfig: { - live: { - enabled: true, - allowReplay: options.allowReplay ?? true, - transcoding: { - enabled: options.transcoding ?? true, - resolutions: ConfigCommand.getCustomConfigResolutions(true) - } - } - } - }) - } - - disableTranscoding () { - return this.updateExistingSubConfig({ - newConfig: { - transcoding: { - enabled: false - } - } - }) - } - - enableTranscoding (webtorrent = true, hls = true) { - return this.updateExistingSubConfig({ - newConfig: { - transcoding: { - enabled: true, - resolutions: ConfigCommand.getCustomConfigResolutions(true), - - webtorrent: { - enabled: webtorrent - }, - hls: { - enabled: hls - } - } - } - }) - } - - getConfig (options: OverrideCommandOptions = {}) { - const path = '/api/v1/config' - - return this.getRequestBody({ - ...options, - - path, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - getAbout (options: OverrideCommandOptions = {}) { - const path = '/api/v1/config/about' - - return this.getRequestBody({ - ...options, - - path, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - getCustomConfig (options: OverrideCommandOptions = {}) { - const path = '/api/v1/config/custom' - - return this.getRequestBody({ - ...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 - }) { - const existing = await this.getCustomConfig(options) - - return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) }) - } - - updateCustomSubConfig (options: OverrideCommandOptions & { - newConfig: DeepPartial - }) { - 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 - } - }, - signup: { - enabled: false, - limit: 5, - requiresEmailVerification: false, - minimumAge: 16 - }, - admin: { - email: 'superadmin1@example.com' - }, - contactForm: { - enabled: true - }, - user: { - videoQuota: 5242881, - videoQuotaDaily: 318742 - }, - videoChannels: { - maxPerUser: 20 - }, - transcoding: { - enabled: true, - 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 - }, - webtorrent: { - enabled: true - }, - hls: { - enabled: false - } - }, - live: { - enabled: true, - allowReplay: false, - maxDuration: -1, - maxInstanceLives: -1, - maxUserLives: 50, - transcoding: { - enabled: true, - threads: 4, - profile: 'default', - resolutions: { - '144p': true, - '240p': true, - '360p': true, - '480p': true, - '720p': true, - '1080p': true, - '1440p': true, - '2160p': true - } - } - }, - import: { - videos: { - concurrency: 3, - http: { - enabled: false - }, - torrent: { - enabled: false - } - } - }, - trending: { - videos: { - algorithms: { - enabled: [ 'best', '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 }) - } -} diff --git a/shared/extra-utils/server/contact-form-command.ts b/shared/extra-utils/server/contact-form-command.ts deleted file mode 100644 index 0e8fd6d84..000000000 --- a/shared/extra-utils/server/contact-form-command.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { HttpStatusCode } from '@shared/models' -import { ContactForm } from '../../models/server' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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 - }) - } -} diff --git a/shared/extra-utils/server/debug-command.ts b/shared/extra-utils/server/debug-command.ts deleted file mode 100644 index 3c5a785bb..000000000 --- a/shared/extra-utils/server/debug-command.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class DebugCommand extends AbstractCommand { - - getDebug (options: OverrideCommandOptions = {}) { - const path = '/api/v1/server/debug' - - return this.getRequestBody({ - ...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 - }) - } -} diff --git a/shared/extra-utils/server/directories.ts b/shared/extra-utils/server/directories.ts deleted file mode 100644 index e6f72d6fc..000000000 --- a/shared/extra-utils/server/directories.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ - -import { expect } from 'chai' -import { pathExists, readdir } from 'fs-extra' -import { join } from 'path' -import { root } from '@shared/core-utils' -import { PeerTubeServer } from './server' - -async function checkTmpIsEmpty (server: PeerTubeServer) { - await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ]) - - if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) { - await checkDirectoryIsEmpty(server, 'tmp/hls') - } -} - -async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) { - const testDirectory = 'test' + server.internalServerNumber - - const directoryPath = join(root(), testDirectory, directory) - - const directoryExists = await pathExists(directoryPath) - expect(directoryExists).to.be.true - - const files = await readdir(directoryPath) - const filtered = files.filter(f => exceptions.includes(f) === false) - - expect(filtered).to.have.lengthOf(0) -} - -export { - checkTmpIsEmpty, - checkDirectoryIsEmpty -} diff --git a/shared/extra-utils/server/follows-command.ts b/shared/extra-utils/server/follows-command.ts deleted file mode 100644 index 01ef6f179..000000000 --- a/shared/extra-utils/server/follows-command.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { pick } from '@shared/core-utils' -import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' -import { PeerTubeServer } from './server' - -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>({ - ...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>({ - ...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 - }) - } -} diff --git a/shared/extra-utils/server/follows.ts b/shared/extra-utils/server/follows.ts deleted file mode 100644 index 698238f29..000000000 --- a/shared/extra-utils/server/follows.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { waitJobs } from './jobs' -import { PeerTubeServer } from './server' - -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 -} diff --git a/shared/extra-utils/server/index.ts b/shared/extra-utils/server/index.ts deleted file mode 100644 index 76a2099da..000000000 --- a/shared/extra-utils/server/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -export * from './config-command' -export * from './contact-form-command' -export * from './debug-command' -export * from './directories' -export * from './follows-command' -export * from './follows' -export * from './jobs' -export * from './jobs-command' -export * from './object-storage-command' -export * from './plugins-command' -export * from './plugins' -export * from './redundancy-command' -export * from './server' -export * from './servers-command' -export * from './servers' -export * from './stats-command' -export * from './tracker' diff --git a/shared/extra-utils/server/jobs-command.ts b/shared/extra-utils/server/jobs-command.ts deleted file mode 100644 index 6636e7e4d..000000000 --- a/shared/extra-utils/server/jobs-command.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { pick } from '@shared/core-utils' -import { HttpStatusCode } from '@shared/models' -import { Job, JobState, JobType, ResultList } from '../../models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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] - } - - 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>({ - ...options, - - path, - query, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - listFailed (options: OverrideCommandOptions & { - jobType?: JobType - }) { - const path = this.buildJobsUrl('failed') - - return this.getRequestBody>({ - ...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 - } -} diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts deleted file mode 100644 index 34fefd444..000000000 --- a/shared/extra-utils/server/jobs.ts +++ /dev/null @@ -1,84 +0,0 @@ - -import { expect } from 'chai' -import { JobState, JobType } from '../../models' -import { wait } from '../miscs' -import { PeerTubeServer } from './server' - -async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer, skipDelayed = false) { - 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[] = [] - - // Check if each server has pending request - for (const server of servers) { - for (const state of states) { - const p = 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 - } - }) - - tasks.push(p) - } - - const p = server.debug.getDebug() - .then(obj => { - if (obj.activityPubMessagesWaiting !== 0) { - pendingRequests = true - } - }) - - tasks.push(p) - } - - 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 -} diff --git a/shared/extra-utils/server/object-storage-command.ts b/shared/extra-utils/server/object-storage-command.ts deleted file mode 100644 index b4de8f4cb..000000000 --- a/shared/extra-utils/server/object-storage-command.ts +++ /dev/null @@ -1,77 +0,0 @@ - -import { HttpStatusCode } from '@shared/models' -import { makePostBodyRequest } from '../requests' -import { AbstractCommand } from '../shared' - -export class ObjectStorageCommand extends AbstractCommand { - static readonly DEFAULT_PLAYLIST_BUCKET = 'streaming-playlists' - static readonly DEFAULT_WEBTORRENT_BUCKET = 'videos' - - static getDefaultConfig () { - return { - object_storage: { - enabled: true, - endpoint: 'http://' + this.getEndpointHost(), - region: this.getRegion(), - - credentials: this.getCredentialsConfig(), - - streaming_playlists: { - bucket_name: this.DEFAULT_PLAYLIST_BUCKET - }, - - videos: { - bucket_name: this.DEFAULT_WEBTORRENT_BUCKET - } - } - } - } - - static getCredentialsConfig () { - return { - access_key_id: 'AKIAIOSFODNN7EXAMPLE', - secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' - } - } - - static getEndpointHost () { - return 'localhost:9444' - } - - static getRegion () { - return 'us-east-1' - } - - static getWebTorrentBaseUrl () { - return `http://${this.DEFAULT_WEBTORRENT_BUCKET}.${this.getEndpointHost()}/` - } - - static getPlaylistBaseUrl () { - return `http://${this.DEFAULT_PLAYLIST_BUCKET}.${this.getEndpointHost()}/` - } - - static async prepareDefaultBuckets () { - await this.createBucket(this.DEFAULT_PLAYLIST_BUCKET) - await this.createBucket(this.DEFAULT_WEBTORRENT_BUCKET) - } - - static async createBucket (name: string) { - await makePostBodyRequest({ - url: this.getEndpointHost(), - path: '/ui/' + name + '?delete', - expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 - }) - - await makePostBodyRequest({ - url: this.getEndpointHost(), - path: '/ui/' + name + '?create', - expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 - }) - - await makePostBodyRequest({ - url: this.getEndpointHost(), - path: '/ui/' + name + '?make-public', - expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 - }) - } -} diff --git a/shared/extra-utils/server/plugins-command.ts b/shared/extra-utils/server/plugins-command.ts deleted file mode 100644 index 1c44711da..000000000 --- a/shared/extra-utils/server/plugins-command.ts +++ /dev/null @@ -1,257 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ - -import { readJSON, writeJSON } from 'fs-extra' -import { join } from 'path' -import { root } from '@shared/core-utils' -import { - HttpStatusCode, - PeerTubePlugin, - PeerTubePluginIndex, - PeertubePluginIndexList, - PluginPackageJson, - PluginTranslation, - PluginType, - PublicServerSetting, - RegisteredServerSettings, - ResultList -} from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class PluginsCommand extends AbstractCommand { - - static getPluginTestPath (suffix = '') { - return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix) - } - - list (options: OverrideCommandOptions & { - start?: number - count?: number - sort?: string - pluginType?: PluginType - uninstalled?: boolean - }) { - const { start, count, sort, pluginType, uninstalled } = options - const path = '/api/v1/plugins' - - return this.getRequestBody>({ - ...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 - currentPeerTubeEngine?: string - search?: string - expectedStatus?: HttpStatusCode - }) { - 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>({ - ...options, - - path, - query, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - get (options: OverrideCommandOptions & { - npmName: string - }) { - const path = '/api/v1/plugins/' + options.npmName - - return this.getRequestBody({ - ...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({ - ...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({ - ...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({ - ...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 { - const path = this.getPackageJSONPath(npmName) - - return readJSON(path) - } - - private getPackageJSONPath (npmName: string) { - return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json')) - } -} diff --git a/shared/extra-utils/server/plugins.ts b/shared/extra-utils/server/plugins.ts deleted file mode 100644 index 0f5fabd5a..000000000 --- a/shared/extra-utils/server/plugins.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ - -import { expect } from 'chai' -import { PeerTubeServer } from '../server/server' - -async function testHelloWorldRegisteredSettings (server: PeerTubeServer) { - const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' }) - - const registeredSettings = body.registeredSettings - expect(registeredSettings).to.have.length.at.least(1) - - const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name') - expect(adminNameSettings).to.not.be.undefined -} - -export { - testHelloWorldRegisteredSettings -} diff --git a/shared/extra-utils/server/redundancy-command.ts b/shared/extra-utils/server/redundancy-command.ts deleted file mode 100644 index e7a8b3c29..000000000 --- a/shared/extra-utils/server/redundancy-command.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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>({ - ...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 - }) - } -} diff --git a/shared/extra-utils/server/server.ts b/shared/extra-utils/server/server.ts deleted file mode 100644 index 339b9cabb..000000000 --- a/shared/extra-utils/server/server.ts +++ /dev/null @@ -1,392 +0,0 @@ -import { ChildProcess, fork } from 'child_process' -import { copy } from 'fs-extra' -import { join } from 'path' -import { root, randomInt } from '@shared/core-utils' -import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '../../models/videos' -import { BulkCommand } from '../bulk' -import { CLICommand } from '../cli' -import { CustomPagesCommand } from '../custom-pages' -import { FeedCommand } from '../feeds' -import { LogsCommand } from '../logs' -import { parallelTests, SQLCommand } from '../miscs' -import { AbusesCommand } from '../moderation' -import { OverviewsCommand } from '../overviews' -import { SearchCommand } from '../search' -import { SocketIOCommand } from '../socket' -import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users' -import { - BlacklistCommand, - CaptionsCommand, - ChangeOwnershipCommand, - ChannelsCommand, - HistoryCommand, - ImportsCommand, - LiveCommand, - PlaylistsCommand, - ServicesCommand, - StreamingPlaylistsCommand, - VideosCommand -} from '../videos' -import { CommentsCommand } from '../videos/comments-command' -import { ConfigCommand } from './config-command' -import { ContactFormCommand } from './contact-form-command' -import { DebugCommand } from './debug-command' -import { FollowsCommand } from './follows-command' -import { JobsCommand } from './jobs-command' -import { PluginsCommand } from './plugins-command' -import { RedundancyCommand } from './redundancy-command' -import { ServersCommand } from './servers-command' -import { StatsCommand } from './stats-command' -import { ObjectStorageCommand } from './object-storage-command' - -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 - - 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 - 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 - streamingPlaylists?: StreamingPlaylistsCommand - channels?: ChannelsCommand - comments?: CommentsCommand - sql?: SQLCommand - notifications?: NotificationsCommand - servers?: ServersCommand - login?: LoginCommand - users?: UsersCommand - objectStorage?: ObjectStorageCommand - videos?: VideosCommand - - 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://localhost:${this.port}` - this.host = `localhost:${this.port}` - this.hostname = 'localhost' - } - - setUrl (url: string) { - const parsed = new URL(url) - - this.url = url - this.host = parsed.host - this.hostname = parsed.hostname - this.port = parseInt(parsed.port) - } - - 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 = Object.create(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 forkOptions = { - silent: true, - env, - detached: true, - execArgv: options.nodeArgs || [] - } - - return new Promise((res, rej) => { - const self = this - let aggregatedLogs = '' - - this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions) - - const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs)) - const onParentExit = () => { - if (!this.app || !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() - }) - }) - } - - async kill () { - if (!this.app) return - - await this.sql.cleanup() - - process.kill(-this.app.pid) - - this.app = null - } - - private randomServer () { - const low = 10 - 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: `test${this.internalServerNumber}/tmp/`, - bin: `test${this.internalServerNumber}/bin/`, - avatars: `test${this.internalServerNumber}/avatars/`, - videos: `test${this.internalServerNumber}/videos/`, - streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`, - redundancy: `test${this.internalServerNumber}/redundancy/`, - logs: `test${this.internalServerNumber}/logs/`, - previews: `test${this.internalServerNumber}/previews/`, - thumbnails: `test${this.internalServerNumber}/thumbnails/`, - torrents: `test${this.internalServerNumber}/torrents/`, - captions: `test${this.internalServerNumber}/captions/`, - cache: `test${this.internalServerNumber}/cache/`, - plugins: `test${this.internalServerNumber}/plugins/` - }, - 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.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.streamingPlaylists = new StreamingPlaylistsCommand(this) - this.channels = new ChannelsCommand(this) - this.comments = new CommentsCommand(this) - this.sql = new SQLCommand(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.objectStorage = new ObjectStorageCommand(this) - } -} diff --git a/shared/extra-utils/server/servers-command.ts b/shared/extra-utils/server/servers-command.ts deleted file mode 100644 index 47420c95f..000000000 --- a/shared/extra-utils/server/servers-command.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { exec } from 'child_process' -import { copy, ensureDir, readFile, remove } from 'fs-extra' -import { basename, join } from 'path' -import { root } from '@shared/core-utils' -import { HttpStatusCode } from '@shared/models' -import { getFileSize, isGithubCI, wait } from '../miscs' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class ServersCommand extends AbstractCommand { - - static flushTests (internalServerNumber: number) { - return new Promise((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 - }) - } - - async cleanupTests () { - const p: Promise[] = [] - - if (isGithubCI()) { - 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) { - p.push(ServersCommand.flushTests(this.server.internalServerNumber)) - } - - if (this.server.customConfigFile) { - p.push(remove(this.server.customConfigFile)) - } - - return p - } - - 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) - } - - buildWebTorrentFilePath (fileUrl: string) { - return this.buildDirectory(join('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) - } -} diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts deleted file mode 100644 index 21ab9405b..000000000 --- a/shared/extra-utils/server/servers.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ensureDir } from 'fs-extra' -import { isGithubCI } from '../miscs' -import { PeerTubeServer, RunServerOptions } from './server' - -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[] = [] - - for (let i = 1; i <= totalServers; i++) { - serverPromises.push(createSingleServer(i, configOverride, options)) - } - - return Promise.all(serverPromises) -} - -async 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[] = [] - for (const server of servers) { - p = p.concat(server.servers.cleanupTests()) - } - - return Promise.all(p) -} - -// --------------------------------------------------------------------------- - -export { - createSingleServer, - createMultipleServers, - cleanupTests, - killallServers -} diff --git a/shared/extra-utils/server/stats-command.ts b/shared/extra-utils/server/stats-command.ts deleted file mode 100644 index 64a452306..000000000 --- a/shared/extra-utils/server/stats-command.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { HttpStatusCode, ServerStats } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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({ - ...options, - - path, - query, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/server/tracker.ts b/shared/extra-utils/server/tracker.ts deleted file mode 100644 index ed43a5924..000000000 --- a/shared/extra-utils/server/tracker.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expect } from 'chai' -import { sha1 } from '@shared/core-utils/crypto' -import { makeGetRequest } from '../requests' - -async function hlsInfohashExist (serverUrl: string, masterPlaylistUrl: string, fileNumber: number) { - const path = '/tracker/announce' - - const infohash = sha1(`2${masterPlaylistUrl}+V${fileNumber}`) - - // From bittorrent-tracker - const infohashBinary = escape(Buffer.from(infohash, 'hex').toString('binary')).replace(/[@*/+]/g, function (char) { - return '%' + char.charCodeAt(0).toString(16).toUpperCase() - }) - - const res = await makeGetRequest({ - url: serverUrl, - path, - rawQuery: `peer_id=-WW0105-NkvYO/egUAr4&info_hash=${infohashBinary}&port=42100`, - expectedStatus: 200 - }) - - expect(res.text).to.not.contain('failure') -} - -export { - hlsInfohashExist -} diff --git a/shared/extra-utils/shared/abstract-command.ts b/shared/extra-utils/shared/abstract-command.ts deleted file mode 100644 index a57c857fc..000000000 --- a/shared/extra-utils/shared/abstract-command.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { isAbsolute, join } from 'path' -import { root } from '../miscs/tests' -import { - makeDeleteRequest, - makeGetRequest, - makePostBodyRequest, - makePutBodyRequest, - makeUploadRequest, - unwrapBody, - unwrapText -} from '../requests/requests' -import { PeerTubeServer } from '../server/server' - -export interface OverrideCommandOptions { - token?: string - expectedStatus?: number -} - -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: number - - // Common optional request parameters - contentType?: string - accept?: string - redirects?: number - range?: string - host?: string - headers?: { [ name: string ]: string } - requestType?: 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 (options: InternalGetCommandOptions) { - return unwrapBody(this.getRequest(options)) - } - - protected getRequestText (options: InternalGetCommandOptions) { - return unwrapText(this.getRequest(options)) - } - - protected getRawRequest (options: Omit) { - 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 } - }) { - const { fields } = options - - return makePutBodyRequest({ - ...this.buildCommonRequestOptions(options), - - fields - }) - } - - protected postBodyRequest (options: InternalCommonCommandOptions & { - fields?: { [ fieldName: string ]: any } - }) { - const { fields } = options - - return makePostBodyRequest({ - ...this.buildCommonRequestOptions(options), - - fields - }) - } - - 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 - : join(root(), 'server', 'tests', 'fixtures', 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 } = options - - return { - url: url ?? this.server.url, - path, - - token: this.buildCommonRequestToken(options), - expectedStatus: this.buildExpectedStatus(options), - - redirects, - contentType, - range, - host, - accept, - headers, - type: requestType, - xForwardedFor - } - } - - protected buildCommonRequestToken (options: Pick) { - const { token } = options - - const fallbackToken = options.implicitToken - ? this.server.accessToken - : undefined - - return token !== undefined ? token : fallbackToken - } - - protected buildExpectedStatus (options: Pick) { - const { expectedStatus, defaultExpectedStatus } = options - - return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus - } -} - -export { - AbstractCommand -} diff --git a/shared/extra-utils/shared/index.ts b/shared/extra-utils/shared/index.ts deleted file mode 100644 index e807ab4f7..000000000 --- a/shared/extra-utils/shared/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './abstract-command' diff --git a/shared/extra-utils/socket/index.ts b/shared/extra-utils/socket/index.ts deleted file mode 100644 index 594329b2f..000000000 --- a/shared/extra-utils/socket/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './socket-io-command' diff --git a/shared/extra-utils/socket/socket-io-command.ts b/shared/extra-utils/socket/socket-io-command.ts deleted file mode 100644 index c277ead28..000000000 --- a/shared/extra-utils/socket/socket-io-command.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { io } from 'socket.io-client' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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') - } -} diff --git a/shared/extra-utils/users/accounts-command.ts b/shared/extra-utils/users/accounts-command.ts deleted file mode 100644 index 98d9d5927..000000000 --- a/shared/extra-utils/users/accounts-command.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { HttpStatusCode, ResultList } from '@shared/models' -import { Account, ActorFollow } from '../../models/actors' -import { AccountVideoRate, VideoRateType } from '../../models/videos' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class AccountsCommand extends AbstractCommand { - - list (options: OverrideCommandOptions & { - sort?: string // default -createdAt - } = {}) { - const { sort = '-createdAt' } = options - const path = '/api/v1/accounts' - - return this.getRequestBody>({ - ...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({ - ...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>({ - ...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>({ - ...options, - - path, - query, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/users/actors.ts b/shared/extra-utils/users/actors.ts deleted file mode 100644 index 12c3e078a..000000000 --- a/shared/extra-utils/users/actors.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ - -import { expect } from 'chai' -import { pathExists, readdir } from 'fs-extra' -import { join } from 'path' -import { root } from '@shared/core-utils' -import { Account, VideoChannel } from '@shared/models' -import { PeerTubeServer } from '../server' - -async function expectChannelsFollows (options: { - server: PeerTubeServer - handle: string - followers: number - following: number -}) { - const { server } = options - const { data } = await server.channels.list() - - return expectActorFollow({ ...options, data }) -} - -async function expectAccountFollows (options: { - server: PeerTubeServer - handle: string - followers: number - following: number -}) { - const { server } = options - const { data } = await server.accounts.list() - - return expectActorFollow({ ...options, data }) -} - -async function checkActorFilesWereRemoved (filename: string, serverNumber: number) { - const testDirectory = 'test' + serverNumber - - for (const directory of [ 'avatars' ]) { - const directoryPath = join(root(), testDirectory, directory) - - const directoryExists = await pathExists(directoryPath) - expect(directoryExists).to.be.true - - const files = await readdir(directoryPath) - for (const file of files) { - expect(file).to.not.contain(filename) - } - } -} - -export { - expectAccountFollows, - expectChannelsFollows, - checkActorFilesWereRemoved -} - -// --------------------------------------------------------------------------- - -function expectActorFollow (options: { - server: PeerTubeServer - data: (Account | VideoChannel)[] - handle: string - followers: number - following: number -}) { - const { server, data, handle, followers, following } = options - - const actor = data.find(a => a.name + '@' + a.host === handle) - const message = `${handle} on ${server.url}` - - expect(actor, message).to.exist - expect(actor.followersCount).to.equal(followers, message) - expect(actor.followingCount).to.equal(following, message) -} diff --git a/shared/extra-utils/users/blocklist-command.ts b/shared/extra-utils/users/blocklist-command.ts deleted file mode 100644 index 2e7ed074d..000000000 --- a/shared/extra-utils/users/blocklist-command.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ - -import { AccountBlock, BlockStatus, HttpStatusCode, ResultList, ServerBlock } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -type ListBlocklistOptions = OverrideCommandOptions & { - start: number - count: number - sort: string // default -createdAt -} - -export class BlocklistCommand extends AbstractCommand { - - listMyAccountBlocklist (options: ListBlocklistOptions) { - const path = '/api/v1/users/me/blocklist/accounts' - - return this.listBlocklist(options, path) - } - - listMyServerBlocklist (options: ListBlocklistOptions) { - const path = '/api/v1/users/me/blocklist/servers' - - return this.listBlocklist(options, path) - } - - listServerAccountBlocklist (options: ListBlocklistOptions) { - const path = '/api/v1/server/blocklist/accounts' - - return this.listBlocklist(options, path) - } - - listServerServerBlocklist (options: ListBlocklistOptions) { - const path = '/api/v1/server/blocklist/servers' - - return this.listBlocklist(options, path) - } - - // --------------------------------------------------------------------------- - - getStatus (options: OverrideCommandOptions & { - accounts?: string[] - hosts?: string[] - }) { - const { accounts, hosts } = options - - const path = '/api/v1/blocklist/status' - - return this.getRequestBody({ - ...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 (options: ListBlocklistOptions, path: string) { - const { start, count, sort = '-createdAt' } = options - - return this.getRequestBody>({ - ...options, - - path, - query: { start, count, sort }, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - -} diff --git a/shared/extra-utils/users/index.ts b/shared/extra-utils/users/index.ts deleted file mode 100644 index 460a06f70..000000000 --- a/shared/extra-utils/users/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './accounts-command' -export * from './actors' -export * from './blocklist-command' -export * from './login' -export * from './login-command' -export * from './notifications' -export * from './notifications-command' -export * from './subscriptions-command' -export * from './users-command' diff --git a/shared/extra-utils/users/login-command.ts b/shared/extra-utils/users/login-command.ts deleted file mode 100644 index 143f72a59..000000000 --- a/shared/extra-utils/users/login-command.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models' -import { unwrapBody } from '../requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class LoginCommand extends AbstractCommand { - - login (options: OverrideCommandOptions & { - client?: { id?: string, secret?: string } - user?: { username: string, password?: string } - } = {}) { - const { client = this.server.store.client, user = this.server.store.user } = 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' - } - - return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({ - ...options, - - path, - requestType: 'form', - fields: body, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - })) - } - - getAccessToken (arg1?: { username: string, password?: string }): Promise - getAccessToken (arg1: string, password?: string): Promise - 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: 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 - }) - } -} diff --git a/shared/extra-utils/users/login.ts b/shared/extra-utils/users/login.ts deleted file mode 100644 index f1df027d3..000000000 --- a/shared/extra-utils/users/login.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PeerTubeServer } from '../server/server' - -function setAccessTokensToServers (servers: PeerTubeServer[]) { - const tasks: Promise[] = [] - - for (const server of servers) { - const p = server.login.getAccessToken() - .then(t => { server.accessToken = t }) - tasks.push(p) - } - - return Promise.all(tasks) -} - -// --------------------------------------------------------------------------- - -export { - setAccessTokensToServers -} diff --git a/shared/extra-utils/users/notifications-command.ts b/shared/extra-utils/users/notifications-command.ts deleted file mode 100644 index 692420b8b..000000000 --- a/shared/extra-utils/users/notifications-command.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { HttpStatusCode, ResultList } from '@shared/models' -import { UserNotification, UserNotificationSetting } from '../../models/users' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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>({ - ...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] - } -} diff --git a/shared/extra-utils/users/notifications.ts b/shared/extra-utils/users/notifications.ts deleted file mode 100644 index 07ccb0f8d..000000000 --- a/shared/extra-utils/users/notifications.ts +++ /dev/null @@ -1,795 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ - -import { expect } from 'chai' -import { inspect } from 'util' -import { AbuseState, PluginType } from '@shared/models' -import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' -import { MockSmtpServer } from '../mock-servers/mock-email' -import { PeerTubeServer } from '../server' -import { doubleFollow } from '../server/follows' -import { createMultipleServers } from '../server/servers' -import { setAccessTokensToServers } from './login' - -type CheckerBaseParams = { - server: PeerTubeServer - emails: any[] - socketNotifications: UserNotification[] - token: string - check?: { web: boolean, mail: boolean } -} - -type CheckerType = 'presence' | 'absence' - -function getAllNotificationsSettings (): UserNotificationSetting { - return { - newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, - newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL - } -} - -async function checkNewVideoFromSubscription (options: CheckerBaseParams & { - videoName: string - shortUUID: string - checkType: CheckerType -}) { - const { videoName, shortUUID } = options - const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - checkVideo(notification.video, videoName, shortUUID) - checkActor(notification.video.channel) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1 - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkVideoIsPublished (options: CheckerBaseParams & { - videoName: string - shortUUID: string - checkType: CheckerType -}) { - const { videoName, shortUUID } = options - const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - checkVideo(notification.video, videoName, shortUUID) - checkActor(notification.video.channel) - } else { - expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) - } - } - - function emailNotificationFinder (email: object) { - const text: string = email['text'] - return text.includes(shortUUID) && text.includes('Your video') - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkMyVideoImportIsFinished (options: CheckerBaseParams & { - videoName: string - shortUUID: string - url: string - success: boolean - checkType: CheckerType -}) { - const { videoName, shortUUID, url, success } = options - - const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.videoImport.targetUrl).to.equal(url) - - if (success) checkVideo(notification.videoImport.video, videoName, shortUUID) - } else { - expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url) - } - } - - function emailNotificationFinder (email: object) { - const text: string = email['text'] - const toFind = success ? ' finished' : ' error' - - return text.includes(url) && text.includes(toFind) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkUserRegistered (options: CheckerBaseParams & { - username: string - checkType: CheckerType -}) { - const { username } = options - const notificationType = UserNotificationType.NEW_USER_REGISTRATION - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - checkActor(notification.account) - expect(notification.account.name).to.equal(username) - } else { - expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username) - } - } - - function emailNotificationFinder (email: object) { - const text: string = email['text'] - - return text.includes(' registered.') && text.includes(username) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkNewActorFollow (options: CheckerBaseParams & { - followType: 'channel' | 'account' - followerName: string - followerDisplayName: string - followingDisplayName: string - checkType: CheckerType -}) { - const { followType, followerName, followerDisplayName, followingDisplayName } = options - const notificationType = UserNotificationType.NEW_FOLLOW - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - checkActor(notification.actorFollow.follower) - expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName) - expect(notification.actorFollow.follower.name).to.equal(followerName) - expect(notification.actorFollow.follower.host).to.not.be.undefined - - const following = notification.actorFollow.following - expect(following.displayName).to.equal(followingDisplayName) - expect(following.type).to.equal(followType) - } else { - expect(notification).to.satisfy(n => { - return n.type !== notificationType || - (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName) - }) - } - } - - function emailNotificationFinder (email: object) { - const text: string = email['text'] - - return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkNewInstanceFollower (options: CheckerBaseParams & { - followerHost: string - checkType: CheckerType -}) { - const { followerHost } = options - const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - checkActor(notification.actorFollow.follower) - expect(notification.actorFollow.follower.name).to.equal('peertube') - expect(notification.actorFollow.follower.host).to.equal(followerHost) - - expect(notification.actorFollow.following.name).to.equal('peertube') - } else { - expect(notification).to.satisfy(n => { - return n.type !== notificationType || n.actorFollow.follower.host !== followerHost - }) - } - } - - function emailNotificationFinder (email: object) { - const text: string = email['text'] - - return text.includes('instance has a new follower') && text.includes(followerHost) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkAutoInstanceFollowing (options: CheckerBaseParams & { - followerHost: string - followingHost: string - checkType: CheckerType -}) { - const { followerHost, followingHost } = options - const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - const following = notification.actorFollow.following - checkActor(following) - expect(following.name).to.equal('peertube') - expect(following.host).to.equal(followingHost) - - expect(notification.actorFollow.follower.name).to.equal('peertube') - expect(notification.actorFollow.follower.host).to.equal(followerHost) - } else { - expect(notification).to.satisfy(n => { - return n.type !== notificationType || n.actorFollow.following.host !== followingHost - }) - } - } - - function emailNotificationFinder (email: object) { - const text: string = email['text'] - - return text.includes(' automatically followed a new instance') && text.includes(followingHost) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkCommentMention (options: CheckerBaseParams & { - shortUUID: string - commentId: number - threadId: number - byAccountDisplayName: string - checkType: CheckerType -}) { - const { shortUUID, commentId, threadId, byAccountDisplayName } = options - const notificationType = UserNotificationType.COMMENT_MENTION - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - checkComment(notification.comment, commentId, threadId) - checkActor(notification.comment.account) - expect(notification.comment.account.displayName).to.equal(byAccountDisplayName) - - checkVideo(notification.comment.video, undefined, shortUUID) - } else { - expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId) - } - } - - function emailNotificationFinder (email: object) { - const text: string = email['text'] - - return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -let lastEmailCount = 0 - -async function checkNewCommentOnMyVideo (options: CheckerBaseParams & { - shortUUID: string - commentId: number - threadId: number - checkType: CheckerType -}) { - const { server, shortUUID, commentId, threadId, checkType, emails } = options - const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - checkComment(notification.comment, commentId, threadId) - checkActor(notification.comment.account) - checkVideo(notification.comment.video, undefined, shortUUID) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.comment === undefined || n.comment.id !== commentId - }) - } - } - - const commentUrl = `http://localhost:${server.port}/w/${shortUUID};threadId=${threadId}` - - function emailNotificationFinder (email: object) { - return email['text'].indexOf(commentUrl) !== -1 - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) - - if (checkType === 'presence') { - // We cannot detect email duplicates, so check we received another email - expect(emails).to.have.length.above(lastEmailCount) - lastEmailCount = emails.length - } -} - -async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & { - shortUUID: string - videoName: string - checkType: CheckerType -}) { - const { shortUUID, videoName } = options - const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.abuse.id).to.be.a('number') - checkVideo(notification.abuse.video, videoName, shortUUID) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1 - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkNewAbuseMessage (options: CheckerBaseParams & { - abuseId: number - message: string - toEmail: string - checkType: CheckerType -}) { - const { abuseId, message, toEmail } = options - const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.abuse.id).to.equal(abuseId) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - const to = email['to'].filter(t => t.address === toEmail) - - return text.indexOf(message) !== -1 && to.length !== 0 - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkAbuseStateChange (options: CheckerBaseParams & { - abuseId: number - state: AbuseState - checkType: CheckerType -}) { - const { abuseId, state } = options - const notificationType = UserNotificationType.ABUSE_STATE_CHANGE - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.abuse.id).to.equal(abuseId) - expect(notification.abuse.state).to.equal(state) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - - const contains = state === AbuseState.ACCEPTED - ? ' accepted' - : ' rejected' - - return text.indexOf(contains) !== -1 - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & { - shortUUID: string - videoName: string - checkType: CheckerType -}) { - const { shortUUID, videoName } = options - const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.abuse.id).to.be.a('number') - checkVideo(notification.abuse.comment.video, videoName, shortUUID) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1 - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & { - displayName: string - checkType: CheckerType -}) { - const { displayName } = options - const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.abuse.id).to.be.a('number') - expect(notification.abuse.account.displayName).to.equal(displayName) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1 - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & { - shortUUID: string - videoName: string - checkType: CheckerType -}) { - const { shortUUID, videoName } = options - const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.videoBlacklist.video.id).to.be.a('number') - checkVideo(notification.videoBlacklist.video, videoName, shortUUID) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1 - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & { - shortUUID: string - videoName: string - blacklistType: 'blacklist' | 'unblacklist' -}) { - const { videoName, shortUUID, blacklistType } = options - const notificationType = blacklistType === 'blacklist' - ? UserNotificationType.BLACKLIST_ON_MY_VIDEO - : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO - - function notificationChecker (notification: UserNotification) { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video - - checkVideo(video, videoName, shortUUID) - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - const blacklistText = blacklistType === 'blacklist' - ? 'blacklisted' - : 'unblacklisted' - - return text.includes(shortUUID) && text.includes(blacklistText) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' }) -} - -async function checkNewPeerTubeVersion (options: CheckerBaseParams & { - latestVersion: string - checkType: CheckerType -}) { - const { latestVersion } = options - const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.peertube).to.exist - expect(notification.peertube.latestVersion).to.equal(latestVersion) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - - return text.includes(latestVersion) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function checkNewPluginVersion (options: CheckerBaseParams & { - pluginType: PluginType - pluginName: string - checkType: CheckerType -}) { - const { pluginName, pluginType } = options - const notificationType = UserNotificationType.NEW_PLUGIN_VERSION - - function notificationChecker (notification: UserNotification, checkType: CheckerType) { - if (checkType === 'presence') { - expect(notification).to.not.be.undefined - expect(notification.type).to.equal(notificationType) - - expect(notification.plugin.name).to.equal(pluginName) - expect(notification.plugin.type).to.equal(pluginType) - } else { - expect(notification).to.satisfy((n: UserNotification) => { - return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName - }) - } - } - - function emailNotificationFinder (email: object) { - const text = email['text'] - - return text.includes(pluginName) - } - - await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) -} - -async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) { - const userNotifications: UserNotification[] = [] - const adminNotifications: UserNotification[] = [] - const adminNotificationsServer2: UserNotification[] = [] - const emails: object[] = [] - - const port = await MockSmtpServer.Instance.collectEmails(emails) - - const overrideConfig = { - smtp: { - hostname: 'localhost', - port - }, - signup: { - limit: 20 - } - } - const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) - - await setAccessTokensToServers(servers) - - if (serversCount > 1) { - await doubleFollow(servers[0], servers[1]) - } - - const user = { username: 'user_1', password: 'super password' } - await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 }) - const userAccessToken = await servers[0].login.getAccessToken(user) - - await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() }) - await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() }) - - if (serversCount > 1) { - await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() }) - } - - { - const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken }) - socket.on('new-notification', n => userNotifications.push(n)) - } - { - const socket = servers[0].socketIO.getUserNotificationSocket() - socket.on('new-notification', n => adminNotifications.push(n)) - } - - if (serversCount > 1) { - const socket = servers[1].socketIO.getUserNotificationSocket() - socket.on('new-notification', n => adminNotificationsServer2.push(n)) - } - - const { videoChannels } = await servers[0].users.getMyInfo() - const channelId = videoChannels[0].id - - return { - userNotifications, - adminNotifications, - adminNotificationsServer2, - userAccessToken, - emails, - servers, - channelId - } -} - -// --------------------------------------------------------------------------- - -export { - getAllNotificationsSettings, - - CheckerBaseParams, - CheckerType, - checkMyVideoImportIsFinished, - checkUserRegistered, - checkAutoInstanceFollowing, - checkVideoIsPublished, - checkNewVideoFromSubscription, - checkNewActorFollow, - checkNewCommentOnMyVideo, - checkNewBlacklistOnMyVideo, - checkCommentMention, - checkNewVideoAbuseForModerators, - checkVideoAutoBlacklistForModerators, - checkNewAbuseMessage, - checkAbuseStateChange, - checkNewInstanceFollower, - prepareNotificationsTest, - checkNewCommentAbuseForModerators, - checkNewAccountAbuseForModerators, - checkNewPeerTubeVersion, - checkNewPluginVersion -} - -// --------------------------------------------------------------------------- - -async function checkNotification (options: CheckerBaseParams & { - notificationChecker: (notification: UserNotification, checkType: CheckerType) => void - emailNotificationFinder: (email: object) => boolean - checkType: CheckerType -}) { - const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options - - const check = options.check || { web: true, mail: true } - - if (check.web) { - const notification = await server.notifications.getLatest({ token: token }) - - if (notification || checkType !== 'absence') { - notificationChecker(notification, checkType) - } - - const socketNotification = socketNotifications.find(n => { - try { - notificationChecker(n, 'presence') - return true - } catch { - return false - } - }) - - if (checkType === 'presence') { - const obj = inspect(socketNotifications, { depth: 5 }) - expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined - } else { - const obj = inspect(socketNotification, { depth: 5 }) - expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined - } - } - - if (check.mail) { - // Last email - const email = emails - .slice() - .reverse() - .find(e => emailNotificationFinder(e)) - - if (checkType === 'presence') { - const texts = emails.map(e => e.text) - expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined - } else { - expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined - } - } -} - -function checkVideo (video: any, videoName?: string, shortUUID?: string) { - if (videoName) { - expect(video.name).to.be.a('string') - expect(video.name).to.not.be.empty - expect(video.name).to.equal(videoName) - } - - if (shortUUID) { - expect(video.shortUUID).to.be.a('string') - expect(video.shortUUID).to.not.be.empty - expect(video.shortUUID).to.equal(shortUUID) - } - - expect(video.id).to.be.a('number') -} - -function checkActor (actor: any) { - expect(actor.displayName).to.be.a('string') - expect(actor.displayName).to.not.be.empty - expect(actor.host).to.not.be.undefined -} - -function checkComment (comment: any, commentId: number, threadId: number) { - expect(comment.id).to.equal(commentId) - expect(comment.threadId).to.equal(threadId) -} diff --git a/shared/extra-utils/users/subscriptions-command.ts b/shared/extra-utils/users/subscriptions-command.ts deleted file mode 100644 index edc60e612..000000000 --- a/shared/extra-utils/users/subscriptions-command.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { HttpStatusCode, ResultList, Video, VideoChannel } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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>({ - ...options, - - path, - query: { - sort, - search - }, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - listVideos (options: OverrideCommandOptions & { - sort?: string // default -createdAt - } = {}) { - const { sort = '-createdAt' } = options - const path = '/api/v1/users/me/subscriptions/videos' - - return this.getRequestBody>({ - ...options, - - path, - query: { sort }, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - get (options: OverrideCommandOptions & { - uri: string - }) { - const path = '/api/v1/users/me/subscriptions/' + options.uri - - return this.getRequestBody({ - ...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 - }) - } -} diff --git a/shared/extra-utils/users/users-command.ts b/shared/extra-utils/users/users-command.ts deleted file mode 100644 index 90c5f2183..000000000 --- a/shared/extra-utils/users/users-command.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { omit } from 'lodash' -import { pick } from '@shared/core-utils' -import { - HttpStatusCode, - MyUser, - ResultList, - User, - UserAdminFlag, - UserCreateResult, - UserRole, - UserUpdate, - UserUpdateMe, - UserVideoQuota, - UserVideoRate -} from '@shared/models' -import { ScopedToken } from '@shared/models/users/user-scoped-token' -import { unwrapBody } from '../requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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({ - ...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?: UserRole - adminFlags?: UserAdminFlag - }) { - const { - username, - adminFlags, - password = 'password', - videoQuota = 42000000, - videoQuotaDaily = -1, - 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?: UserRole) { - 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 - } - } - - async generateUserAndToken (username: string, role?: UserRole) { - const password = 'password' - await this.create({ username, password, role }) - - return this.server.login.getAccessToken({ username, password }) - } - - register (options: OverrideCommandOptions & { - username: string - password?: string - displayName?: string - channel?: { - name: string - displayName: string - } - }) { - const { username, password = 'password', displayName, channel } = options - const path = '/api/v1/users/register' - - return this.postBodyRequest({ - ...options, - - path, - fields: { - username, - password, - email: username + '@example.com', - displayName, - channel - }, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - - // --------------------------------------------------------------------------- - - getMyInfo (options: OverrideCommandOptions = {}) { - const path = '/api/v1/users/me' - - return this.getRequestBody({ - ...options, - - path, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - getMyQuotaUsed (options: OverrideCommandOptions = {}) { - const path = '/api/v1/users/me/video-quota-used' - - return this.getRequestBody({ - ...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({ - ...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, 'url', 'accessToken') - - 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({ - ...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>({ - ...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?: UserAdminFlag - pluginAuth?: string - role?: UserRole - }) { - 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 - }) - } -} diff --git a/shared/extra-utils/videos/blacklist-command.ts b/shared/extra-utils/videos/blacklist-command.ts deleted file mode 100644 index 3a2ef89ba..000000000 --- a/shared/extra-utils/videos/blacklist-command.ts +++ /dev/null @@ -1,76 +0,0 @@ - -import { HttpStatusCode, ResultList } from '@shared/models' -import { VideoBlacklist, VideoBlacklistType } from '../../models/videos' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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 - } = {}) { - const { sort, type } = options - const path = '/api/v1/videos/blacklist/' - - const query = { sort, type } - - return this.getRequestBody>({ - ...options, - - path, - query, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/videos/captions-command.ts b/shared/extra-utils/videos/captions-command.ts deleted file mode 100644 index a65ea99e3..000000000 --- a/shared/extra-utils/videos/captions-command.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { HttpStatusCode, ResultList, VideoCaption } from '@shared/models' -import { buildAbsoluteFixturePath } from '../miscs' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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 - }) { - const { videoId } = options - const path = '/api/v1/videos/' + videoId + '/captions' - - return this.getRequestBody>({ - ...options, - - path, - 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 - }) - } -} diff --git a/shared/extra-utils/videos/captions.ts b/shared/extra-utils/videos/captions.ts deleted file mode 100644 index 35e722408..000000000 --- a/shared/extra-utils/videos/captions.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect } from 'chai' -import request from 'supertest' -import { HttpStatusCode } from '@shared/models' - -async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) { - const res = await request(url) - .get(captionPath) - .expect(HttpStatusCode.OK_200) - - if (toTest instanceof RegExp) { - expect(res.text).to.match(toTest) - } else { - expect(res.text).to.contain(toTest) - } -} - -// --------------------------------------------------------------------------- - -export { - testCaptionFile -} diff --git a/shared/extra-utils/videos/change-ownership-command.ts b/shared/extra-utils/videos/change-ownership-command.ts deleted file mode 100644 index ad4c726ef..000000000 --- a/shared/extra-utils/videos/change-ownership-command.ts +++ /dev/null @@ -1,68 +0,0 @@ - -import { HttpStatusCode, ResultList, VideoChangeOwnership } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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>({ - ...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 - }) - } -} diff --git a/shared/extra-utils/videos/channels-command.ts b/shared/extra-utils/videos/channels-command.ts deleted file mode 100644 index e406e570b..000000000 --- a/shared/extra-utils/videos/channels-command.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { pick } from '@shared/core-utils' -import { ActorFollow, HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models' -import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' -import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' -import { unwrapBody } from '../requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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>({ - ...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>({ - ...options, - - path, - query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) }, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - async create (options: OverrideCommandOptions & { - attributes: Partial - }) { - 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({ - ...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>({ - ...options, - - path, - query, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/videos/channels.ts b/shared/extra-utils/videos/channels.ts deleted file mode 100644 index 756c47453..000000000 --- a/shared/extra-utils/videos/channels.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PeerTubeServer } from '../server/server' - -function setDefaultVideoChannel (servers: PeerTubeServer[]) { - const tasks: Promise[] = [] - - 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) -} - -export { - setDefaultVideoChannel -} diff --git a/shared/extra-utils/videos/comments-command.ts b/shared/extra-utils/videos/comments-command.ts deleted file mode 100644 index f0d163a07..000000000 --- a/shared/extra-utils/videos/comments-command.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { pick } from 'lodash' -import { HttpStatusCode, ResultList, VideoComment, VideoCommentThreads, VideoCommentThreadTree } from '@shared/models' -import { unwrapBody } from '../requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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 - search?: string - searchAccount?: string - searchVideo?: string - } = {}) { - const { sort = '-createdAt' } = options - const path = '/api/v1/videos/comments' - - const query = { sort, ...pick(options, [ 'start', 'count', 'isLocal', 'search', 'searchAccount', 'searchVideo' ]) } - - return this.getRequestBody>({ - ...options, - - path, - query, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - listThreads (options: OverrideCommandOptions & { - videoId: number | string - start?: number - count?: number - sort?: string - }) { - const { start, count, sort, videoId } = options - const path = '/api/v1/videos/' + videoId + '/comment-threads' - - return this.getRequestBody({ - ...options, - - path, - query: { start, count, sort }, - 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({ - ...options, - - path, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - async createThread (options: OverrideCommandOptions & { - videoId: number | string - text: string - }) { - const { videoId, text } = options - const path = '/api/v1/videos/' + videoId + '/comment-threads' - - const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({ - ...options, - - path, - fields: { text }, - 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 - }) { - const { videoId, toCommentId, text } = options - const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId - - const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({ - ...options, - - path, - fields: { text }, - 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 - }) - } -} diff --git a/shared/extra-utils/videos/history-command.ts b/shared/extra-utils/videos/history-command.ts deleted file mode 100644 index 13b7150c1..000000000 --- a/shared/extra-utils/videos/history-command.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { HttpStatusCode, ResultList, Video } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class HistoryCommand extends AbstractCommand { - - wathVideo (options: OverrideCommandOptions & { - videoId: number | string - currentTime: number - }) { - const { videoId, currentTime } = options - - const path = '/api/v1/videos/' + videoId + '/watching' - const fields = { currentTime } - - return this.putBodyRequest({ - ...options, - - path, - fields, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - - list (options: OverrideCommandOptions & { - search?: string - } = {}) { - const { search } = options - const path = '/api/v1/users/me/history/videos' - - return this.getRequestBody>({ - ...options, - - path, - query: { - search - }, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - remove (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 - }) - } -} diff --git a/shared/extra-utils/videos/imports-command.ts b/shared/extra-utils/videos/imports-command.ts deleted file mode 100644 index e4944694d..000000000 --- a/shared/extra-utils/videos/imports-command.ts +++ /dev/null @@ -1,47 +0,0 @@ - -import { HttpStatusCode, ResultList } from '@shared/models' -import { VideoImport, VideoImportCreate } from '../../models/videos' -import { unwrapBody } from '../requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class ImportsCommand extends AbstractCommand { - - importVideo (options: OverrideCommandOptions & { - attributes: VideoImportCreate & { torrentfile?: string } - }) { - const { attributes } = options - const path = '/api/v1/videos/imports' - - let attaches: any = {} - if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile } - - return unwrapBody(this.postUploadRequest({ - ...options, - - path, - attaches, - fields: options.attributes, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - })) - } - - getMyVideoImports (options: OverrideCommandOptions & { - sort?: string - } = {}) { - const { sort } = options - const path = '/api/v1/users/me/videos/imports' - - const query = {} - if (sort) query['sort'] = sort - - return this.getRequestBody>({ - ...options, - - path, - query: { sort }, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/videos/index.ts b/shared/extra-utils/videos/index.ts deleted file mode 100644 index 26e663f46..000000000 --- a/shared/extra-utils/videos/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export * from './blacklist-command' -export * from './captions-command' -export * from './captions' -export * from './change-ownership-command' -export * from './channels' -export * from './channels-command' -export * from './comments-command' -export * from './history-command' -export * from './imports-command' -export * from './live-command' -export * from './live' -export * from './playlists-command' -export * from './playlists' -export * from './services-command' -export * from './streaming-playlists-command' -export * from './streaming-playlists' -export * from './comments-command' -export * from './videos-command' -export * from './videos' diff --git a/shared/extra-utils/videos/live-command.ts b/shared/extra-utils/videos/live-command.ts deleted file mode 100644 index 74f5d3089..000000000 --- a/shared/extra-utils/videos/live-command.ts +++ /dev/null @@ -1,155 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ - -import { readdir } from 'fs-extra' -import { omit } from 'lodash' -import { join } from 'path' -import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models' -import { wait } from '../miscs' -import { unwrapBody } from '../requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' -import { sendRTMPStream, testFfmpegStreamError } from './live' - -export class LiveCommand extends AbstractCommand { - - get (options: OverrideCommandOptions & { - videoId: number | string - }) { - const path = '/api/v1/videos/live' - - return this.getRequestBody({ - ...options, - - path: path + '/' + options.videoId, - 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 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 }) - } - - waitUntilSegmentGeneration (options: OverrideCommandOptions & { - videoUUID: string - resolution: number - segment: number - }) { - const { resolution, segment, videoUUID } = options - const segmentName = `${resolution}-00000${segment}.ts` - - return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false) - } - - async waitUntilSaved (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) - } - - 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: VideoState - }) { - 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) - } -} diff --git a/shared/extra-utils/videos/live.ts b/shared/extra-utils/videos/live.ts deleted file mode 100644 index d3665bc90..000000000 --- a/shared/extra-utils/videos/live.ts +++ /dev/null @@ -1,137 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ - -import { expect } from 'chai' -import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg' -import { pathExists, readdir } from 'fs-extra' -import { join } from 'path' -import { buildAbsoluteFixturePath, wait } from '../miscs' -import { PeerTubeServer } from '../server/server' - -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 50') - command.outputOption('-keyint_min 2') - 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.run() - - return command -} - -function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) { - return new Promise((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, 35000) - } 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 waitUntilLiveSavedOnAllServers (servers: PeerTubeServer[], videoId: string) { - for (const server of servers) { - await server.live.waitUntilSaved({ videoId }) - } -} - -async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) { - const basePath = server.servers.buildDirectory('streaming-playlists') - const hlsPath = join(basePath, 'hls', videoUUID) - - if (resolutions.length === 0) { - const result = await pathExists(hlsPath) - expect(result).to.be.false - - return - } - - const files = await readdir(hlsPath) - - // fragmented file and playlist per resolution + master playlist + segments sha256 json file - expect(files).to.have.lengthOf(resolutions.length * 2 + 2) - - for (const resolution of resolutions) { - const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`)) - expect(fragmentedFile).to.exist - - const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`)) - expect(playlistFile).to.exist - } - - const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8')) - expect(masterPlaylistFile).to.exist - - const shaFile = files.find(f => f.endsWith('-segments-sha256.json')) - expect(shaFile).to.exist -} - -export { - sendRTMPStream, - waitFfmpegUntilError, - testFfmpegStreamError, - stopFfmpeg, - waitUntilLivePublishedOnAllServers, - waitUntilLiveSavedOnAllServers, - checkLiveCleanupAfterSave -} diff --git a/shared/extra-utils/videos/playlists-command.ts b/shared/extra-utils/videos/playlists-command.ts deleted file mode 100644 index ce23900d3..000000000 --- a/shared/extra-utils/videos/playlists-command.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { omit } from 'lodash' -import { pick } from '@shared/core-utils' -import { - BooleanBothQuery, - HttpStatusCode, - ResultList, - VideoExistInPlaylist, - VideoPlaylist, - VideoPlaylistCreate, - VideoPlaylistCreateResult, - VideoPlaylistElement, - VideoPlaylistElementCreate, - VideoPlaylistElementCreateResult, - VideoPlaylistElementUpdate, - VideoPlaylistReorder, - VideoPlaylistType, - VideoPlaylistUpdate -} from '@shared/models' -import { unwrapBody } from '../requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class PlaylistsCommand extends AbstractCommand { - - list (options: OverrideCommandOptions & { - start?: number - count?: number - sort?: string - }) { - const path = '/api/v1/video-playlists' - const query = pick(options, [ 'start', 'count', 'sort' ]) - - return this.getRequestBody>({ - ...options, - - path, - query, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - listByChannel (options: OverrideCommandOptions & { - handle: string - start?: number - count?: number - sort?: string - }) { - const path = '/api/v1/video-channels/' + options.handle + '/video-playlists' - const query = pick(options, [ 'start', 'count', 'sort' ]) - - return this.getRequestBody>({ - ...options, - - path, - query, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - listByAccount (options: OverrideCommandOptions & { - handle: string - start?: number - count?: number - sort?: string - search?: string - playlistType?: VideoPlaylistType - }) { - const path = '/api/v1/accounts/' + options.handle + '/video-playlists' - const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ]) - - return this.getRequestBody>({ - ...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({ - ...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>({ - ...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({ - ...options, - - path, - query: { videoIds }, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } -} diff --git a/shared/extra-utils/videos/playlists.ts b/shared/extra-utils/videos/playlists.ts deleted file mode 100644 index 3dde52bb9..000000000 --- a/shared/extra-utils/videos/playlists.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { expect } from 'chai' -import { readdir } from 'fs-extra' -import { join } from 'path' -import { root } from '../miscs' - -async function checkPlaylistFilesWereRemoved ( - playlistUUID: string, - internalServerNumber: number, - directories = [ 'thumbnails' ] -) { - const testDirectory = 'test' + internalServerNumber - - for (const directory of directories) { - const directoryPath = join(root(), testDirectory, directory) - - const files = await readdir(directoryPath) - for (const file of files) { - expect(file).to.not.contain(playlistUUID) - } - } -} - -export { - checkPlaylistFilesWereRemoved -} diff --git a/shared/extra-utils/videos/services-command.ts b/shared/extra-utils/videos/services-command.ts deleted file mode 100644 index 06760df42..000000000 --- a/shared/extra-utils/videos/services-command.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { HttpStatusCode } from '@shared/models' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -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 - }) - } -} diff --git a/shared/extra-utils/videos/streaming-playlists-command.ts b/shared/extra-utils/videos/streaming-playlists-command.ts deleted file mode 100644 index 5d40d35cb..000000000 --- a/shared/extra-utils/videos/streaming-playlists-command.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { HttpStatusCode } from '@shared/models' -import { unwrapBody, unwrapTextOrDecode, unwrapBodyOrDecodeToJSON } from '../requests' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export class StreamingPlaylistsCommand extends AbstractCommand { - - get (options: OverrideCommandOptions & { - url: string - }) { - return unwrapTextOrDecode(this.getRawRequest({ - ...options, - - url: options.url, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - })) - } - - getSegment (options: OverrideCommandOptions & { - url: string - range?: string - }) { - return unwrapBody(this.getRawRequest({ - ...options, - - url: options.url, - range: options.range, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - })) - } - - getSegmentSha256 (options: OverrideCommandOptions & { - url: string - }) { - return unwrapBodyOrDecodeToJSON<{ [ id: string ]: string }>(this.getRawRequest({ - ...options, - - url: options.url, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - })) - } -} diff --git a/shared/extra-utils/videos/streaming-playlists.ts b/shared/extra-utils/videos/streaming-playlists.ts deleted file mode 100644 index 0451c0efe..000000000 --- a/shared/extra-utils/videos/streaming-playlists.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { expect } from 'chai' -import { basename } from 'path' -import { sha256 } from '@shared/core-utils/crypto' -import { removeFragmentedMP4Ext } from '@shared/core-utils' -import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models' -import { PeerTubeServer } from '../server' - -async function checkSegmentHash (options: { - server: PeerTubeServer - baseUrlPlaylist: string - baseUrlSegment: string - resolution: number - hlsPlaylist: VideoStreamingPlaylist -}) { - const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options - const command = server.streamingPlaylists - - const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) - const videoName = basename(file.fileUrl) - - const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` }) - - const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) - - const length = parseInt(matches[1], 10) - const offset = parseInt(matches[2], 10) - const range = `${offset}-${offset + length - 1}` - - const segmentBody = await command.getSegment({ - url: `${baseUrlSegment}/${videoName}`, - expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206, - range: `bytes=${range}` - }) - - const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) - expect(sha256(segmentBody)).to.equal(shaBody[videoName][range]) -} - -async function checkLiveSegmentHash (options: { - server: PeerTubeServer - baseUrlSegment: string - videoUUID: string - segmentName: string - hlsPlaylist: VideoStreamingPlaylist -}) { - const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options - const command = server.streamingPlaylists - - const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` }) - const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) - - expect(sha256(segmentBody)).to.equal(shaBody[segmentName]) -} - -async function checkResolutionsInMasterPlaylist (options: { - server: PeerTubeServer - playlistUrl: string - resolutions: number[] -}) { - const { server, playlistUrl, resolutions } = options - - const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl }) - - for (const resolution of resolutions) { - const reg = new RegExp( - '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"' - ) - - expect(masterPlaylist).to.match(reg) - } -} - -export { - checkSegmentHash, - checkLiveSegmentHash, - checkResolutionsInMasterPlaylist -} diff --git a/shared/extra-utils/videos/videos-command.ts b/shared/extra-utils/videos/videos-command.ts deleted file mode 100644 index 8ea828b40..000000000 --- a/shared/extra-utils/videos/videos-command.ts +++ /dev/null @@ -1,679 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ - -import { expect } from 'chai' -import { createReadStream, stat } from 'fs-extra' -import got, { Response as GotResponse } from 'got' -import { omit } from 'lodash' -import validator from 'validator' -import { buildUUID } from '@shared/core-utils/uuid' -import { pick } from '@shared/core-utils' -import { - HttpStatusCode, - ResultList, - UserVideoRateType, - Video, - VideoCreate, - VideoCreateResult, - VideoDetails, - VideoFileMetadata, - VideoPrivacy, - VideosCommonQuery, - VideoTranscodingCreate -} from '@shared/models' -import { buildAbsoluteFixturePath, wait } from '../miscs' -import { unwrapBody } from '../requests' -import { waitJobs } from '../server' -import { AbstractCommand, OverrideCommandOptions } from '../shared' - -export type VideoEdit = Partial> & { - 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 VideoPrivacy]: 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(this.getRawRequest({ - ...options, - - url: options.url, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - })) - } - - // --------------------------------------------------------------------------- - - view (options: OverrideCommandOptions & { - id: number | string - xForwardedFor?: string - }) { - const { id, xForwardedFor } = options - const path = '/api/v1/videos/' + id + '/views' - - return this.postBodyRequest({ - ...options, - - path, - xForwardedFor, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - - rate (options: OverrideCommandOptions & { - id: number | string - rating: UserVideoRateType - }) { - const { id, rating } = options - const path = '/api/v1/videos/' + id + '/rate' - - return this.putBodyRequest({ - ...options, - - path, - fields: { rating }, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - - // --------------------------------------------------------------------------- - - get (options: OverrideCommandOptions & { - id: number | string - }) { - const path = '/api/v1/videos/' + options.id - - return this.getRequestBody({ - ...options, - - path, - implicitToken: false, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - getWithToken (options: OverrideCommandOptions & { - id: number | string - }) { - return this.get({ - ...options, - - token: this.buildCommonRequestToken({ ...options, implicitToken: true }) - }) - } - - async getId (options: OverrideCommandOptions & { - uuid: number | string - }) { - const { uuid } = options - - if (validator.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>({ - ...options, - - path, - query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]), - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.OK_200 - }) - } - - // --------------------------------------------------------------------------- - - list (options: OverrideCommandOptions & VideosCommonQuery = {}) { - const path = '/api/v1/videos' - - const query = this.buildListQuery(options) - - return this.getRequestBody>({ - ...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 }) - }) - } - - listByAccount (options: OverrideCommandOptions & VideosCommonQuery & { - handle: string - }) { - const { handle, search } = options - const path = '/api/v1/accounts/' + handle + '/videos' - - return this.getRequestBody>({ - ...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>({ - ...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 - } = {}) { - const { mode = 'legacy' } = 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, attributes }) - - // Wait torrent generation - const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 }) - if (expectedStatus === HttpStatusCode.OK_200) { - 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 { - 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 & { - attributes: VideoEdit - }): Promise { - const { attributes, expectedStatus } = 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, 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, pathUploadId, videoFilePath, size }) - - if (result.statusCode === HttpStatusCode.OK_200) { - await this.endResumableUpload({ ...options, expectedStatus: HttpStatusCode.NO_CONTENT_204, 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 & { - attributes: VideoEdit - size: number - mimetype: string - - originalName?: string - lastModified?: number - }) { - const { attributes, originalName, lastModified, size, mimetype } = options - - const path = '/api/v1/videos/upload-resumable' - - return this.postUploadRequest({ - ...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 - }) - } - - sendResumableChunks (options: OverrideCommandOptions & { - pathUploadId: string - videoFilePath: string - size: number - contentLength?: number - contentRangeBuilder?: (start: number, chunk: any) => string - }) { - const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options - - const path = '/api/v1/videos/upload-resumable' - let start = 0 - - const token = this.buildCommonRequestToken({ ...options, implicitToken: true }) - const url = this.server.url - - const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 }) - return new Promise>((resolve, reject) => { - readable.on('data', async function onData (chunk) { - readable.pause() - - const headers = { - 'Authorization': 'Bearer ' + token, - 'Content-Type': 'application/octet-stream', - 'Content-Range': contentRangeBuilder - ? contentRangeBuilder(start, chunk) - : `bytes ${start}-${start + chunk.length - 1}/${size}`, - 'Content-Length': contentLength ? contentLength + '' : chunk.length + '' - } - - const res = await got<{ video: VideoCreateResult }>({ - url, - method: 'put', - headers, - path: path + '?' + pathUploadId, - body: chunk, - responseType: 'json', - throwHttpErrors: false - }) - - start += chunk.length - - if (res.statusCode === expectedStatus) { - return resolve(res) - } - - if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) { - readable.off('data', onData) - return reject(new Error('Incorrect transient behaviour sending intermediary chunks')) - } - - readable.resume() - }) - }) - } - - endResumableUpload (options: OverrideCommandOptions & { - pathUploadId: string - }) { - return this.deleteRequest({ - ...options, - - path: '/api/v1/videos/upload-resumable', - rawQuery: options.pathUploadId, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - - quickUpload (options: OverrideCommandOptions & { - name: string - nsfw?: boolean - privacy?: VideoPrivacy - fixture?: 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 - - 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 } - } - - // --------------------------------------------------------------------------- - - removeHLSFiles (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 - }) - } - - removeWebTorrentFiles (options: OverrideCommandOptions & { - videoId: number | string - }) { - const path = '/api/v1/videos/' + options.videoId + '/webtorrent' - - return this.deleteRequest({ - ...options, - - path, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - - runTranscoding (options: OverrideCommandOptions & { - videoId: number | string - transcodingType: 'hls' | 'webtorrent' - }) { - const path = '/api/v1/videos/' + options.videoId + '/transcoding' - - const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ]) - - return this.postBodyRequest({ - ...options, - - path, - fields, - implicitToken: true, - defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 - }) - } - - // --------------------------------------------------------------------------- - - private buildListQuery (options: VideosCommonQuery) { - return pick(options, [ - 'start', - 'count', - 'sort', - 'nsfw', - 'isLive', - 'categoryOneOf', - 'licenceOneOf', - 'languageOneOf', - '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 - } -} diff --git a/shared/extra-utils/videos/videos.ts b/shared/extra-utils/videos/videos.ts deleted file mode 100644 index 2c3464aa8..000000000 --- a/shared/extra-utils/videos/videos.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ - -import { expect } from 'chai' -import { pathExists, readdir } from 'fs-extra' -import { basename, join } from 'path' -import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models' -import { waitJobs } from '../server' -import { PeerTubeServer } from '../server/server' -import { VideoEdit } from './videos-command' - -async function checkVideoFilesWereRemoved (options: { - server: PeerTubeServer - video: VideoDetails - captions?: VideoCaption[] - onlyVideoFiles?: boolean // default false -}) { - const { video, server, captions = [], onlyVideoFiles = false } = options - - const webtorrentFiles = video.files || [] - const hlsFiles = video.streamingPlaylists[0]?.files || [] - - const thumbnailName = basename(video.thumbnailPath) - const previewName = basename(video.previewPath) - - const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl)) - - const captionNames = captions.map(c => basename(c.captionPath)) - - const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl)) - const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl)) - - let directories: { [ directory: string ]: string[] } = { - videos: webtorrentFilenames, - redundancy: webtorrentFilenames, - [join('playlists', 'hls')]: hlsFilenames, - [join('redundancy', 'hls')]: hlsFilenames - } - - if (onlyVideoFiles !== true) { - directories = { - ...directories, - - thumbnails: [ thumbnailName ], - previews: [ previewName ], - torrents: torrentNames, - captions: captionNames - } - } - - for (const directory of Object.keys(directories)) { - const directoryPath = server.servers.buildDirectory(directory) - - const directoryExists = await pathExists(directoryPath) - if (directoryExists === false) continue - - const existingFiles = await readdir(directoryPath) - for (const existingFile of existingFiles) { - for (const shouldNotExist of directories[directory]) { - expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist) - } - } - } -} - -async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) { - for (const server of servers) { - server.store.videoDetails = await server.videos.get({ id: uuid }) - } -} - -function checkUploadVideoParam ( - server: PeerTubeServer, - token: string, - attributes: Partial, - expectedStatus = HttpStatusCode.OK_200, - mode: 'legacy' | 'resumable' = 'legacy' -) { - return mode === 'legacy' - ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus }) - : server.videos.buildResumeUpload({ token, attributes, expectedStatus }) -} - -// serverNumber starts from 1 -async function uploadRandomVideoOnServers ( - servers: PeerTubeServer[], - serverNumber: number, - additionalParams?: VideoEdit & { prefixName?: string } -) { - const server = servers.find(s => s.serverNumber === serverNumber) - const res = await server.videos.randomUpload({ wait: false, additionalParams }) - - await waitJobs(servers) - - return res -} - -// --------------------------------------------------------------------------- - -export { - checkUploadVideoParam, - uploadRandomVideoOnServers, - checkVideoFilesWereRemoved, - saveVideoInServers -} diff --git a/shared/server-commands/bulk/bulk-command.ts b/shared/server-commands/bulk/bulk-command.ts new file mode 100644 index 000000000..b5c5673ce --- /dev/null +++ b/shared/server-commands/bulk/bulk-command.ts @@ -0,0 +1,20 @@ +import { BulkRemoveCommentsOfBody, HttpStatusCode } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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 + }) + } +} diff --git a/shared/server-commands/bulk/index.ts b/shared/server-commands/bulk/index.ts new file mode 100644 index 000000000..391597243 --- /dev/null +++ b/shared/server-commands/bulk/index.ts @@ -0,0 +1 @@ +export * from './bulk-command' diff --git a/shared/server-commands/cli/cli-command.ts b/shared/server-commands/cli/cli-command.ts new file mode 100644 index 000000000..ab9738174 --- /dev/null +++ b/shared/server-commands/cli/cli-command.ts @@ -0,0 +1,27 @@ +import { exec } from 'child_process' +import { AbstractCommand } from '../shared' + +export class CLICommand extends AbstractCommand { + + static exec (command: string) { + return new Promise((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}`) + } +} diff --git a/shared/server-commands/cli/index.ts b/shared/server-commands/cli/index.ts new file mode 100644 index 000000000..91b5abfbe --- /dev/null +++ b/shared/server-commands/cli/index.ts @@ -0,0 +1 @@ +export * from './cli-command' diff --git a/shared/server-commands/custom-pages/custom-pages-command.ts b/shared/server-commands/custom-pages/custom-pages-command.ts new file mode 100644 index 000000000..cd869a8de --- /dev/null +++ b/shared/server-commands/custom-pages/custom-pages-command.ts @@ -0,0 +1,33 @@ +import { CustomPage, HttpStatusCode } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class CustomPagesCommand extends AbstractCommand { + + getInstanceHomepage (options: OverrideCommandOptions = {}) { + const path = '/api/v1/custom-pages/homepage/instance' + + return this.getRequestBody({ + ...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 + }) + } +} diff --git a/shared/server-commands/custom-pages/index.ts b/shared/server-commands/custom-pages/index.ts new file mode 100644 index 000000000..58aed04f2 --- /dev/null +++ b/shared/server-commands/custom-pages/index.ts @@ -0,0 +1 @@ +export * from './custom-pages-command' diff --git a/shared/server-commands/feeds/feeds-command.ts b/shared/server-commands/feeds/feeds-command.ts new file mode 100644 index 000000000..3c95f9536 --- /dev/null +++ b/shared/server-commands/feeds/feeds-command.ts @@ -0,0 +1,44 @@ + +import { HttpStatusCode } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +type FeedType = 'videos' | 'video-comments' | 'subscriptions' + +export class FeedCommand extends AbstractCommand { + + getXML (options: OverrideCommandOptions & { + feed: FeedType + format?: string + }) { + const { feed, format } = options + const path = '/feeds/' + feed + '.xml' + + return this.getRequestText({ + ...options, + + path, + query: format ? { format } : undefined, + accept: 'application/xml', + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getJSON (options: OverrideCommandOptions & { + feed: FeedType + query?: { [ id: string ]: any } + }) { + const { feed, query } = options + const path = '/feeds/' + feed + '.json' + + return this.getRequestText({ + ...options, + + path, + query, + accept: 'application/json', + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/feeds/index.ts b/shared/server-commands/feeds/index.ts new file mode 100644 index 000000000..662a22b6f --- /dev/null +++ b/shared/server-commands/feeds/index.ts @@ -0,0 +1 @@ +export * from './feeds-command' diff --git a/shared/server-commands/index.ts b/shared/server-commands/index.ts new file mode 100644 index 000000000..4b3636d06 --- /dev/null +++ b/shared/server-commands/index.ts @@ -0,0 +1,15 @@ +export * from './bulk' +export * from './cli' +export * from './custom-pages' +export * from './feeds' +export * from './logs' +export * from './miscs' +export * from './mock-servers' +export * from './moderation' +export * from './overviews' +export * from './requests' +export * from './search' +export * from './server' +export * from './socket' +export * from './users' +export * from './videos' diff --git a/shared/server-commands/logs/index.ts b/shared/server-commands/logs/index.ts new file mode 100644 index 000000000..69452d7f0 --- /dev/null +++ b/shared/server-commands/logs/index.ts @@ -0,0 +1 @@ +export * from './logs-command' diff --git a/shared/server-commands/logs/logs-command.ts b/shared/server-commands/logs/logs-command.ts new file mode 100644 index 000000000..7b5c66c0c --- /dev/null +++ b/shared/server-commands/logs/logs-command.ts @@ -0,0 +1,44 @@ +import { HttpStatusCode } from '@shared/models' +import { LogLevel } from '../../models/server/log-level.type' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class LogsCommand extends AbstractCommand { + + getLogs (options: OverrideCommandOptions & { + startDate: Date + endDate?: Date + level?: LogLevel + tagsOneOf?: string[] + }) { + const { startDate, endDate, tagsOneOf, level } = options + const path = '/api/v1/server/logs' + + return this.getRequestBody({ + ...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 + }) + } + +} diff --git a/shared/server-commands/miscs/checks.ts b/shared/server-commands/miscs/checks.ts new file mode 100644 index 000000000..589928997 --- /dev/null +++ b/shared/server-commands/miscs/checks.ts @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ + +import { expect } from 'chai' +import { pathExists, readFile } from 'fs-extra' +import { join } from 'path' +import { root } from '@shared/core-utils' +import { HttpStatusCode } from '@shared/models' +import { makeGetRequest } from '../requests' +import { PeerTubeServer } from '../server' + +// Default interval -> 5 minutes +function dateIsValid (dateString: string, interval = 300000) { + const dateToCheck = new Date(dateString) + const now = new Date() + + return Math.abs(now.getTime() - dateToCheck.getTime()) <= interval +} + +function expectStartWith (str: string, start: string) { + expect(str.startsWith(start), `${str} does not start with ${start}`).to.be.true +} + +async function expectLogDoesNotContain (server: PeerTubeServer, str: string) { + const content = await server.servers.getLogContent() + + expect(content.toString()).to.not.contain(str) +} + +async function testImage (url: string, imageName: string, imagePath: string, extension = '.jpg') { + const res = await makeGetRequest({ + url, + path: imagePath, + expectedStatus: HttpStatusCode.OK_200 + }) + + const body = res.body + + const data = await readFile(join(root(), 'server', 'tests', 'fixtures', imageName + extension)) + const minLength = body.length - ((30 * body.length) / 100) + const maxLength = body.length + ((30 * body.length) / 100) + + expect(data.length).to.be.above(minLength, 'the generated image is way smaller than the recorded fixture') + expect(data.length).to.be.below(maxLength, 'the generated image is way larger than the recorded fixture') +} + +async function testFileExistsOrNot (server: PeerTubeServer, directory: string, filePath: string, exist: boolean) { + const base = server.servers.buildDirectory(directory) + + expect(await pathExists(join(base, filePath))).to.equal(exist) +} + +export { + dateIsValid, + testImage, + expectLogDoesNotContain, + testFileExistsOrNot, + expectStartWith +} diff --git a/shared/server-commands/miscs/generate.ts b/shared/server-commands/miscs/generate.ts new file mode 100644 index 000000000..93673a063 --- /dev/null +++ b/shared/server-commands/miscs/generate.ts @@ -0,0 +1,75 @@ +import { expect } from 'chai' +import ffmpeg from 'fluent-ffmpeg' +import { ensureDir, pathExists } from 'fs-extra' +import { dirname } from 'path' +import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '@shared/extra-utils/ffprobe' +import { getMaxBitrate } from '@shared/core-utils' +import { buildAbsoluteFixturePath } from './tests' + +async function ensureHasTooBigBitrate (fixturePath: string) { + const bitrate = await getVideoFileBitrate(fixturePath) + const dataResolution = await getVideoFileResolution(fixturePath) + const fps = await getVideoFileFPS(fixturePath) + + const maxBitrate = getMaxBitrate({ ...dataResolution, fps }) + expect(bitrate).to.be.above(maxBitrate) +} + +async function generateHighBitrateVideo () { + const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) + + await ensureDir(dirname(tempFixturePath)) + + const exists = await pathExists(tempFixturePath) + if (!exists) { + console.log('Generating high bitrate video.') + + // Generate a random, high bitrate video on the fly, so we don't have to include + // a large file in the repo. The video needs to have a certain minimum length so + // that FFmpeg properly applies bitrate limits. + // https://stackoverflow.com/a/15795112 + return new Promise((res, rej) => { + ffmpeg() + .outputOptions([ '-f rawvideo', '-video_size 1920x1080', '-i /dev/urandom' ]) + .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) + .outputOptions([ '-maxrate 10M', '-bufsize 10M' ]) + .output(tempFixturePath) + .on('error', rej) + .on('end', () => res(tempFixturePath)) + .run() + }) + } + + await ensureHasTooBigBitrate(tempFixturePath) + + return tempFixturePath +} + +async function generateVideoWithFramerate (fps = 60) { + const tempFixturePath = buildAbsoluteFixturePath(`video_${fps}fps.mp4`, true) + + await ensureDir(dirname(tempFixturePath)) + + const exists = await pathExists(tempFixturePath) + if (!exists) { + console.log('Generating video with framerate %d.', fps) + + return new Promise((res, rej) => { + ffmpeg() + .outputOptions([ '-f rawvideo', '-video_size 1280x720', '-i /dev/urandom' ]) + .outputOptions([ '-ac 2', '-f s16le', '-i /dev/urandom', '-t 10' ]) + .outputOptions([ `-r ${fps}` ]) + .output(tempFixturePath) + .on('error', rej) + .on('end', () => res(tempFixturePath)) + .run() + }) + } + + return tempFixturePath +} + +export { + generateHighBitrateVideo, + generateVideoWithFramerate +} diff --git a/shared/server-commands/miscs/index.ts b/shared/server-commands/miscs/index.ts new file mode 100644 index 000000000..4474661de --- /dev/null +++ b/shared/server-commands/miscs/index.ts @@ -0,0 +1,5 @@ +export * from './checks' +export * from './generate' +export * from './sql-command' +export * from './tests' +export * from './webtorrent' diff --git a/shared/server-commands/miscs/sql-command.ts b/shared/server-commands/miscs/sql-command.ts new file mode 100644 index 000000000..bedb3349b --- /dev/null +++ b/shared/server-commands/miscs/sql-command.ts @@ -0,0 +1,141 @@ +import { QueryTypes, Sequelize } from 'sequelize' +import { AbstractCommand } from '../shared/abstract-command' + +export class SQLCommand extends AbstractCommand { + private sequelize: Sequelize + + deleteAll (table: string) { + const seq = this.getSequelize() + + const options = { type: QueryTypes.DELETE } + + return seq.query(`DELETE FROM "${table}"`, options) + } + + async getCount (table: string) { + const seq = this.getSequelize() + + const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } + + const [ { total } ] = await seq.query<{ total: string }>(`SELECT COUNT(*) as total FROM "${table}"`, options) + if (total === null) return 0 + + return parseInt(total, 10) + } + + setActorField (to: string, field: string, value: string) { + const seq = this.getSequelize() + + const options = { type: QueryTypes.UPDATE } + + return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options) + } + + setVideoField (uuid: string, field: string, value: string) { + const seq = this.getSequelize() + + const options = { type: QueryTypes.UPDATE } + + return seq.query(`UPDATE video SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) + } + + setPlaylistField (uuid: string, field: string, value: string) { + const seq = this.getSequelize() + + const options = { type: QueryTypes.UPDATE } + + return seq.query(`UPDATE "videoPlaylist" SET "${field}" = '${value}' WHERE uuid = '${uuid}'`, options) + } + + async countVideoViewsOf (uuid: string) { + const seq = this.getSequelize() + + const query = 'SELECT SUM("videoView"."views") AS "total" FROM "videoView" ' + + `INNER JOIN "video" ON "video"."id" = "videoView"."videoId" WHERE "video"."uuid" = '${uuid}'` + + const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } + const [ { total } ] = await seq.query<{ total: number }>(query, options) + + if (!total) return 0 + + return parseInt(total + '', 10) + } + + getActorImage (filename: string) { + return this.selectQuery(`SELECT * FROM "actorImage" WHERE filename = '${filename}'`) + .then(rows => rows[0]) + } + + selectQuery (query: string) { + const seq = this.getSequelize() + const options = { type: QueryTypes.SELECT as QueryTypes.SELECT } + + return seq.query(query, options) + } + + updateQuery (query: string) { + const seq = this.getSequelize() + const options = { type: QueryTypes.UPDATE as QueryTypes.UPDATE } + + return seq.query(query, options) + } + + setPluginField (pluginName: string, field: string, value: string) { + const seq = this.getSequelize() + + const options = { type: QueryTypes.UPDATE } + + return seq.query(`UPDATE "plugin" SET "${field}" = '${value}' WHERE "name" = '${pluginName}'`, options) + } + + setPluginVersion (pluginName: string, newVersion: string) { + return this.setPluginField(pluginName, 'version', newVersion) + } + + setPluginLatestVersion (pluginName: string, newVersion: string) { + return this.setPluginField(pluginName, 'latestVersion', newVersion) + } + + setActorFollowScores (newScore: number) { + const seq = this.getSequelize() + + const options = { type: QueryTypes.UPDATE } + + return seq.query(`UPDATE "actorFollow" SET "score" = ${newScore}`, options) + } + + setTokenField (accessToken: string, field: string, value: string) { + const seq = this.getSequelize() + + const options = { type: QueryTypes.UPDATE } + + return seq.query(`UPDATE "oAuthToken" SET "${field}" = '${value}' WHERE "accessToken" = '${accessToken}'`, options) + } + + async cleanup () { + if (!this.sequelize) return + + await this.sequelize.close() + this.sequelize = undefined + } + + private getSequelize () { + if (this.sequelize) return this.sequelize + + const dbname = 'peertube_test' + this.server.internalServerNumber + const username = 'peertube' + const password = 'peertube' + const host = 'localhost' + const port = 5432 + + this.sequelize = new Sequelize(dbname, username, password, { + dialect: 'postgres', + host, + port, + logging: false + }) + + return this.sequelize + } + +} diff --git a/shared/server-commands/miscs/tests.ts b/shared/server-commands/miscs/tests.ts new file mode 100644 index 000000000..658fe5fd3 --- /dev/null +++ b/shared/server-commands/miscs/tests.ts @@ -0,0 +1,101 @@ +import { stat } from 'fs-extra' +import { basename, isAbsolute, join, resolve } from 'path' + +const FIXTURE_URLS = { + peertube_long: 'https://peertube2.cpy.re/videos/watch/122d093a-1ede-43bd-bd34-59d2931ffc5e', + peertube_short: 'https://peertube2.cpy.re/w/3fbif9S3WmtTP8gGsC5HBd', + + youtube: 'https://www.youtube.com/watch?v=msX3jv1XdvM', + + /** + * The video is used to check format-selection correctness wrt. HDR, + * which brings its own set of oddities outside of a MediaSource. + * + * The video needs to have the following format_ids: + * (which you can check by using `youtube-dl -F`): + * - (webm vp9) + * - (mp4 avc1) + * - (webm vp9.2 HDR) + */ + youtubeHDR: 'https://www.youtube.com/watch?v=RQgnBB9z_N4', + + // eslint-disable-next-line max-len + magnet: 'magnet:?xs=https%3A%2F%2Fpeertube2.cpy.re%2Flazy-static%2Ftorrents%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.torrent&xt=urn:btih:0f498834733e8057ed5c6f2ee2b4efd8d84a76ee&dn=super+peertube2+video&tr=https%3A%2F%2Fpeertube2.cpy.re%2Ftracker%2Fannounce&tr=wss%3A%2F%2Fpeertube2.cpy.re%3A443%2Ftracker%2Fsocket&ws=https%3A%2F%2Fpeertube2.cpy.re%2Fstatic%2Fwebseed%2Fb209ca00-c8bb-4b2b-b421-1ede169f3dbc-720.mp4', + + badVideo: 'https://download.cpy.re/peertube/bad_video.mp4', + goodVideo: 'https://download.cpy.re/peertube/good_video.mp4', + goodVideo720: 'https://download.cpy.re/peertube/good_video_720.mp4', + + file4K: 'https://download.cpy.re/peertube/4k_file.txt' +} + +function parallelTests () { + return process.env.MOCHA_PARALLEL === 'true' +} + +function isGithubCI () { + return !!process.env.GITHUB_WORKSPACE +} + +function areHttpImportTestsDisabled () { + const disabled = process.env.DISABLE_HTTP_IMPORT_TESTS === 'true' + + if (disabled) console.log('DISABLE_HTTP_IMPORT_TESTS env set to "true" so import tests are disabled') + + return disabled +} + +function areObjectStorageTestsDisabled () { + const disabled = process.env.ENABLE_OBJECT_STORAGE_TESTS !== 'true' + + if (disabled) console.log('ENABLE_OBJECT_STORAGE_TESTS env is not set to "true" so object storage tests are disabled') + + return disabled +} + +function buildAbsoluteFixturePath (path: string, customCIPath = false) { + if (isAbsolute(path)) return path + + if (customCIPath && process.env.GITHUB_WORKSPACE) { + return join(process.env.GITHUB_WORKSPACE, 'fixtures', path) + } + + return join(root(), 'server', 'tests', 'fixtures', path) +} + +function root () { + // We are in /miscs + let root = join(__dirname, '..', '..', '..') + + if (basename(root) === 'dist') root = resolve(root, '..') + + return root +} + +function wait (milliseconds: number) { + return new Promise(resolve => setTimeout(resolve, milliseconds)) +} + +async function getFileSize (path: string) { + const stats = await stat(path) + + return stats.size +} + +function buildRequestStub (): any { + return { } +} + +export { + FIXTURE_URLS, + + parallelTests, + isGithubCI, + areHttpImportTestsDisabled, + buildAbsoluteFixturePath, + getFileSize, + buildRequestStub, + areObjectStorageTestsDisabled, + wait, + root +} diff --git a/shared/server-commands/miscs/webtorrent.ts b/shared/server-commands/miscs/webtorrent.ts new file mode 100644 index 000000000..0683f8893 --- /dev/null +++ b/shared/server-commands/miscs/webtorrent.ts @@ -0,0 +1,46 @@ +import { readFile } from 'fs-extra' +import parseTorrent from 'parse-torrent' +import { basename, join } from 'path' +import * as WebTorrent from 'webtorrent' +import { VideoFile } from '@shared/models' +import { PeerTubeServer } from '../server' + +let webtorrent: WebTorrent.Instance + +function webtorrentAdd (torrentId: string, refreshWebTorrent = false) { + const WebTorrent = require('webtorrent') + + if (webtorrent && refreshWebTorrent) webtorrent.destroy() + if (!webtorrent || refreshWebTorrent) webtorrent = new WebTorrent() + + webtorrent.on('error', err => console.error('Error in webtorrent', err)) + + return new Promise(res => { + const torrent = webtorrent.add(torrentId, res) + + torrent.on('error', err => console.error('Error in webtorrent torrent', err)) + torrent.on('warning', warn => { + const msg = typeof warn === 'string' + ? warn + : warn.message + + if (msg.includes('Unsupported')) return + + console.error('Warning in webtorrent torrent', warn) + }) + }) +} + +async function parseTorrentVideo (server: PeerTubeServer, file: VideoFile) { + const torrentName = basename(file.torrentUrl) + const torrentPath = server.servers.buildDirectory(join('torrents', torrentName)) + + const data = await readFile(torrentPath) + + return parseTorrent(data) +} + +export { + webtorrentAdd, + parseTorrentVideo +} diff --git a/shared/server-commands/mock-servers/index.ts b/shared/server-commands/mock-servers/index.ts new file mode 100644 index 000000000..93c00c788 --- /dev/null +++ b/shared/server-commands/mock-servers/index.ts @@ -0,0 +1,5 @@ +export * from './mock-email' +export * from './mock-instances-index' +export * from './mock-joinpeertube-versions' +export * from './mock-plugin-blocklist' +export * from './mock-object-storage' diff --git a/shared/server-commands/mock-servers/mock-429.ts b/shared/server-commands/mock-servers/mock-429.ts new file mode 100644 index 000000000..9e0d1281a --- /dev/null +++ b/shared/server-commands/mock-servers/mock-429.ts @@ -0,0 +1,33 @@ +import express from 'express' +import { Server } from 'http' +import { getPort, randomListen, terminateServer } from './utils' + +export class Mock429 { + private server: Server + private responseSent = false + + async initialize () { + const app = express() + + app.get('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { + + if (!this.responseSent) { + this.responseSent = true + + // Retry after 5 seconds + res.header('retry-after', '2') + return res.sendStatus(429) + } + + return res.sendStatus(200) + }) + + this.server = await randomListen(app) + + return getPort(this.server) + } + + terminate () { + return terminateServer(this.server) + } +} diff --git a/shared/server-commands/mock-servers/mock-email.ts b/shared/server-commands/mock-servers/mock-email.ts new file mode 100644 index 000000000..f646c1621 --- /dev/null +++ b/shared/server-commands/mock-servers/mock-email.ts @@ -0,0 +1,63 @@ +import { ChildProcess } from 'child_process' +import MailDev from '@peertube/maildev' +import { randomInt } from '@shared/core-utils' +import { parallelTests } from '../miscs' + +class MockSmtpServer { + + private static instance: MockSmtpServer + private started = false + private emailChildProcess: ChildProcess + private emails: object[] + + private constructor () { } + + collectEmails (emailsCollection: object[]) { + return new Promise((res, rej) => { + const port = parallelTests() ? randomInt(1000, 2000) : 1025 + this.emails = emailsCollection + + if (this.started) { + return res(undefined) + } + + const maildev = new MailDev({ + ip: '127.0.0.1', + smtp: port, + disableWeb: true, + silent: true + }) + + maildev.on('new', email => { + this.emails.push(email) + }) + + maildev.listen(err => { + if (err) return rej(err) + + this.started = true + + return res(port) + }) + }) + } + + kill () { + if (!this.emailChildProcess) return + + process.kill(this.emailChildProcess.pid) + + this.emailChildProcess = null + MockSmtpServer.instance = null + } + + static get Instance () { + return this.instance || (this.instance = new this()) + } +} + +// --------------------------------------------------------------------------- + +export { + MockSmtpServer +} diff --git a/shared/server-commands/mock-servers/mock-instances-index.ts b/shared/server-commands/mock-servers/mock-instances-index.ts new file mode 100644 index 000000000..92b12d6f3 --- /dev/null +++ b/shared/server-commands/mock-servers/mock-instances-index.ts @@ -0,0 +1,46 @@ +import express from 'express' +import { Server } from 'http' +import { getPort, randomListen, terminateServer } from './utils' + +export class MockInstancesIndex { + private server: Server + + private readonly indexInstances: { host: string, createdAt: string }[] = [] + + async initialize () { + const app = express() + + app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url) + + return next() + }) + + app.get('/api/v1/instances/hosts', (req: express.Request, res: express.Response) => { + const since = req.query.since + + const filtered = this.indexInstances.filter(i => { + if (!since) return true + + return i.createdAt > since + }) + + return res.json({ + total: filtered.length, + data: filtered + }) + }) + + this.server = await randomListen(app) + + return getPort(this.server) + } + + addInstance (host: string) { + this.indexInstances.push({ host, createdAt: new Date().toISOString() }) + } + + terminate () { + return terminateServer(this.server) + } +} diff --git a/shared/server-commands/mock-servers/mock-joinpeertube-versions.ts b/shared/server-commands/mock-servers/mock-joinpeertube-versions.ts new file mode 100644 index 000000000..e7906ea56 --- /dev/null +++ b/shared/server-commands/mock-servers/mock-joinpeertube-versions.ts @@ -0,0 +1,34 @@ +import express from 'express' +import { Server } from 'http' +import { getPort, randomListen } from './utils' + +export class MockJoinPeerTubeVersions { + private server: Server + private latestVersion: string + + async initialize () { + const app = express() + + app.use('/', (req: express.Request, res: express.Response, next: express.NextFunction) => { + if (process.env.DEBUG) console.log('Receiving request on mocked server %s.', req.url) + + return next() + }) + + app.get('/versions.json', (req: express.Request, res: express.Response) => { + return res.json({ + peertube: { + latestVersion: this.latestVersion + } + }) + }) + + this.server = await randomListen(app) + + return getPort(this.server) + } + + setLatestVersion (latestVersion: string) { + this.latestVersion = latestVersion + } +} diff --git a/shared/server-commands/mock-servers/mock-object-storage.ts b/shared/server-commands/mock-servers/mock-object-storage.ts new file mode 100644 index 000000000..d135c2631 --- /dev/null +++ b/shared/server-commands/mock-servers/mock-object-storage.ts @@ -0,0 +1,41 @@ +import express from 'express' +import got, { RequestError } from 'got' +import { Server } from 'http' +import { pipeline } from 'stream' +import { ObjectStorageCommand } from '../server' +import { getPort, randomListen, terminateServer } from './utils' + +export class MockObjectStorage { + private server: Server + + async initialize () { + const app = express() + + app.get('/:bucketName/:path(*)', (req: express.Request, res: express.Response, next: express.NextFunction) => { + const url = `http://${req.params.bucketName}.${ObjectStorageCommand.getEndpointHost()}/${req.params.path}` + + if (process.env.DEBUG) { + console.log('Receiving request on mocked server %s.', req.url) + console.log('Proxifying request to %s', url) + } + + return pipeline( + got.stream(url, { throwHttpErrors: false }), + res, + (err: RequestError) => { + if (!err) return + + console.error('Pipeline failed.', err) + } + ) + }) + + this.server = await randomListen(app) + + return getPort(this.server) + } + + terminate () { + return terminateServer(this.server) + } +} diff --git a/shared/server-commands/mock-servers/mock-plugin-blocklist.ts b/shared/server-commands/mock-servers/mock-plugin-blocklist.ts new file mode 100644 index 000000000..f8a271cba --- /dev/null +++ b/shared/server-commands/mock-servers/mock-plugin-blocklist.ts @@ -0,0 +1,36 @@ +import express, { Request, Response } from 'express' +import { Server } from 'http' +import { getPort, randomListen, terminateServer } from './utils' + +type BlocklistResponse = { + data: { + value: string + action?: 'add' | 'remove' + updatedAt?: string + }[] +} + +export class MockBlocklist { + private body: BlocklistResponse + private server: Server + + async initialize () { + const app = express() + + app.get('/blocklist', (req: Request, res: Response) => { + return res.json(this.body) + }) + + this.server = await randomListen(app) + + return getPort(this.server) + } + + replace (body: BlocklistResponse) { + this.body = body + } + + terminate () { + return terminateServer(this.server) + } +} diff --git a/shared/server-commands/mock-servers/mock-proxy.ts b/shared/server-commands/mock-servers/mock-proxy.ts new file mode 100644 index 000000000..75ac79055 --- /dev/null +++ b/shared/server-commands/mock-servers/mock-proxy.ts @@ -0,0 +1,25 @@ + +import { createServer, Server } from 'http' +import proxy from 'proxy' +import { getPort, terminateServer } from './utils' + +class MockProxy { + private server: Server + + initialize () { + return new Promise(res => { + this.server = proxy(createServer()) + this.server.listen(0, () => res(getPort(this.server))) + }) + } + + terminate () { + return terminateServer(this.server) + } +} + +// --------------------------------------------------------------------------- + +export { + MockProxy +} diff --git a/shared/server-commands/mock-servers/utils.ts b/shared/server-commands/mock-servers/utils.ts new file mode 100644 index 000000000..235642439 --- /dev/null +++ b/shared/server-commands/mock-servers/utils.ts @@ -0,0 +1,33 @@ +import { Express } from 'express' +import { Server } from 'http' +import { AddressInfo } from 'net' + +function randomListen (app: Express) { + return new Promise(res => { + const server = app.listen(0, () => res(server)) + }) +} + +function getPort (server: Server) { + const address = server.address() as AddressInfo + + return address.port +} + +function terminateServer (server: Server) { + if (!server) return Promise.resolve() + + return new Promise((res, rej) => { + server.close(err => { + if (err) return rej(err) + + return res() + }) + }) +} + +export { + randomListen, + getPort, + terminateServer +} diff --git a/shared/server-commands/moderation/abuses-command.ts b/shared/server-commands/moderation/abuses-command.ts new file mode 100644 index 000000000..0db32ba46 --- /dev/null +++ b/shared/server-commands/moderation/abuses-command.ts @@ -0,0 +1,228 @@ +import { pick } from '@shared/core-utils' +import { + AbuseFilter, + AbuseMessage, + AbusePredefinedReasonsString, + AbuseState, + AbuseUpdate, + AbuseVideoIs, + AdminAbuse, + HttpStatusCode, + ResultList, + UserAbuse +} from '@shared/models' +import { unwrapBody } from '../requests/requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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?: AbuseState + 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>({ + ...options, + + path, + query, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getUserList (options: OverrideCommandOptions & { + start?: number + count?: number + sort?: string + + id?: number + search?: string + state?: AbuseState + }) { + 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>({ + ...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>({ + ...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 + }) + } + +} diff --git a/shared/server-commands/moderation/index.ts b/shared/server-commands/moderation/index.ts new file mode 100644 index 000000000..b37643956 --- /dev/null +++ b/shared/server-commands/moderation/index.ts @@ -0,0 +1 @@ +export * from './abuses-command' diff --git a/shared/server-commands/overviews/index.ts b/shared/server-commands/overviews/index.ts new file mode 100644 index 000000000..e19551907 --- /dev/null +++ b/shared/server-commands/overviews/index.ts @@ -0,0 +1 @@ +export * from './overviews-command' diff --git a/shared/server-commands/overviews/overviews-command.ts b/shared/server-commands/overviews/overviews-command.ts new file mode 100644 index 000000000..06b4892d2 --- /dev/null +++ b/shared/server-commands/overviews/overviews-command.ts @@ -0,0 +1,23 @@ +import { HttpStatusCode, VideosOverview } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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({ + ...options, + + path, + query, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/requests/check-api-params.ts b/shared/server-commands/requests/check-api-params.ts new file mode 100644 index 000000000..26ba1e913 --- /dev/null +++ b/shared/server-commands/requests/check-api-params.ts @@ -0,0 +1,48 @@ +import { HttpStatusCode } from '@shared/models' +import { makeGetRequest } from './requests' + +function checkBadStartPagination (url: string, path: string, token?: string, query = {}) { + return makeGetRequest({ + url, + path, + token, + query: { ...query, start: 'hello' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) +} + +async function checkBadCountPagination (url: string, path: string, token?: string, query = {}) { + await makeGetRequest({ + url, + path, + token, + query: { ...query, count: 'hello' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) + + await makeGetRequest({ + url, + path, + token, + query: { ...query, count: 2000 }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) +} + +function checkBadSortPagination (url: string, path: string, token?: string, query = {}) { + return makeGetRequest({ + url, + path, + token, + query: { ...query, sort: 'hello' }, + expectedStatus: HttpStatusCode.BAD_REQUEST_400 + }) +} + +// --------------------------------------------------------------------------- + +export { + checkBadStartPagination, + checkBadCountPagination, + checkBadSortPagination +} diff --git a/shared/server-commands/requests/index.ts b/shared/server-commands/requests/index.ts new file mode 100644 index 000000000..501163f92 --- /dev/null +++ b/shared/server-commands/requests/index.ts @@ -0,0 +1,3 @@ +// Don't include activitypub that import stuff from server +export * from './check-api-params' +export * from './requests' diff --git a/shared/server-commands/requests/requests.ts b/shared/server-commands/requests/requests.ts new file mode 100644 index 000000000..b6b9024ed --- /dev/null +++ b/shared/server-commands/requests/requests.ts @@ -0,0 +1,208 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ + +import { decode } from 'querystring' +import request from 'supertest' +import { URL } from 'url' +import { HttpStatusCode } from '@shared/models' +import { buildAbsoluteFixturePath } from '../miscs/tests' + +export type CommonRequestParams = { + url: string + path?: string + contentType?: string + range?: string + redirects?: number + accept?: string + host?: string + token?: string + headers?: { [ name: string ]: string } + type?: string + xForwardedFor?: string + expectedStatus?: HttpStatusCode +} + +function makeRawRequest (url: string, expectedStatus?: HttpStatusCode, range?: string) { + const { host, protocol, pathname } = new URL(url) + + return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, expectedStatus, range }) +} + +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 = HttpStatusCode.OK_200) { + return makeGetRequest({ + url, + path, + expectedStatus: 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?: HttpStatusCode +}) { + 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 (test: request.Test): Promise { + return test.then(res => res.body) +} + +function unwrapText (test: request.Test): Promise { + return test.then(res => res.text) +} + +function unwrapBodyOrDecodeToJSON (test: request.Test): Promise { + return test.then(res => { + if (res.body instanceof Buffer) { + return JSON.parse(new TextDecoder().decode(res.body)) + } + + return res.body + }) +} + +function unwrapTextOrDecode (test: request.Test): Promise { + 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.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.expectedStatus) req.expect(options.expectedStatus) + 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 +} + +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]) + } + } +} diff --git a/shared/server-commands/search/index.ts b/shared/server-commands/search/index.ts new file mode 100644 index 000000000..48dbe8ae9 --- /dev/null +++ b/shared/server-commands/search/index.ts @@ -0,0 +1 @@ +export * from './search-command' diff --git a/shared/server-commands/search/search-command.ts b/shared/server-commands/search/search-command.ts new file mode 100644 index 000000000..0fbbcd6ef --- /dev/null +++ b/shared/server-commands/search/search-command.ts @@ -0,0 +1,98 @@ +import { + HttpStatusCode, + ResultList, + Video, + VideoChannel, + VideoChannelsSearchQuery, + VideoPlaylist, + VideoPlaylistsSearchQuery, + VideosSearchQuery +} from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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>({ + ...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>({ + ...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: search, + sort: sort ?? '-publishedAt' + } + }) + } + + advancedVideoSearch (options: OverrideCommandOptions & { + search: VideosSearchQuery + }) { + const { search } = options + const path = '/api/v1/search/videos' + + return this.getRequestBody>({ + ...options, + + path, + query: search, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/server/config-command.ts b/shared/server-commands/server/config-command.ts new file mode 100644 index 000000000..89ae8eb4f --- /dev/null +++ b/shared/server-commands/server/config-command.ts @@ -0,0 +1,353 @@ +import { merge } from 'lodash' +import { DeepPartial } from '@shared/typescript-utils' +import { About, HttpStatusCode, ServerConfig } from '@shared/models' +import { CustomConfig } from '../../models/server/custom-config.model' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class ConfigCommand extends AbstractCommand { + + static getCustomConfigResolutions (enabled: boolean) { + return { + '144p': enabled, + '240p': enabled, + '360p': enabled, + '480p': enabled, + '720p': enabled, + '1080p': enabled, + '1440p': enabled, + '2160p': enabled + } + } + + enableImports () { + return this.updateExistingSubConfig({ + newConfig: { + import: { + videos: { + http: { + enabled: true + }, + + torrent: { + enabled: true + } + } + } + } + }) + } + + enableLive (options: { + allowReplay?: boolean + transcoding?: boolean + } = {}) { + return this.updateExistingSubConfig({ + newConfig: { + live: { + enabled: true, + allowReplay: options.allowReplay ?? true, + transcoding: { + enabled: options.transcoding ?? true, + resolutions: ConfigCommand.getCustomConfigResolutions(true) + } + } + } + }) + } + + disableTranscoding () { + return this.updateExistingSubConfig({ + newConfig: { + transcoding: { + enabled: false + } + } + }) + } + + enableTranscoding (webtorrent = true, hls = true) { + return this.updateExistingSubConfig({ + newConfig: { + transcoding: { + enabled: true, + resolutions: ConfigCommand.getCustomConfigResolutions(true), + + webtorrent: { + enabled: webtorrent + }, + hls: { + enabled: hls + } + } + } + }) + } + + getConfig (options: OverrideCommandOptions = {}) { + const path = '/api/v1/config' + + return this.getRequestBody({ + ...options, + + path, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getAbout (options: OverrideCommandOptions = {}) { + const path = '/api/v1/config/about' + + return this.getRequestBody({ + ...options, + + path, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getCustomConfig (options: OverrideCommandOptions = {}) { + const path = '/api/v1/config/custom' + + return this.getRequestBody({ + ...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 + }) { + const existing = await this.getCustomConfig(options) + + return this.updateCustomConfig({ ...options, newCustomConfig: merge({}, existing, options.newConfig) }) + } + + updateCustomSubConfig (options: OverrideCommandOptions & { + newConfig: DeepPartial + }) { + 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 + } + }, + signup: { + enabled: false, + limit: 5, + requiresEmailVerification: false, + minimumAge: 16 + }, + admin: { + email: 'superadmin1@example.com' + }, + contactForm: { + enabled: true + }, + user: { + videoQuota: 5242881, + videoQuotaDaily: 318742 + }, + videoChannels: { + maxPerUser: 20 + }, + transcoding: { + enabled: true, + 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 + }, + webtorrent: { + enabled: true + }, + hls: { + enabled: false + } + }, + live: { + enabled: true, + allowReplay: false, + maxDuration: -1, + maxInstanceLives: -1, + maxUserLives: 50, + transcoding: { + enabled: true, + threads: 4, + profile: 'default', + resolutions: { + '144p': true, + '240p': true, + '360p': true, + '480p': true, + '720p': true, + '1080p': true, + '1440p': true, + '2160p': true + } + } + }, + import: { + videos: { + concurrency: 3, + http: { + enabled: false + }, + torrent: { + enabled: false + } + } + }, + trending: { + videos: { + algorithms: { + enabled: [ 'best', '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 }) + } +} diff --git a/shared/server-commands/server/contact-form-command.ts b/shared/server-commands/server/contact-form-command.ts new file mode 100644 index 000000000..0e8fd6d84 --- /dev/null +++ b/shared/server-commands/server/contact-form-command.ts @@ -0,0 +1,31 @@ +import { HttpStatusCode } from '@shared/models' +import { ContactForm } from '../../models/server' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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 + }) + } +} diff --git a/shared/server-commands/server/debug-command.ts b/shared/server-commands/server/debug-command.ts new file mode 100644 index 000000000..3c5a785bb --- /dev/null +++ b/shared/server-commands/server/debug-command.ts @@ -0,0 +1,33 @@ +import { Debug, HttpStatusCode, SendDebugCommand } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class DebugCommand extends AbstractCommand { + + getDebug (options: OverrideCommandOptions = {}) { + const path = '/api/v1/server/debug' + + return this.getRequestBody({ + ...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 + }) + } +} diff --git a/shared/server-commands/server/directories.ts b/shared/server-commands/server/directories.ts new file mode 100644 index 000000000..e6f72d6fc --- /dev/null +++ b/shared/server-commands/server/directories.ts @@ -0,0 +1,34 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { pathExists, readdir } from 'fs-extra' +import { join } from 'path' +import { root } from '@shared/core-utils' +import { PeerTubeServer } from './server' + +async function checkTmpIsEmpty (server: PeerTubeServer) { + await checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css', 'hls', 'resumable-uploads' ]) + + if (await pathExists(join('test' + server.internalServerNumber, 'tmp', 'hls'))) { + await checkDirectoryIsEmpty(server, 'tmp/hls') + } +} + +async function checkDirectoryIsEmpty (server: PeerTubeServer, directory: string, exceptions: string[] = []) { + const testDirectory = 'test' + server.internalServerNumber + + const directoryPath = join(root(), testDirectory, directory) + + const directoryExists = await pathExists(directoryPath) + expect(directoryExists).to.be.true + + const files = await readdir(directoryPath) + const filtered = files.filter(f => exceptions.includes(f) === false) + + expect(filtered).to.have.lengthOf(0) +} + +export { + checkTmpIsEmpty, + checkDirectoryIsEmpty +} diff --git a/shared/server-commands/server/follows-command.ts b/shared/server-commands/server/follows-command.ts new file mode 100644 index 000000000..01ef6f179 --- /dev/null +++ b/shared/server-commands/server/follows-command.ts @@ -0,0 +1,139 @@ +import { pick } from '@shared/core-utils' +import { ActivityPubActorType, ActorFollow, FollowState, HttpStatusCode, ResultList, ServerFollowCreate } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' +import { PeerTubeServer } from './server' + +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>({ + ...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>({ + ...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 + }) + } +} diff --git a/shared/server-commands/server/follows.ts b/shared/server-commands/server/follows.ts new file mode 100644 index 000000000..698238f29 --- /dev/null +++ b/shared/server-commands/server/follows.ts @@ -0,0 +1,20 @@ +import { waitJobs } from './jobs' +import { PeerTubeServer } from './server' + +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 +} diff --git a/shared/server-commands/server/index.ts b/shared/server-commands/server/index.ts new file mode 100644 index 000000000..76a2099da --- /dev/null +++ b/shared/server-commands/server/index.ts @@ -0,0 +1,17 @@ +export * from './config-command' +export * from './contact-form-command' +export * from './debug-command' +export * from './directories' +export * from './follows-command' +export * from './follows' +export * from './jobs' +export * from './jobs-command' +export * from './object-storage-command' +export * from './plugins-command' +export * from './plugins' +export * from './redundancy-command' +export * from './server' +export * from './servers-command' +export * from './servers' +export * from './stats-command' +export * from './tracker' diff --git a/shared/server-commands/server/jobs-command.ts b/shared/server-commands/server/jobs-command.ts new file mode 100644 index 000000000..6636e7e4d --- /dev/null +++ b/shared/server-commands/server/jobs-command.ts @@ -0,0 +1,61 @@ +import { pick } from '@shared/core-utils' +import { HttpStatusCode } from '@shared/models' +import { Job, JobState, JobType, ResultList } from '../../models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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] + } + + 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>({ + ...options, + + path, + query, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + listFailed (options: OverrideCommandOptions & { + jobType?: JobType + }) { + const path = this.buildJobsUrl('failed') + + return this.getRequestBody>({ + ...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 + } +} diff --git a/shared/server-commands/server/jobs.ts b/shared/server-commands/server/jobs.ts new file mode 100644 index 000000000..34fefd444 --- /dev/null +++ b/shared/server-commands/server/jobs.ts @@ -0,0 +1,84 @@ + +import { expect } from 'chai' +import { JobState, JobType } from '../../models' +import { wait } from '../miscs' +import { PeerTubeServer } from './server' + +async function waitJobs (serversArg: PeerTubeServer[] | PeerTubeServer, skipDelayed = false) { + 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[] = [] + + // Check if each server has pending request + for (const server of servers) { + for (const state of states) { + const p = 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 + } + }) + + tasks.push(p) + } + + const p = server.debug.getDebug() + .then(obj => { + if (obj.activityPubMessagesWaiting !== 0) { + pendingRequests = true + } + }) + + tasks.push(p) + } + + 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 +} diff --git a/shared/server-commands/server/object-storage-command.ts b/shared/server-commands/server/object-storage-command.ts new file mode 100644 index 000000000..b4de8f4cb --- /dev/null +++ b/shared/server-commands/server/object-storage-command.ts @@ -0,0 +1,77 @@ + +import { HttpStatusCode } from '@shared/models' +import { makePostBodyRequest } from '../requests' +import { AbstractCommand } from '../shared' + +export class ObjectStorageCommand extends AbstractCommand { + static readonly DEFAULT_PLAYLIST_BUCKET = 'streaming-playlists' + static readonly DEFAULT_WEBTORRENT_BUCKET = 'videos' + + static getDefaultConfig () { + return { + object_storage: { + enabled: true, + endpoint: 'http://' + this.getEndpointHost(), + region: this.getRegion(), + + credentials: this.getCredentialsConfig(), + + streaming_playlists: { + bucket_name: this.DEFAULT_PLAYLIST_BUCKET + }, + + videos: { + bucket_name: this.DEFAULT_WEBTORRENT_BUCKET + } + } + } + } + + static getCredentialsConfig () { + return { + access_key_id: 'AKIAIOSFODNN7EXAMPLE', + secret_access_key: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' + } + } + + static getEndpointHost () { + return 'localhost:9444' + } + + static getRegion () { + return 'us-east-1' + } + + static getWebTorrentBaseUrl () { + return `http://${this.DEFAULT_WEBTORRENT_BUCKET}.${this.getEndpointHost()}/` + } + + static getPlaylistBaseUrl () { + return `http://${this.DEFAULT_PLAYLIST_BUCKET}.${this.getEndpointHost()}/` + } + + static async prepareDefaultBuckets () { + await this.createBucket(this.DEFAULT_PLAYLIST_BUCKET) + await this.createBucket(this.DEFAULT_WEBTORRENT_BUCKET) + } + + static async createBucket (name: string) { + await makePostBodyRequest({ + url: this.getEndpointHost(), + path: '/ui/' + name + '?delete', + expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 + }) + + await makePostBodyRequest({ + url: this.getEndpointHost(), + path: '/ui/' + name + '?create', + expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 + }) + + await makePostBodyRequest({ + url: this.getEndpointHost(), + path: '/ui/' + name + '?make-public', + expectedStatus: HttpStatusCode.TEMPORARY_REDIRECT_307 + }) + } +} diff --git a/shared/server-commands/server/plugins-command.ts b/shared/server-commands/server/plugins-command.ts new file mode 100644 index 000000000..1c44711da --- /dev/null +++ b/shared/server-commands/server/plugins-command.ts @@ -0,0 +1,257 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { readJSON, writeJSON } from 'fs-extra' +import { join } from 'path' +import { root } from '@shared/core-utils' +import { + HttpStatusCode, + PeerTubePlugin, + PeerTubePluginIndex, + PeertubePluginIndexList, + PluginPackageJson, + PluginTranslation, + PluginType, + PublicServerSetting, + RegisteredServerSettings, + ResultList +} from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class PluginsCommand extends AbstractCommand { + + static getPluginTestPath (suffix = '') { + return join(root(), 'server', 'tests', 'fixtures', 'peertube-plugin-test' + suffix) + } + + list (options: OverrideCommandOptions & { + start?: number + count?: number + sort?: string + pluginType?: PluginType + uninstalled?: boolean + }) { + const { start, count, sort, pluginType, uninstalled } = options + const path = '/api/v1/plugins' + + return this.getRequestBody>({ + ...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 + currentPeerTubeEngine?: string + search?: string + expectedStatus?: HttpStatusCode + }) { + 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>({ + ...options, + + path, + query, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + get (options: OverrideCommandOptions & { + npmName: string + }) { + const path = '/api/v1/plugins/' + options.npmName + + return this.getRequestBody({ + ...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({ + ...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({ + ...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({ + ...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 { + const path = this.getPackageJSONPath(npmName) + + return readJSON(path) + } + + private getPackageJSONPath (npmName: string) { + return this.server.servers.buildDirectory(join('plugins', 'node_modules', npmName, 'package.json')) + } +} diff --git a/shared/server-commands/server/plugins.ts b/shared/server-commands/server/plugins.ts new file mode 100644 index 000000000..c6316898d --- /dev/null +++ b/shared/server-commands/server/plugins.ts @@ -0,0 +1,18 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { PeerTubeServer } from './server' + +async function testHelloWorldRegisteredSettings (server: PeerTubeServer) { + const body = await server.plugins.getRegisteredSettings({ npmName: 'peertube-plugin-hello-world' }) + + const registeredSettings = body.registeredSettings + expect(registeredSettings).to.have.length.at.least(1) + + const adminNameSettings = registeredSettings.find(s => s.name === 'admin-name') + expect(adminNameSettings).to.not.be.undefined +} + +export { + testHelloWorldRegisteredSettings +} diff --git a/shared/server-commands/server/redundancy-command.ts b/shared/server-commands/server/redundancy-command.ts new file mode 100644 index 000000000..e7a8b3c29 --- /dev/null +++ b/shared/server-commands/server/redundancy-command.ts @@ -0,0 +1,80 @@ +import { HttpStatusCode, ResultList, VideoRedundanciesTarget, VideoRedundancy } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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>({ + ...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 + }) + } +} diff --git a/shared/server-commands/server/server.ts b/shared/server-commands/server/server.ts new file mode 100644 index 000000000..339b9cabb --- /dev/null +++ b/shared/server-commands/server/server.ts @@ -0,0 +1,392 @@ +import { ChildProcess, fork } from 'child_process' +import { copy } from 'fs-extra' +import { join } from 'path' +import { root, randomInt } from '@shared/core-utils' +import { Video, VideoChannel, VideoCreateResult, VideoDetails } from '../../models/videos' +import { BulkCommand } from '../bulk' +import { CLICommand } from '../cli' +import { CustomPagesCommand } from '../custom-pages' +import { FeedCommand } from '../feeds' +import { LogsCommand } from '../logs' +import { parallelTests, SQLCommand } from '../miscs' +import { AbusesCommand } from '../moderation' +import { OverviewsCommand } from '../overviews' +import { SearchCommand } from '../search' +import { SocketIOCommand } from '../socket' +import { AccountsCommand, BlocklistCommand, LoginCommand, NotificationsCommand, SubscriptionsCommand, UsersCommand } from '../users' +import { + BlacklistCommand, + CaptionsCommand, + ChangeOwnershipCommand, + ChannelsCommand, + HistoryCommand, + ImportsCommand, + LiveCommand, + PlaylistsCommand, + ServicesCommand, + StreamingPlaylistsCommand, + VideosCommand +} from '../videos' +import { CommentsCommand } from '../videos/comments-command' +import { ConfigCommand } from './config-command' +import { ContactFormCommand } from './contact-form-command' +import { DebugCommand } from './debug-command' +import { FollowsCommand } from './follows-command' +import { JobsCommand } from './jobs-command' +import { PluginsCommand } from './plugins-command' +import { RedundancyCommand } from './redundancy-command' +import { ServersCommand } from './servers-command' +import { StatsCommand } from './stats-command' +import { ObjectStorageCommand } from './object-storage-command' + +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 + + 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 + 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 + streamingPlaylists?: StreamingPlaylistsCommand + channels?: ChannelsCommand + comments?: CommentsCommand + sql?: SQLCommand + notifications?: NotificationsCommand + servers?: ServersCommand + login?: LoginCommand + users?: UsersCommand + objectStorage?: ObjectStorageCommand + videos?: VideosCommand + + 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://localhost:${this.port}` + this.host = `localhost:${this.port}` + this.hostname = 'localhost' + } + + setUrl (url: string) { + const parsed = new URL(url) + + this.url = url + this.host = parsed.host + this.hostname = parsed.hostname + this.port = parseInt(parsed.port) + } + + 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 = Object.create(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 forkOptions = { + silent: true, + env, + detached: true, + execArgv: options.nodeArgs || [] + } + + return new Promise((res, rej) => { + const self = this + let aggregatedLogs = '' + + this.app = fork(join(root(), 'dist', 'server.js'), options.peertubeArgs || [], forkOptions) + + const onPeerTubeExit = () => rej(new Error('Process exited:\n' + aggregatedLogs)) + const onParentExit = () => { + if (!this.app || !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() + }) + }) + } + + async kill () { + if (!this.app) return + + await this.sql.cleanup() + + process.kill(-this.app.pid) + + this.app = null + } + + private randomServer () { + const low = 10 + 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: `test${this.internalServerNumber}/tmp/`, + bin: `test${this.internalServerNumber}/bin/`, + avatars: `test${this.internalServerNumber}/avatars/`, + videos: `test${this.internalServerNumber}/videos/`, + streaming_playlists: `test${this.internalServerNumber}/streaming-playlists/`, + redundancy: `test${this.internalServerNumber}/redundancy/`, + logs: `test${this.internalServerNumber}/logs/`, + previews: `test${this.internalServerNumber}/previews/`, + thumbnails: `test${this.internalServerNumber}/thumbnails/`, + torrents: `test${this.internalServerNumber}/torrents/`, + captions: `test${this.internalServerNumber}/captions/`, + cache: `test${this.internalServerNumber}/cache/`, + plugins: `test${this.internalServerNumber}/plugins/` + }, + 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.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.streamingPlaylists = new StreamingPlaylistsCommand(this) + this.channels = new ChannelsCommand(this) + this.comments = new CommentsCommand(this) + this.sql = new SQLCommand(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.objectStorage = new ObjectStorageCommand(this) + } +} diff --git a/shared/server-commands/server/servers-command.ts b/shared/server-commands/server/servers-command.ts new file mode 100644 index 000000000..47420c95f --- /dev/null +++ b/shared/server-commands/server/servers-command.ts @@ -0,0 +1,92 @@ +import { exec } from 'child_process' +import { copy, ensureDir, readFile, remove } from 'fs-extra' +import { basename, join } from 'path' +import { root } from '@shared/core-utils' +import { HttpStatusCode } from '@shared/models' +import { getFileSize, isGithubCI, wait } from '../miscs' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class ServersCommand extends AbstractCommand { + + static flushTests (internalServerNumber: number) { + return new Promise((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 + }) + } + + async cleanupTests () { + const p: Promise[] = [] + + if (isGithubCI()) { + 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) { + p.push(ServersCommand.flushTests(this.server.internalServerNumber)) + } + + if (this.server.customConfigFile) { + p.push(remove(this.server.customConfigFile)) + } + + return p + } + + 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) + } + + buildWebTorrentFilePath (fileUrl: string) { + return this.buildDirectory(join('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) + } +} diff --git a/shared/server-commands/server/servers.ts b/shared/server-commands/server/servers.ts new file mode 100644 index 000000000..21ab9405b --- /dev/null +++ b/shared/server-commands/server/servers.ts @@ -0,0 +1,49 @@ +import { ensureDir } from 'fs-extra' +import { isGithubCI } from '../miscs' +import { PeerTubeServer, RunServerOptions } from './server' + +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[] = [] + + for (let i = 1; i <= totalServers; i++) { + serverPromises.push(createSingleServer(i, configOverride, options)) + } + + return Promise.all(serverPromises) +} + +async 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[] = [] + for (const server of servers) { + p = p.concat(server.servers.cleanupTests()) + } + + return Promise.all(p) +} + +// --------------------------------------------------------------------------- + +export { + createSingleServer, + createMultipleServers, + cleanupTests, + killallServers +} diff --git a/shared/server-commands/server/stats-command.ts b/shared/server-commands/server/stats-command.ts new file mode 100644 index 000000000..64a452306 --- /dev/null +++ b/shared/server-commands/server/stats-command.ts @@ -0,0 +1,25 @@ +import { HttpStatusCode, ServerStats } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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({ + ...options, + + path, + query, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/server/tracker.ts b/shared/server-commands/server/tracker.ts new file mode 100644 index 000000000..ed43a5924 --- /dev/null +++ b/shared/server-commands/server/tracker.ts @@ -0,0 +1,27 @@ +import { expect } from 'chai' +import { sha1 } from '@shared/core-utils/crypto' +import { makeGetRequest } from '../requests' + +async function hlsInfohashExist (serverUrl: string, masterPlaylistUrl: string, fileNumber: number) { + const path = '/tracker/announce' + + const infohash = sha1(`2${masterPlaylistUrl}+V${fileNumber}`) + + // From bittorrent-tracker + const infohashBinary = escape(Buffer.from(infohash, 'hex').toString('binary')).replace(/[@*/+]/g, function (char) { + return '%' + char.charCodeAt(0).toString(16).toUpperCase() + }) + + const res = await makeGetRequest({ + url: serverUrl, + path, + rawQuery: `peer_id=-WW0105-NkvYO/egUAr4&info_hash=${infohashBinary}&port=42100`, + expectedStatus: 200 + }) + + expect(res.text).to.not.contain('failure') +} + +export { + hlsInfohashExist +} diff --git a/shared/server-commands/shared/abstract-command.ts b/shared/server-commands/shared/abstract-command.ts new file mode 100644 index 000000000..a57c857fc --- /dev/null +++ b/shared/server-commands/shared/abstract-command.ts @@ -0,0 +1,211 @@ +import { isAbsolute, join } from 'path' +import { root } from '../miscs/tests' +import { + makeDeleteRequest, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + makeUploadRequest, + unwrapBody, + unwrapText +} from '../requests/requests' +import { PeerTubeServer } from '../server/server' + +export interface OverrideCommandOptions { + token?: string + expectedStatus?: number +} + +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: number + + // Common optional request parameters + contentType?: string + accept?: string + redirects?: number + range?: string + host?: string + headers?: { [ name: string ]: string } + requestType?: 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 (options: InternalGetCommandOptions) { + return unwrapBody(this.getRequest(options)) + } + + protected getRequestText (options: InternalGetCommandOptions) { + return unwrapText(this.getRequest(options)) + } + + protected getRawRequest (options: Omit) { + 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 } + }) { + const { fields } = options + + return makePutBodyRequest({ + ...this.buildCommonRequestOptions(options), + + fields + }) + } + + protected postBodyRequest (options: InternalCommonCommandOptions & { + fields?: { [ fieldName: string ]: any } + }) { + const { fields } = options + + return makePostBodyRequest({ + ...this.buildCommonRequestOptions(options), + + fields + }) + } + + 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 + : join(root(), 'server', 'tests', 'fixtures', 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 } = options + + return { + url: url ?? this.server.url, + path, + + token: this.buildCommonRequestToken(options), + expectedStatus: this.buildExpectedStatus(options), + + redirects, + contentType, + range, + host, + accept, + headers, + type: requestType, + xForwardedFor + } + } + + protected buildCommonRequestToken (options: Pick) { + const { token } = options + + const fallbackToken = options.implicitToken + ? this.server.accessToken + : undefined + + return token !== undefined ? token : fallbackToken + } + + protected buildExpectedStatus (options: Pick) { + const { expectedStatus, defaultExpectedStatus } = options + + return expectedStatus !== undefined ? expectedStatus : defaultExpectedStatus + } +} + +export { + AbstractCommand +} diff --git a/shared/server-commands/shared/index.ts b/shared/server-commands/shared/index.ts new file mode 100644 index 000000000..e807ab4f7 --- /dev/null +++ b/shared/server-commands/shared/index.ts @@ -0,0 +1 @@ +export * from './abstract-command' diff --git a/shared/server-commands/socket/index.ts b/shared/server-commands/socket/index.ts new file mode 100644 index 000000000..594329b2f --- /dev/null +++ b/shared/server-commands/socket/index.ts @@ -0,0 +1 @@ +export * from './socket-io-command' diff --git a/shared/server-commands/socket/socket-io-command.ts b/shared/server-commands/socket/socket-io-command.ts new file mode 100644 index 000000000..c277ead28 --- /dev/null +++ b/shared/server-commands/socket/socket-io-command.ts @@ -0,0 +1,15 @@ +import { io } from 'socket.io-client' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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') + } +} diff --git a/shared/server-commands/users/accounts-command.ts b/shared/server-commands/users/accounts-command.ts new file mode 100644 index 000000000..98d9d5927 --- /dev/null +++ b/shared/server-commands/users/accounts-command.ts @@ -0,0 +1,78 @@ +import { HttpStatusCode, ResultList } from '@shared/models' +import { Account, ActorFollow } from '../../models/actors' +import { AccountVideoRate, VideoRateType } from '../../models/videos' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class AccountsCommand extends AbstractCommand { + + list (options: OverrideCommandOptions & { + sort?: string // default -createdAt + } = {}) { + const { sort = '-createdAt' } = options + const path = '/api/v1/accounts' + + return this.getRequestBody>({ + ...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({ + ...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>({ + ...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>({ + ...options, + + path, + query, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/users/actors.ts b/shared/server-commands/users/actors.ts new file mode 100644 index 000000000..12c3e078a --- /dev/null +++ b/shared/server-commands/users/actors.ts @@ -0,0 +1,73 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { pathExists, readdir } from 'fs-extra' +import { join } from 'path' +import { root } from '@shared/core-utils' +import { Account, VideoChannel } from '@shared/models' +import { PeerTubeServer } from '../server' + +async function expectChannelsFollows (options: { + server: PeerTubeServer + handle: string + followers: number + following: number +}) { + const { server } = options + const { data } = await server.channels.list() + + return expectActorFollow({ ...options, data }) +} + +async function expectAccountFollows (options: { + server: PeerTubeServer + handle: string + followers: number + following: number +}) { + const { server } = options + const { data } = await server.accounts.list() + + return expectActorFollow({ ...options, data }) +} + +async function checkActorFilesWereRemoved (filename: string, serverNumber: number) { + const testDirectory = 'test' + serverNumber + + for (const directory of [ 'avatars' ]) { + const directoryPath = join(root(), testDirectory, directory) + + const directoryExists = await pathExists(directoryPath) + expect(directoryExists).to.be.true + + const files = await readdir(directoryPath) + for (const file of files) { + expect(file).to.not.contain(filename) + } + } +} + +export { + expectAccountFollows, + expectChannelsFollows, + checkActorFilesWereRemoved +} + +// --------------------------------------------------------------------------- + +function expectActorFollow (options: { + server: PeerTubeServer + data: (Account | VideoChannel)[] + handle: string + followers: number + following: number +}) { + const { server, data, handle, followers, following } = options + + const actor = data.find(a => a.name + '@' + a.host === handle) + const message = `${handle} on ${server.url}` + + expect(actor, message).to.exist + expect(actor.followersCount).to.equal(followers, message) + expect(actor.followingCount).to.equal(following, message) +} diff --git a/shared/server-commands/users/blocklist-command.ts b/shared/server-commands/users/blocklist-command.ts new file mode 100644 index 000000000..2e7ed074d --- /dev/null +++ b/shared/server-commands/users/blocklist-command.ts @@ -0,0 +1,162 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { AccountBlock, BlockStatus, HttpStatusCode, ResultList, ServerBlock } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +type ListBlocklistOptions = OverrideCommandOptions & { + start: number + count: number + sort: string // default -createdAt +} + +export class BlocklistCommand extends AbstractCommand { + + listMyAccountBlocklist (options: ListBlocklistOptions) { + const path = '/api/v1/users/me/blocklist/accounts' + + return this.listBlocklist(options, path) + } + + listMyServerBlocklist (options: ListBlocklistOptions) { + const path = '/api/v1/users/me/blocklist/servers' + + return this.listBlocklist(options, path) + } + + listServerAccountBlocklist (options: ListBlocklistOptions) { + const path = '/api/v1/server/blocklist/accounts' + + return this.listBlocklist(options, path) + } + + listServerServerBlocklist (options: ListBlocklistOptions) { + const path = '/api/v1/server/blocklist/servers' + + return this.listBlocklist(options, path) + } + + // --------------------------------------------------------------------------- + + getStatus (options: OverrideCommandOptions & { + accounts?: string[] + hosts?: string[] + }) { + const { accounts, hosts } = options + + const path = '/api/v1/blocklist/status' + + return this.getRequestBody({ + ...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 (options: ListBlocklistOptions, path: string) { + const { start, count, sort = '-createdAt' } = options + + return this.getRequestBody>({ + ...options, + + path, + query: { start, count, sort }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + +} diff --git a/shared/server-commands/users/index.ts b/shared/server-commands/users/index.ts new file mode 100644 index 000000000..460a06f70 --- /dev/null +++ b/shared/server-commands/users/index.ts @@ -0,0 +1,9 @@ +export * from './accounts-command' +export * from './actors' +export * from './blocklist-command' +export * from './login' +export * from './login-command' +export * from './notifications' +export * from './notifications-command' +export * from './subscriptions-command' +export * from './users-command' diff --git a/shared/server-commands/users/login-command.ts b/shared/server-commands/users/login-command.ts new file mode 100644 index 000000000..143f72a59 --- /dev/null +++ b/shared/server-commands/users/login-command.ts @@ -0,0 +1,132 @@ +import { HttpStatusCode, PeerTubeProblemDocument } from '@shared/models' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class LoginCommand extends AbstractCommand { + + login (options: OverrideCommandOptions & { + client?: { id?: string, secret?: string } + user?: { username: string, password?: string } + } = {}) { + const { client = this.server.store.client, user = this.server.store.user } = 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' + } + + return unwrapBody<{ access_token: string, refresh_token: string } & PeerTubeProblemDocument>(this.postBodyRequest({ + ...options, + + path, + requestType: 'form', + fields: body, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + getAccessToken (arg1?: { username: string, password?: string }): Promise + getAccessToken (arg1: string, password?: string): Promise + 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: 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 + }) + } +} diff --git a/shared/server-commands/users/login.ts b/shared/server-commands/users/login.ts new file mode 100644 index 000000000..f1df027d3 --- /dev/null +++ b/shared/server-commands/users/login.ts @@ -0,0 +1,19 @@ +import { PeerTubeServer } from '../server/server' + +function setAccessTokensToServers (servers: PeerTubeServer[]) { + const tasks: Promise[] = [] + + for (const server of servers) { + const p = server.login.getAccessToken() + .then(t => { server.accessToken = t }) + tasks.push(p) + } + + return Promise.all(tasks) +} + +// --------------------------------------------------------------------------- + +export { + setAccessTokensToServers +} diff --git a/shared/server-commands/users/notifications-command.ts b/shared/server-commands/users/notifications-command.ts new file mode 100644 index 000000000..692420b8b --- /dev/null +++ b/shared/server-commands/users/notifications-command.ts @@ -0,0 +1,86 @@ +import { HttpStatusCode, ResultList } from '@shared/models' +import { UserNotification, UserNotificationSetting } from '../../models/users' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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>({ + ...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] + } +} diff --git a/shared/server-commands/users/notifications.ts b/shared/server-commands/users/notifications.ts new file mode 100644 index 000000000..07ccb0f8d --- /dev/null +++ b/shared/server-commands/users/notifications.ts @@ -0,0 +1,795 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import { inspect } from 'util' +import { AbuseState, PluginType } from '@shared/models' +import { UserNotification, UserNotificationSetting, UserNotificationSettingValue, UserNotificationType } from '../../models/users' +import { MockSmtpServer } from '../mock-servers/mock-email' +import { PeerTubeServer } from '../server' +import { doubleFollow } from '../server/follows' +import { createMultipleServers } from '../server/servers' +import { setAccessTokensToServers } from './login' + +type CheckerBaseParams = { + server: PeerTubeServer + emails: any[] + socketNotifications: UserNotification[] + token: string + check?: { web: boolean, mail: boolean } +} + +type CheckerType = 'presence' | 'absence' + +function getAllNotificationsSettings (): UserNotificationSetting { + return { + newVideoFromSubscription: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + newCommentOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + abuseAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + videoAutoBlacklistAsModerator: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + blacklistOnMyVideo: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + myVideoImportFinished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + myVideoPublished: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + commentMention: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + newFollow: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + newUserRegistration: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + newInstanceFollower: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + abuseNewMessage: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + abuseStateChange: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + autoInstanceFollowing: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + newPeerTubeVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL, + newPluginVersion: UserNotificationSettingValue.WEB | UserNotificationSettingValue.EMAIL + } +} + +async function checkNewVideoFromSubscription (options: CheckerBaseParams & { + videoName: string + shortUUID: string + checkType: CheckerType +}) { + const { videoName, shortUUID } = options + const notificationType = UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkVideo(notification.video, videoName, shortUUID) + checkActor(notification.video.channel) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.type !== UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION || n.video.name !== videoName + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + return text.indexOf(shortUUID) !== -1 && text.indexOf('Your subscription') !== -1 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkVideoIsPublished (options: CheckerBaseParams & { + videoName: string + shortUUID: string + checkType: CheckerType +}) { + const { videoName, shortUUID } = options + const notificationType = UserNotificationType.MY_VIDEO_PUBLISHED + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkVideo(notification.video, videoName, shortUUID) + checkActor(notification.video.channel) + } else { + expect(notification.video).to.satisfy(v => v === undefined || v.name !== videoName) + } + } + + function emailNotificationFinder (email: object) { + const text: string = email['text'] + return text.includes(shortUUID) && text.includes('Your video') + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkMyVideoImportIsFinished (options: CheckerBaseParams & { + videoName: string + shortUUID: string + url: string + success: boolean + checkType: CheckerType +}) { + const { videoName, shortUUID, url, success } = options + + const notificationType = success ? UserNotificationType.MY_VIDEO_IMPORT_SUCCESS : UserNotificationType.MY_VIDEO_IMPORT_ERROR + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.videoImport.targetUrl).to.equal(url) + + if (success) checkVideo(notification.videoImport.video, videoName, shortUUID) + } else { + expect(notification.videoImport).to.satisfy(i => i === undefined || i.targetUrl !== url) + } + } + + function emailNotificationFinder (email: object) { + const text: string = email['text'] + const toFind = success ? ' finished' : ' error' + + return text.includes(url) && text.includes(toFind) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkUserRegistered (options: CheckerBaseParams & { + username: string + checkType: CheckerType +}) { + const { username } = options + const notificationType = UserNotificationType.NEW_USER_REGISTRATION + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkActor(notification.account) + expect(notification.account.name).to.equal(username) + } else { + expect(notification).to.satisfy(n => n.type !== notificationType || n.account.name !== username) + } + } + + function emailNotificationFinder (email: object) { + const text: string = email['text'] + + return text.includes(' registered.') && text.includes(username) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkNewActorFollow (options: CheckerBaseParams & { + followType: 'channel' | 'account' + followerName: string + followerDisplayName: string + followingDisplayName: string + checkType: CheckerType +}) { + const { followType, followerName, followerDisplayName, followingDisplayName } = options + const notificationType = UserNotificationType.NEW_FOLLOW + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkActor(notification.actorFollow.follower) + expect(notification.actorFollow.follower.displayName).to.equal(followerDisplayName) + expect(notification.actorFollow.follower.name).to.equal(followerName) + expect(notification.actorFollow.follower.host).to.not.be.undefined + + const following = notification.actorFollow.following + expect(following.displayName).to.equal(followingDisplayName) + expect(following.type).to.equal(followType) + } else { + expect(notification).to.satisfy(n => { + return n.type !== notificationType || + (n.actorFollow.follower.name !== followerName && n.actorFollow.following !== followingDisplayName) + }) + } + } + + function emailNotificationFinder (email: object) { + const text: string = email['text'] + + return text.includes(followType) && text.includes(followingDisplayName) && text.includes(followerDisplayName) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkNewInstanceFollower (options: CheckerBaseParams & { + followerHost: string + checkType: CheckerType +}) { + const { followerHost } = options + const notificationType = UserNotificationType.NEW_INSTANCE_FOLLOWER + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkActor(notification.actorFollow.follower) + expect(notification.actorFollow.follower.name).to.equal('peertube') + expect(notification.actorFollow.follower.host).to.equal(followerHost) + + expect(notification.actorFollow.following.name).to.equal('peertube') + } else { + expect(notification).to.satisfy(n => { + return n.type !== notificationType || n.actorFollow.follower.host !== followerHost + }) + } + } + + function emailNotificationFinder (email: object) { + const text: string = email['text'] + + return text.includes('instance has a new follower') && text.includes(followerHost) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkAutoInstanceFollowing (options: CheckerBaseParams & { + followerHost: string + followingHost: string + checkType: CheckerType +}) { + const { followerHost, followingHost } = options + const notificationType = UserNotificationType.AUTO_INSTANCE_FOLLOWING + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + const following = notification.actorFollow.following + checkActor(following) + expect(following.name).to.equal('peertube') + expect(following.host).to.equal(followingHost) + + expect(notification.actorFollow.follower.name).to.equal('peertube') + expect(notification.actorFollow.follower.host).to.equal(followerHost) + } else { + expect(notification).to.satisfy(n => { + return n.type !== notificationType || n.actorFollow.following.host !== followingHost + }) + } + } + + function emailNotificationFinder (email: object) { + const text: string = email['text'] + + return text.includes(' automatically followed a new instance') && text.includes(followingHost) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkCommentMention (options: CheckerBaseParams & { + shortUUID: string + commentId: number + threadId: number + byAccountDisplayName: string + checkType: CheckerType +}) { + const { shortUUID, commentId, threadId, byAccountDisplayName } = options + const notificationType = UserNotificationType.COMMENT_MENTION + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkComment(notification.comment, commentId, threadId) + checkActor(notification.comment.account) + expect(notification.comment.account.displayName).to.equal(byAccountDisplayName) + + checkVideo(notification.comment.video, undefined, shortUUID) + } else { + expect(notification).to.satisfy(n => n.type !== notificationType || n.comment.id !== commentId) + } + } + + function emailNotificationFinder (email: object) { + const text: string = email['text'] + + return text.includes(' mentioned ') && text.includes(shortUUID) && text.includes(byAccountDisplayName) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +let lastEmailCount = 0 + +async function checkNewCommentOnMyVideo (options: CheckerBaseParams & { + shortUUID: string + commentId: number + threadId: number + checkType: CheckerType +}) { + const { server, shortUUID, commentId, threadId, checkType, emails } = options + const notificationType = UserNotificationType.NEW_COMMENT_ON_MY_VIDEO + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + checkComment(notification.comment, commentId, threadId) + checkActor(notification.comment.account) + checkVideo(notification.comment.video, undefined, shortUUID) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.comment === undefined || n.comment.id !== commentId + }) + } + } + + const commentUrl = `http://localhost:${server.port}/w/${shortUUID};threadId=${threadId}` + + function emailNotificationFinder (email: object) { + return email['text'].indexOf(commentUrl) !== -1 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) + + if (checkType === 'presence') { + // We cannot detect email duplicates, so check we received another email + expect(emails).to.have.length.above(lastEmailCount) + lastEmailCount = emails.length + } +} + +async function checkNewVideoAbuseForModerators (options: CheckerBaseParams & { + shortUUID: string + videoName: string + checkType: CheckerType +}) { + const { shortUUID, videoName } = options + const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.abuse.id).to.be.a('number') + checkVideo(notification.abuse.video, videoName, shortUUID) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.abuse === undefined || n.abuse.video.shortUUID !== shortUUID + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkNewAbuseMessage (options: CheckerBaseParams & { + abuseId: number + message: string + toEmail: string + checkType: CheckerType +}) { + const { abuseId, message, toEmail } = options + const notificationType = UserNotificationType.ABUSE_NEW_MESSAGE + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.abuse.id).to.equal(abuseId) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.type !== notificationType || n.abuse === undefined || n.abuse.id !== abuseId + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + const to = email['to'].filter(t => t.address === toEmail) + + return text.indexOf(message) !== -1 && to.length !== 0 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkAbuseStateChange (options: CheckerBaseParams & { + abuseId: number + state: AbuseState + checkType: CheckerType +}) { + const { abuseId, state } = options + const notificationType = UserNotificationType.ABUSE_STATE_CHANGE + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.abuse.id).to.equal(abuseId) + expect(notification.abuse.state).to.equal(state) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.abuse === undefined || n.abuse.id !== abuseId + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + + const contains = state === AbuseState.ACCEPTED + ? ' accepted' + : ' rejected' + + return text.indexOf(contains) !== -1 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkNewCommentAbuseForModerators (options: CheckerBaseParams & { + shortUUID: string + videoName: string + checkType: CheckerType +}) { + const { shortUUID, videoName } = options + const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.abuse.id).to.be.a('number') + checkVideo(notification.abuse.comment.video, videoName, shortUUID) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.abuse === undefined || n.abuse.comment.video.shortUUID !== shortUUID + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + return text.indexOf(shortUUID) !== -1 && text.indexOf('abuse') !== -1 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkNewAccountAbuseForModerators (options: CheckerBaseParams & { + displayName: string + checkType: CheckerType +}) { + const { displayName } = options + const notificationType = UserNotificationType.NEW_ABUSE_FOR_MODERATORS + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.abuse.id).to.be.a('number') + expect(notification.abuse.account.displayName).to.equal(displayName) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.abuse === undefined || n.abuse.account.displayName !== displayName + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + return text.indexOf(displayName) !== -1 && text.indexOf('abuse') !== -1 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkVideoAutoBlacklistForModerators (options: CheckerBaseParams & { + shortUUID: string + videoName: string + checkType: CheckerType +}) { + const { shortUUID, videoName } = options + const notificationType = UserNotificationType.VIDEO_AUTO_BLACKLIST_FOR_MODERATORS + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.videoBlacklist.video.id).to.be.a('number') + checkVideo(notification.videoBlacklist.video, videoName, shortUUID) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.video === undefined || n.video.shortUUID !== shortUUID + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + return text.indexOf(shortUUID) !== -1 && email['text'].indexOf('video-auto-blacklist/list') !== -1 + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkNewBlacklistOnMyVideo (options: CheckerBaseParams & { + shortUUID: string + videoName: string + blacklistType: 'blacklist' | 'unblacklist' +}) { + const { videoName, shortUUID, blacklistType } = options + const notificationType = blacklistType === 'blacklist' + ? UserNotificationType.BLACKLIST_ON_MY_VIDEO + : UserNotificationType.UNBLACKLIST_ON_MY_VIDEO + + function notificationChecker (notification: UserNotification) { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + const video = blacklistType === 'blacklist' ? notification.videoBlacklist.video : notification.video + + checkVideo(video, videoName, shortUUID) + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + const blacklistText = blacklistType === 'blacklist' + ? 'blacklisted' + : 'unblacklisted' + + return text.includes(shortUUID) && text.includes(blacklistText) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder, checkType: 'presence' }) +} + +async function checkNewPeerTubeVersion (options: CheckerBaseParams & { + latestVersion: string + checkType: CheckerType +}) { + const { latestVersion } = options + const notificationType = UserNotificationType.NEW_PEERTUBE_VERSION + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.peertube).to.exist + expect(notification.peertube.latestVersion).to.equal(latestVersion) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.peertube === undefined || n.peertube.latestVersion !== latestVersion + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + + return text.includes(latestVersion) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function checkNewPluginVersion (options: CheckerBaseParams & { + pluginType: PluginType + pluginName: string + checkType: CheckerType +}) { + const { pluginName, pluginType } = options + const notificationType = UserNotificationType.NEW_PLUGIN_VERSION + + function notificationChecker (notification: UserNotification, checkType: CheckerType) { + if (checkType === 'presence') { + expect(notification).to.not.be.undefined + expect(notification.type).to.equal(notificationType) + + expect(notification.plugin.name).to.equal(pluginName) + expect(notification.plugin.type).to.equal(pluginType) + } else { + expect(notification).to.satisfy((n: UserNotification) => { + return n === undefined || n.plugin === undefined || n.plugin.name !== pluginName + }) + } + } + + function emailNotificationFinder (email: object) { + const text = email['text'] + + return text.includes(pluginName) + } + + await checkNotification({ ...options, notificationChecker, emailNotificationFinder }) +} + +async function prepareNotificationsTest (serversCount = 3, overrideConfigArg: any = {}) { + const userNotifications: UserNotification[] = [] + const adminNotifications: UserNotification[] = [] + const adminNotificationsServer2: UserNotification[] = [] + const emails: object[] = [] + + const port = await MockSmtpServer.Instance.collectEmails(emails) + + const overrideConfig = { + smtp: { + hostname: 'localhost', + port + }, + signup: { + limit: 20 + } + } + const servers = await createMultipleServers(serversCount, Object.assign(overrideConfig, overrideConfigArg)) + + await setAccessTokensToServers(servers) + + if (serversCount > 1) { + await doubleFollow(servers[0], servers[1]) + } + + const user = { username: 'user_1', password: 'super password' } + await servers[0].users.create({ ...user, videoQuota: 10 * 1000 * 1000 }) + const userAccessToken = await servers[0].login.getAccessToken(user) + + await servers[0].notifications.updateMySettings({ token: userAccessToken, settings: getAllNotificationsSettings() }) + await servers[0].notifications.updateMySettings({ settings: getAllNotificationsSettings() }) + + if (serversCount > 1) { + await servers[1].notifications.updateMySettings({ settings: getAllNotificationsSettings() }) + } + + { + const socket = servers[0].socketIO.getUserNotificationSocket({ token: userAccessToken }) + socket.on('new-notification', n => userNotifications.push(n)) + } + { + const socket = servers[0].socketIO.getUserNotificationSocket() + socket.on('new-notification', n => adminNotifications.push(n)) + } + + if (serversCount > 1) { + const socket = servers[1].socketIO.getUserNotificationSocket() + socket.on('new-notification', n => adminNotificationsServer2.push(n)) + } + + const { videoChannels } = await servers[0].users.getMyInfo() + const channelId = videoChannels[0].id + + return { + userNotifications, + adminNotifications, + adminNotificationsServer2, + userAccessToken, + emails, + servers, + channelId + } +} + +// --------------------------------------------------------------------------- + +export { + getAllNotificationsSettings, + + CheckerBaseParams, + CheckerType, + checkMyVideoImportIsFinished, + checkUserRegistered, + checkAutoInstanceFollowing, + checkVideoIsPublished, + checkNewVideoFromSubscription, + checkNewActorFollow, + checkNewCommentOnMyVideo, + checkNewBlacklistOnMyVideo, + checkCommentMention, + checkNewVideoAbuseForModerators, + checkVideoAutoBlacklistForModerators, + checkNewAbuseMessage, + checkAbuseStateChange, + checkNewInstanceFollower, + prepareNotificationsTest, + checkNewCommentAbuseForModerators, + checkNewAccountAbuseForModerators, + checkNewPeerTubeVersion, + checkNewPluginVersion +} + +// --------------------------------------------------------------------------- + +async function checkNotification (options: CheckerBaseParams & { + notificationChecker: (notification: UserNotification, checkType: CheckerType) => void + emailNotificationFinder: (email: object) => boolean + checkType: CheckerType +}) { + const { server, token, checkType, notificationChecker, emailNotificationFinder, socketNotifications, emails } = options + + const check = options.check || { web: true, mail: true } + + if (check.web) { + const notification = await server.notifications.getLatest({ token: token }) + + if (notification || checkType !== 'absence') { + notificationChecker(notification, checkType) + } + + const socketNotification = socketNotifications.find(n => { + try { + notificationChecker(n, 'presence') + return true + } catch { + return false + } + }) + + if (checkType === 'presence') { + const obj = inspect(socketNotifications, { depth: 5 }) + expect(socketNotification, 'The socket notification is absent when it should be present. ' + obj).to.not.be.undefined + } else { + const obj = inspect(socketNotification, { depth: 5 }) + expect(socketNotification, 'The socket notification is present when it should not be present. ' + obj).to.be.undefined + } + } + + if (check.mail) { + // Last email + const email = emails + .slice() + .reverse() + .find(e => emailNotificationFinder(e)) + + if (checkType === 'presence') { + const texts = emails.map(e => e.text) + expect(email, 'The email is absent when is should be present. ' + inspect(texts)).to.not.be.undefined + } else { + expect(email, 'The email is present when is should not be present. ' + inspect(email)).to.be.undefined + } + } +} + +function checkVideo (video: any, videoName?: string, shortUUID?: string) { + if (videoName) { + expect(video.name).to.be.a('string') + expect(video.name).to.not.be.empty + expect(video.name).to.equal(videoName) + } + + if (shortUUID) { + expect(video.shortUUID).to.be.a('string') + expect(video.shortUUID).to.not.be.empty + expect(video.shortUUID).to.equal(shortUUID) + } + + expect(video.id).to.be.a('number') +} + +function checkActor (actor: any) { + expect(actor.displayName).to.be.a('string') + expect(actor.displayName).to.not.be.empty + expect(actor.host).to.not.be.undefined +} + +function checkComment (comment: any, commentId: number, threadId: number) { + expect(comment.id).to.equal(commentId) + expect(comment.threadId).to.equal(threadId) +} diff --git a/shared/server-commands/users/subscriptions-command.ts b/shared/server-commands/users/subscriptions-command.ts new file mode 100644 index 000000000..edc60e612 --- /dev/null +++ b/shared/server-commands/users/subscriptions-command.ts @@ -0,0 +1,99 @@ +import { HttpStatusCode, ResultList, Video, VideoChannel } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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>({ + ...options, + + path, + query: { + sort, + search + }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + listVideos (options: OverrideCommandOptions & { + sort?: string // default -createdAt + } = {}) { + const { sort = '-createdAt' } = options + const path = '/api/v1/users/me/subscriptions/videos' + + return this.getRequestBody>({ + ...options, + + path, + query: { sort }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + get (options: OverrideCommandOptions & { + uri: string + }) { + const path = '/api/v1/users/me/subscriptions/' + options.uri + + return this.getRequestBody({ + ...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 + }) + } +} diff --git a/shared/server-commands/users/users-command.ts b/shared/server-commands/users/users-command.ts new file mode 100644 index 000000000..90c5f2183 --- /dev/null +++ b/shared/server-commands/users/users-command.ts @@ -0,0 +1,416 @@ +import { omit } from 'lodash' +import { pick } from '@shared/core-utils' +import { + HttpStatusCode, + MyUser, + ResultList, + User, + UserAdminFlag, + UserCreateResult, + UserRole, + UserUpdate, + UserUpdateMe, + UserVideoQuota, + UserVideoRate +} from '@shared/models' +import { ScopedToken } from '@shared/models/users/user-scoped-token' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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({ + ...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?: UserRole + adminFlags?: UserAdminFlag + }) { + const { + username, + adminFlags, + password = 'password', + videoQuota = 42000000, + videoQuotaDaily = -1, + 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?: UserRole) { + 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 + } + } + + async generateUserAndToken (username: string, role?: UserRole) { + const password = 'password' + await this.create({ username, password, role }) + + return this.server.login.getAccessToken({ username, password }) + } + + register (options: OverrideCommandOptions & { + username: string + password?: string + displayName?: string + channel?: { + name: string + displayName: string + } + }) { + const { username, password = 'password', displayName, channel } = options + const path = '/api/v1/users/register' + + return this.postBodyRequest({ + ...options, + + path, + fields: { + username, + password, + email: username + '@example.com', + displayName, + channel + }, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + // --------------------------------------------------------------------------- + + getMyInfo (options: OverrideCommandOptions = {}) { + const path = '/api/v1/users/me' + + return this.getRequestBody({ + ...options, + + path, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getMyQuotaUsed (options: OverrideCommandOptions = {}) { + const path = '/api/v1/users/me/video-quota-used' + + return this.getRequestBody({ + ...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({ + ...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, 'url', 'accessToken') + + 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({ + ...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>({ + ...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?: UserAdminFlag + pluginAuth?: string + role?: UserRole + }) { + 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 + }) + } +} diff --git a/shared/server-commands/videos/blacklist-command.ts b/shared/server-commands/videos/blacklist-command.ts new file mode 100644 index 000000000..3a2ef89ba --- /dev/null +++ b/shared/server-commands/videos/blacklist-command.ts @@ -0,0 +1,76 @@ + +import { HttpStatusCode, ResultList } from '@shared/models' +import { VideoBlacklist, VideoBlacklistType } from '../../models/videos' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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 + } = {}) { + const { sort, type } = options + const path = '/api/v1/videos/blacklist/' + + const query = { sort, type } + + return this.getRequestBody>({ + ...options, + + path, + query, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/videos/captions-command.ts b/shared/server-commands/videos/captions-command.ts new file mode 100644 index 000000000..a65ea99e3 --- /dev/null +++ b/shared/server-commands/videos/captions-command.ts @@ -0,0 +1,65 @@ +import { HttpStatusCode, ResultList, VideoCaption } from '@shared/models' +import { buildAbsoluteFixturePath } from '../miscs' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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 + }) { + const { videoId } = options + const path = '/api/v1/videos/' + videoId + '/captions' + + return this.getRequestBody>({ + ...options, + + path, + 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 + }) + } +} diff --git a/shared/server-commands/videos/captions.ts b/shared/server-commands/videos/captions.ts new file mode 100644 index 000000000..35e722408 --- /dev/null +++ b/shared/server-commands/videos/captions.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai' +import request from 'supertest' +import { HttpStatusCode } from '@shared/models' + +async function testCaptionFile (url: string, captionPath: string, toTest: RegExp | string) { + const res = await request(url) + .get(captionPath) + .expect(HttpStatusCode.OK_200) + + if (toTest instanceof RegExp) { + expect(res.text).to.match(toTest) + } else { + expect(res.text).to.contain(toTest) + } +} + +// --------------------------------------------------------------------------- + +export { + testCaptionFile +} diff --git a/shared/server-commands/videos/change-ownership-command.ts b/shared/server-commands/videos/change-ownership-command.ts new file mode 100644 index 000000000..ad4c726ef --- /dev/null +++ b/shared/server-commands/videos/change-ownership-command.ts @@ -0,0 +1,68 @@ + +import { HttpStatusCode, ResultList, VideoChangeOwnership } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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>({ + ...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 + }) + } +} diff --git a/shared/server-commands/videos/channels-command.ts b/shared/server-commands/videos/channels-command.ts new file mode 100644 index 000000000..e406e570b --- /dev/null +++ b/shared/server-commands/videos/channels-command.ts @@ -0,0 +1,178 @@ +import { pick } from '@shared/core-utils' +import { ActorFollow, HttpStatusCode, ResultList, VideoChannel, VideoChannelCreateResult } from '@shared/models' +import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' +import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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>({ + ...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>({ + ...options, + + path, + query: { sort, ...pick(options, [ 'start', 'count', 'withStats', 'search' ]) }, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + async create (options: OverrideCommandOptions & { + attributes: Partial + }) { + 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({ + ...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>({ + ...options, + + path, + query, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/videos/channels.ts b/shared/server-commands/videos/channels.ts new file mode 100644 index 000000000..756c47453 --- /dev/null +++ b/shared/server-commands/videos/channels.ts @@ -0,0 +1,18 @@ +import { PeerTubeServer } from '../server/server' + +function setDefaultVideoChannel (servers: PeerTubeServer[]) { + const tasks: Promise[] = [] + + 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) +} + +export { + setDefaultVideoChannel +} diff --git a/shared/server-commands/videos/comments-command.ts b/shared/server-commands/videos/comments-command.ts new file mode 100644 index 000000000..f0d163a07 --- /dev/null +++ b/shared/server-commands/videos/comments-command.ts @@ -0,0 +1,152 @@ +import { pick } from 'lodash' +import { HttpStatusCode, ResultList, VideoComment, VideoCommentThreads, VideoCommentThreadTree } from '@shared/models' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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 + search?: string + searchAccount?: string + searchVideo?: string + } = {}) { + const { sort = '-createdAt' } = options + const path = '/api/v1/videos/comments' + + const query = { sort, ...pick(options, [ 'start', 'count', 'isLocal', 'search', 'searchAccount', 'searchVideo' ]) } + + return this.getRequestBody>({ + ...options, + + path, + query, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + listThreads (options: OverrideCommandOptions & { + videoId: number | string + start?: number + count?: number + sort?: string + }) { + const { start, count, sort, videoId } = options + const path = '/api/v1/videos/' + videoId + '/comment-threads' + + return this.getRequestBody({ + ...options, + + path, + query: { start, count, sort }, + 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({ + ...options, + + path, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + async createThread (options: OverrideCommandOptions & { + videoId: number | string + text: string + }) { + const { videoId, text } = options + const path = '/api/v1/videos/' + videoId + '/comment-threads' + + const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({ + ...options, + + path, + fields: { text }, + 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 + }) { + const { videoId, toCommentId, text } = options + const path = '/api/v1/videos/' + videoId + '/comments/' + toCommentId + + const body = await unwrapBody<{ comment: VideoComment }>(this.postBodyRequest({ + ...options, + + path, + fields: { text }, + 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 + }) + } +} diff --git a/shared/server-commands/videos/history-command.ts b/shared/server-commands/videos/history-command.ts new file mode 100644 index 000000000..13b7150c1 --- /dev/null +++ b/shared/server-commands/videos/history-command.ts @@ -0,0 +1,58 @@ +import { HttpStatusCode, ResultList, Video } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class HistoryCommand extends AbstractCommand { + + wathVideo (options: OverrideCommandOptions & { + videoId: number | string + currentTime: number + }) { + const { videoId, currentTime } = options + + const path = '/api/v1/videos/' + videoId + '/watching' + const fields = { currentTime } + + return this.putBodyRequest({ + ...options, + + path, + fields, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + list (options: OverrideCommandOptions & { + search?: string + } = {}) { + const { search } = options + const path = '/api/v1/users/me/history/videos' + + return this.getRequestBody>({ + ...options, + + path, + query: { + search + }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + remove (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 + }) + } +} diff --git a/shared/server-commands/videos/imports-command.ts b/shared/server-commands/videos/imports-command.ts new file mode 100644 index 000000000..e4944694d --- /dev/null +++ b/shared/server-commands/videos/imports-command.ts @@ -0,0 +1,47 @@ + +import { HttpStatusCode, ResultList } from '@shared/models' +import { VideoImport, VideoImportCreate } from '../../models/videos' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class ImportsCommand extends AbstractCommand { + + importVideo (options: OverrideCommandOptions & { + attributes: VideoImportCreate & { torrentfile?: string } + }) { + const { attributes } = options + const path = '/api/v1/videos/imports' + + let attaches: any = {} + if (attributes.torrentfile) attaches = { torrentfile: attributes.torrentfile } + + return unwrapBody(this.postUploadRequest({ + ...options, + + path, + attaches, + fields: options.attributes, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + getMyVideoImports (options: OverrideCommandOptions & { + sort?: string + } = {}) { + const { sort } = options + const path = '/api/v1/users/me/videos/imports' + + const query = {} + if (sort) query['sort'] = sort + + return this.getRequestBody>({ + ...options, + + path, + query: { sort }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/videos/index.ts b/shared/server-commands/videos/index.ts new file mode 100644 index 000000000..26e663f46 --- /dev/null +++ b/shared/server-commands/videos/index.ts @@ -0,0 +1,19 @@ +export * from './blacklist-command' +export * from './captions-command' +export * from './captions' +export * from './change-ownership-command' +export * from './channels' +export * from './channels-command' +export * from './comments-command' +export * from './history-command' +export * from './imports-command' +export * from './live-command' +export * from './live' +export * from './playlists-command' +export * from './playlists' +export * from './services-command' +export * from './streaming-playlists-command' +export * from './streaming-playlists' +export * from './comments-command' +export * from './videos-command' +export * from './videos' diff --git a/shared/server-commands/videos/live-command.ts b/shared/server-commands/videos/live-command.ts new file mode 100644 index 000000000..74f5d3089 --- /dev/null +++ b/shared/server-commands/videos/live-command.ts @@ -0,0 +1,155 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { readdir } from 'fs-extra' +import { omit } from 'lodash' +import { join } from 'path' +import { HttpStatusCode, LiveVideo, LiveVideoCreate, LiveVideoUpdate, VideoCreateResult, VideoDetails, VideoState } from '@shared/models' +import { wait } from '../miscs' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' +import { sendRTMPStream, testFfmpegStreamError } from './live' + +export class LiveCommand extends AbstractCommand { + + get (options: OverrideCommandOptions & { + videoId: number | string + }) { + const path = '/api/v1/videos/live' + + return this.getRequestBody({ + ...options, + + path: path + '/' + options.videoId, + 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 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 }) + } + + waitUntilSegmentGeneration (options: OverrideCommandOptions & { + videoUUID: string + resolution: number + segment: number + }) { + const { resolution, segment, videoUUID } = options + const segmentName = `${resolution}-00000${segment}.ts` + + return this.server.servers.waitUntilLog(`${videoUUID}/${segmentName}`, 2, false) + } + + async waitUntilSaved (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) + } + + 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: VideoState + }) { + 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) + } +} diff --git a/shared/server-commands/videos/live.ts b/shared/server-commands/videos/live.ts new file mode 100644 index 000000000..d3665bc90 --- /dev/null +++ b/shared/server-commands/videos/live.ts @@ -0,0 +1,137 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ + +import { expect } from 'chai' +import ffmpeg, { FfmpegCommand } from 'fluent-ffmpeg' +import { pathExists, readdir } from 'fs-extra' +import { join } from 'path' +import { buildAbsoluteFixturePath, wait } from '../miscs' +import { PeerTubeServer } from '../server/server' + +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 50') + command.outputOption('-keyint_min 2') + 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.run() + + return command +} + +function waitFfmpegUntilError (command: FfmpegCommand, successAfterMS = 10000) { + return new Promise((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, 35000) + } 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 waitUntilLiveSavedOnAllServers (servers: PeerTubeServer[], videoId: string) { + for (const server of servers) { + await server.live.waitUntilSaved({ videoId }) + } +} + +async function checkLiveCleanupAfterSave (server: PeerTubeServer, videoUUID: string, resolutions: number[] = []) { + const basePath = server.servers.buildDirectory('streaming-playlists') + const hlsPath = join(basePath, 'hls', videoUUID) + + if (resolutions.length === 0) { + const result = await pathExists(hlsPath) + expect(result).to.be.false + + return + } + + const files = await readdir(hlsPath) + + // fragmented file and playlist per resolution + master playlist + segments sha256 json file + expect(files).to.have.lengthOf(resolutions.length * 2 + 2) + + for (const resolution of resolutions) { + const fragmentedFile = files.find(f => f.endsWith(`-${resolution}-fragmented.mp4`)) + expect(fragmentedFile).to.exist + + const playlistFile = files.find(f => f.endsWith(`${resolution}.m3u8`)) + expect(playlistFile).to.exist + } + + const masterPlaylistFile = files.find(f => f.endsWith('-master.m3u8')) + expect(masterPlaylistFile).to.exist + + const shaFile = files.find(f => f.endsWith('-segments-sha256.json')) + expect(shaFile).to.exist +} + +export { + sendRTMPStream, + waitFfmpegUntilError, + testFfmpegStreamError, + stopFfmpeg, + waitUntilLivePublishedOnAllServers, + waitUntilLiveSavedOnAllServers, + checkLiveCleanupAfterSave +} diff --git a/shared/server-commands/videos/playlists-command.ts b/shared/server-commands/videos/playlists-command.ts new file mode 100644 index 000000000..ce23900d3 --- /dev/null +++ b/shared/server-commands/videos/playlists-command.ts @@ -0,0 +1,280 @@ +import { omit } from 'lodash' +import { pick } from '@shared/core-utils' +import { + BooleanBothQuery, + HttpStatusCode, + ResultList, + VideoExistInPlaylist, + VideoPlaylist, + VideoPlaylistCreate, + VideoPlaylistCreateResult, + VideoPlaylistElement, + VideoPlaylistElementCreate, + VideoPlaylistElementCreateResult, + VideoPlaylistElementUpdate, + VideoPlaylistReorder, + VideoPlaylistType, + VideoPlaylistUpdate +} from '@shared/models' +import { unwrapBody } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class PlaylistsCommand extends AbstractCommand { + + list (options: OverrideCommandOptions & { + start?: number + count?: number + sort?: string + }) { + const path = '/api/v1/video-playlists' + const query = pick(options, [ 'start', 'count', 'sort' ]) + + return this.getRequestBody>({ + ...options, + + path, + query, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + listByChannel (options: OverrideCommandOptions & { + handle: string + start?: number + count?: number + sort?: string + }) { + const path = '/api/v1/video-channels/' + options.handle + '/video-playlists' + const query = pick(options, [ 'start', 'count', 'sort' ]) + + return this.getRequestBody>({ + ...options, + + path, + query, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + listByAccount (options: OverrideCommandOptions & { + handle: string + start?: number + count?: number + sort?: string + search?: string + playlistType?: VideoPlaylistType + }) { + const path = '/api/v1/accounts/' + options.handle + '/video-playlists' + const query = pick(options, [ 'start', 'count', 'sort', 'search', 'playlistType' ]) + + return this.getRequestBody>({ + ...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({ + ...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>({ + ...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({ + ...options, + + path, + query: { videoIds }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } +} diff --git a/shared/server-commands/videos/playlists.ts b/shared/server-commands/videos/playlists.ts new file mode 100644 index 000000000..3dde52bb9 --- /dev/null +++ b/shared/server-commands/videos/playlists.ts @@ -0,0 +1,25 @@ +import { expect } from 'chai' +import { readdir } from 'fs-extra' +import { join } from 'path' +import { root } from '../miscs' + +async function checkPlaylistFilesWereRemoved ( + playlistUUID: string, + internalServerNumber: number, + directories = [ 'thumbnails' ] +) { + const testDirectory = 'test' + internalServerNumber + + for (const directory of directories) { + const directoryPath = join(root(), testDirectory, directory) + + const files = await readdir(directoryPath) + for (const file of files) { + expect(file).to.not.contain(playlistUUID) + } + } +} + +export { + checkPlaylistFilesWereRemoved +} diff --git a/shared/server-commands/videos/services-command.ts b/shared/server-commands/videos/services-command.ts new file mode 100644 index 000000000..06760df42 --- /dev/null +++ b/shared/server-commands/videos/services-command.ts @@ -0,0 +1,29 @@ +import { HttpStatusCode } from '@shared/models' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +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 + }) + } +} diff --git a/shared/server-commands/videos/streaming-playlists-command.ts b/shared/server-commands/videos/streaming-playlists-command.ts new file mode 100644 index 000000000..5d40d35cb --- /dev/null +++ b/shared/server-commands/videos/streaming-playlists-command.ts @@ -0,0 +1,44 @@ +import { HttpStatusCode } from '@shared/models' +import { unwrapBody, unwrapTextOrDecode, unwrapBodyOrDecodeToJSON } from '../requests' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export class StreamingPlaylistsCommand extends AbstractCommand { + + get (options: OverrideCommandOptions & { + url: string + }) { + return unwrapTextOrDecode(this.getRawRequest({ + ...options, + + url: options.url, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + getSegment (options: OverrideCommandOptions & { + url: string + range?: string + }) { + return unwrapBody(this.getRawRequest({ + ...options, + + url: options.url, + range: options.range, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + getSegmentSha256 (options: OverrideCommandOptions & { + url: string + }) { + return unwrapBodyOrDecodeToJSON<{ [ id: string ]: string }>(this.getRawRequest({ + ...options, + + url: options.url, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } +} diff --git a/shared/server-commands/videos/streaming-playlists.ts b/shared/server-commands/videos/streaming-playlists.ts new file mode 100644 index 000000000..0451c0efe --- /dev/null +++ b/shared/server-commands/videos/streaming-playlists.ts @@ -0,0 +1,77 @@ +import { expect } from 'chai' +import { basename } from 'path' +import { sha256 } from '@shared/core-utils/crypto' +import { removeFragmentedMP4Ext } from '@shared/core-utils' +import { HttpStatusCode, VideoStreamingPlaylist } from '@shared/models' +import { PeerTubeServer } from '../server' + +async function checkSegmentHash (options: { + server: PeerTubeServer + baseUrlPlaylist: string + baseUrlSegment: string + resolution: number + hlsPlaylist: VideoStreamingPlaylist +}) { + const { server, baseUrlPlaylist, baseUrlSegment, resolution, hlsPlaylist } = options + const command = server.streamingPlaylists + + const file = hlsPlaylist.files.find(f => f.resolution.id === resolution) + const videoName = basename(file.fileUrl) + + const playlist = await command.get({ url: `${baseUrlPlaylist}/${removeFragmentedMP4Ext(videoName)}.m3u8` }) + + const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist) + + const length = parseInt(matches[1], 10) + const offset = parseInt(matches[2], 10) + const range = `${offset}-${offset + length - 1}` + + const segmentBody = await command.getSegment({ + url: `${baseUrlSegment}/${videoName}`, + expectedStatus: HttpStatusCode.PARTIAL_CONTENT_206, + range: `bytes=${range}` + }) + + const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) + expect(sha256(segmentBody)).to.equal(shaBody[videoName][range]) +} + +async function checkLiveSegmentHash (options: { + server: PeerTubeServer + baseUrlSegment: string + videoUUID: string + segmentName: string + hlsPlaylist: VideoStreamingPlaylist +}) { + const { server, baseUrlSegment, videoUUID, segmentName, hlsPlaylist } = options + const command = server.streamingPlaylists + + const segmentBody = await command.getSegment({ url: `${baseUrlSegment}/${videoUUID}/${segmentName}` }) + const shaBody = await command.getSegmentSha256({ url: hlsPlaylist.segmentsSha256Url }) + + expect(sha256(segmentBody)).to.equal(shaBody[segmentName]) +} + +async function checkResolutionsInMasterPlaylist (options: { + server: PeerTubeServer + playlistUrl: string + resolutions: number[] +}) { + const { server, playlistUrl, resolutions } = options + + const masterPlaylist = await server.streamingPlaylists.get({ url: playlistUrl }) + + for (const resolution of resolutions) { + const reg = new RegExp( + '#EXT-X-STREAM-INF:BANDWIDTH=\\d+,RESOLUTION=\\d+x' + resolution + ',(FRAME-RATE=\\d+,)?CODECS="avc1.64001f,mp4a.40.2"' + ) + + expect(masterPlaylist).to.match(reg) + } +} + +export { + checkSegmentHash, + checkLiveSegmentHash, + checkResolutionsInMasterPlaylist +} diff --git a/shared/server-commands/videos/videos-command.ts b/shared/server-commands/videos/videos-command.ts new file mode 100644 index 000000000..8ea828b40 --- /dev/null +++ b/shared/server-commands/videos/videos-command.ts @@ -0,0 +1,679 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ + +import { expect } from 'chai' +import { createReadStream, stat } from 'fs-extra' +import got, { Response as GotResponse } from 'got' +import { omit } from 'lodash' +import validator from 'validator' +import { buildUUID } from '@shared/core-utils/uuid' +import { pick } from '@shared/core-utils' +import { + HttpStatusCode, + ResultList, + UserVideoRateType, + Video, + VideoCreate, + VideoCreateResult, + VideoDetails, + VideoFileMetadata, + VideoPrivacy, + VideosCommonQuery, + VideoTranscodingCreate +} from '@shared/models' +import { buildAbsoluteFixturePath, wait } from '../miscs' +import { unwrapBody } from '../requests' +import { waitJobs } from '../server' +import { AbstractCommand, OverrideCommandOptions } from '../shared' + +export type VideoEdit = Partial> & { + 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 VideoPrivacy]: 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(this.getRawRequest({ + ...options, + + url: options.url, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + })) + } + + // --------------------------------------------------------------------------- + + view (options: OverrideCommandOptions & { + id: number | string + xForwardedFor?: string + }) { + const { id, xForwardedFor } = options + const path = '/api/v1/videos/' + id + '/views' + + return this.postBodyRequest({ + ...options, + + path, + xForwardedFor, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + rate (options: OverrideCommandOptions & { + id: number | string + rating: UserVideoRateType + }) { + const { id, rating } = options + const path = '/api/v1/videos/' + id + '/rate' + + return this.putBodyRequest({ + ...options, + + path, + fields: { rating }, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + // --------------------------------------------------------------------------- + + get (options: OverrideCommandOptions & { + id: number | string + }) { + const path = '/api/v1/videos/' + options.id + + return this.getRequestBody({ + ...options, + + path, + implicitToken: false, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + getWithToken (options: OverrideCommandOptions & { + id: number | string + }) { + return this.get({ + ...options, + + token: this.buildCommonRequestToken({ ...options, implicitToken: true }) + }) + } + + async getId (options: OverrideCommandOptions & { + uuid: number | string + }) { + const { uuid } = options + + if (validator.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>({ + ...options, + + path, + query: pick(options, [ 'start', 'count', 'sort', 'search', 'isLive', 'channelId' ]), + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.OK_200 + }) + } + + // --------------------------------------------------------------------------- + + list (options: OverrideCommandOptions & VideosCommonQuery = {}) { + const path = '/api/v1/videos' + + const query = this.buildListQuery(options) + + return this.getRequestBody>({ + ...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 }) + }) + } + + listByAccount (options: OverrideCommandOptions & VideosCommonQuery & { + handle: string + }) { + const { handle, search } = options + const path = '/api/v1/accounts/' + handle + '/videos' + + return this.getRequestBody>({ + ...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>({ + ...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 + } = {}) { + const { mode = 'legacy' } = 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, attributes }) + + // Wait torrent generation + const expectedStatus = this.buildExpectedStatus({ ...options, defaultExpectedStatus: HttpStatusCode.OK_200 }) + if (expectedStatus === HttpStatusCode.OK_200) { + 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 { + 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 & { + attributes: VideoEdit + }): Promise { + const { attributes, expectedStatus } = 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, 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, pathUploadId, videoFilePath, size }) + + if (result.statusCode === HttpStatusCode.OK_200) { + await this.endResumableUpload({ ...options, expectedStatus: HttpStatusCode.NO_CONTENT_204, 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 & { + attributes: VideoEdit + size: number + mimetype: string + + originalName?: string + lastModified?: number + }) { + const { attributes, originalName, lastModified, size, mimetype } = options + + const path = '/api/v1/videos/upload-resumable' + + return this.postUploadRequest({ + ...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 + }) + } + + sendResumableChunks (options: OverrideCommandOptions & { + pathUploadId: string + videoFilePath: string + size: number + contentLength?: number + contentRangeBuilder?: (start: number, chunk: any) => string + }) { + const { pathUploadId, videoFilePath, size, contentLength, contentRangeBuilder, expectedStatus = HttpStatusCode.OK_200 } = options + + const path = '/api/v1/videos/upload-resumable' + let start = 0 + + const token = this.buildCommonRequestToken({ ...options, implicitToken: true }) + const url = this.server.url + + const readable = createReadStream(videoFilePath, { highWaterMark: 8 * 1024 }) + return new Promise>((resolve, reject) => { + readable.on('data', async function onData (chunk) { + readable.pause() + + const headers = { + 'Authorization': 'Bearer ' + token, + 'Content-Type': 'application/octet-stream', + 'Content-Range': contentRangeBuilder + ? contentRangeBuilder(start, chunk) + : `bytes ${start}-${start + chunk.length - 1}/${size}`, + 'Content-Length': contentLength ? contentLength + '' : chunk.length + '' + } + + const res = await got<{ video: VideoCreateResult }>({ + url, + method: 'put', + headers, + path: path + '?' + pathUploadId, + body: chunk, + responseType: 'json', + throwHttpErrors: false + }) + + start += chunk.length + + if (res.statusCode === expectedStatus) { + return resolve(res) + } + + if (res.statusCode !== HttpStatusCode.PERMANENT_REDIRECT_308) { + readable.off('data', onData) + return reject(new Error('Incorrect transient behaviour sending intermediary chunks')) + } + + readable.resume() + }) + }) + } + + endResumableUpload (options: OverrideCommandOptions & { + pathUploadId: string + }) { + return this.deleteRequest({ + ...options, + + path: '/api/v1/videos/upload-resumable', + rawQuery: options.pathUploadId, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + quickUpload (options: OverrideCommandOptions & { + name: string + nsfw?: boolean + privacy?: VideoPrivacy + fixture?: 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 + + 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 } + } + + // --------------------------------------------------------------------------- + + removeHLSFiles (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 + }) + } + + removeWebTorrentFiles (options: OverrideCommandOptions & { + videoId: number | string + }) { + const path = '/api/v1/videos/' + options.videoId + '/webtorrent' + + return this.deleteRequest({ + ...options, + + path, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + runTranscoding (options: OverrideCommandOptions & { + videoId: number | string + transcodingType: 'hls' | 'webtorrent' + }) { + const path = '/api/v1/videos/' + options.videoId + '/transcoding' + + const fields: VideoTranscodingCreate = pick(options, [ 'transcodingType' ]) + + return this.postBodyRequest({ + ...options, + + path, + fields, + implicitToken: true, + defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204 + }) + } + + // --------------------------------------------------------------------------- + + private buildListQuery (options: VideosCommonQuery) { + return pick(options, [ + 'start', + 'count', + 'sort', + 'nsfw', + 'isLive', + 'categoryOneOf', + 'licenceOneOf', + 'languageOneOf', + '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 + } +} diff --git a/shared/server-commands/videos/videos.ts b/shared/server-commands/videos/videos.ts new file mode 100644 index 000000000..2c3464aa8 --- /dev/null +++ b/shared/server-commands/videos/videos.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/no-floating-promises */ + +import { expect } from 'chai' +import { pathExists, readdir } from 'fs-extra' +import { basename, join } from 'path' +import { HttpStatusCode, VideoCaption, VideoDetails } from '@shared/models' +import { waitJobs } from '../server' +import { PeerTubeServer } from '../server/server' +import { VideoEdit } from './videos-command' + +async function checkVideoFilesWereRemoved (options: { + server: PeerTubeServer + video: VideoDetails + captions?: VideoCaption[] + onlyVideoFiles?: boolean // default false +}) { + const { video, server, captions = [], onlyVideoFiles = false } = options + + const webtorrentFiles = video.files || [] + const hlsFiles = video.streamingPlaylists[0]?.files || [] + + const thumbnailName = basename(video.thumbnailPath) + const previewName = basename(video.previewPath) + + const torrentNames = webtorrentFiles.concat(hlsFiles).map(f => basename(f.torrentUrl)) + + const captionNames = captions.map(c => basename(c.captionPath)) + + const webtorrentFilenames = webtorrentFiles.map(f => basename(f.fileUrl)) + const hlsFilenames = hlsFiles.map(f => basename(f.fileUrl)) + + let directories: { [ directory: string ]: string[] } = { + videos: webtorrentFilenames, + redundancy: webtorrentFilenames, + [join('playlists', 'hls')]: hlsFilenames, + [join('redundancy', 'hls')]: hlsFilenames + } + + if (onlyVideoFiles !== true) { + directories = { + ...directories, + + thumbnails: [ thumbnailName ], + previews: [ previewName ], + torrents: torrentNames, + captions: captionNames + } + } + + for (const directory of Object.keys(directories)) { + const directoryPath = server.servers.buildDirectory(directory) + + const directoryExists = await pathExists(directoryPath) + if (directoryExists === false) continue + + const existingFiles = await readdir(directoryPath) + for (const existingFile of existingFiles) { + for (const shouldNotExist of directories[directory]) { + expect(existingFile, `File ${existingFile} should not exist in ${directoryPath}`).to.not.contain(shouldNotExist) + } + } + } +} + +async function saveVideoInServers (servers: PeerTubeServer[], uuid: string) { + for (const server of servers) { + server.store.videoDetails = await server.videos.get({ id: uuid }) + } +} + +function checkUploadVideoParam ( + server: PeerTubeServer, + token: string, + attributes: Partial, + expectedStatus = HttpStatusCode.OK_200, + mode: 'legacy' | 'resumable' = 'legacy' +) { + return mode === 'legacy' + ? server.videos.buildLegacyUpload({ token, attributes, expectedStatus }) + : server.videos.buildResumeUpload({ token, attributes, expectedStatus }) +} + +// serverNumber starts from 1 +async function uploadRandomVideoOnServers ( + servers: PeerTubeServer[], + serverNumber: number, + additionalParams?: VideoEdit & { prefixName?: string } +) { + const server = servers.find(s => s.serverNumber === serverNumber) + const res = await server.videos.randomUpload({ wait: false, additionalParams }) + + await waitJobs(servers) + + return res +} + +// --------------------------------------------------------------------------- + +export { + checkUploadVideoParam, + uploadRandomVideoOnServers, + checkVideoFilesWereRemoved, + saveVideoInServers +} diff --git a/shared/tsconfig.types.json b/shared/tsconfig.types.json index b01d12e53..73c1cae6c 100644 --- a/shared/tsconfig.types.json +++ b/shared/tsconfig.types.json @@ -5,5 +5,8 @@ "stripInternal": true, "removeComments": false, "emitDeclarationOnly": true - } + }, + "exclude": [ + "server-commands/" + ] } diff --git a/shared/typescript-utils/index.ts b/shared/typescript-utils/index.ts new file mode 100644 index 000000000..c9f6f047d --- /dev/null +++ b/shared/typescript-utils/index.ts @@ -0,0 +1 @@ +export * from './types' diff --git a/shared/typescript-utils/types.ts b/shared/typescript-utils/types.ts new file mode 100644 index 000000000..bd2a97b98 --- /dev/null +++ b/shared/typescript-utils/types.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/array-type */ + +export type FunctionPropertyNames = { + [K in keyof T]: T[K] extends Function ? K : never +}[keyof T] + +export type FunctionProperties = Pick> + +export type AttributesOnly = { + [K in keyof T]: T[K] extends Function ? never : T[K] +} + +export type PickWith = { + [P in KT]: T[P] extends V ? V : never +} + +export type PickWithOpt = { + [P in KT]?: T[P] extends V ? V : never +} + +// https://github.com/krzkaczor/ts-essentials Rocks! +export type DeepPartial = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : DeepPartial +} + +type Primitive = string | Function | number | boolean | Symbol | undefined | null +export type DeepOmitHelper = { + [P in K]: // extra level of indirection needed to trigger homomorhic behavior + T[P] extends infer TP // distribute over unions + ? TP extends Primitive + ? TP // leave primitives and functions alone + : TP extends any[] + ? DeepOmitArray // Array special handling + : DeepOmit + : never +} +export type DeepOmit = T extends Primitive ? T : DeepOmitHelper> + +export type DeepOmitArray = { + [P in keyof T]: DeepOmit +} -- cgit v1.2.3