diff --git a/package.json b/package.json index fde913574..306476c6a 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,6 @@ "clean:server:test": "scripty", "watch:client": "scripty", "watch:server": "scripty", - "plugin:install": "node ./dist/scripts/plugin/install.js", - "plugin:uninstall": "node ./dist/scripts/plugin/uninstall.js", "danger:clean:dev": "scripty", "danger:clean:prod": "scripty", "danger:clean:modules": "scripty", @@ -45,6 +43,7 @@ "dev": "scripty", "dev:server": "scripty", "dev:client": "scripty", + "dev:cli": "scripty", "start": "node dist/server", "start:server": "node dist/server --no-client", "update-host": "node ./dist/scripts/update-host.js", diff --git a/scripts/dev/cli.sh b/scripts/dev/cli.sh new file mode 100755 index 000000000..4b6fe5508 --- /dev/null +++ b/scripts/dev/cli.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -eu + +rm -rf ./dist/server/tools/ + +( + cd ./server/tools + yarn install --pure-lockfile +) + +mkdir -p "./dist/server/tools" +cp -r "./server/tools/node_modules" "./dist/server/tools" + +npm run tsc -- --watch --project ./server/tools/tsconfig.json diff --git a/scripts/plugin/install.ts b/scripts/plugin/install.ts deleted file mode 100755 index 1725cbeb6..000000000 --- a/scripts/plugin/install.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { initDatabaseModels } from '../../server/initializers/database' -import * as program from 'commander' -import { PluginManager } from '../../server/lib/plugins/plugin-manager' -import { isAbsolute } from 'path' - -program - .option('-n, --plugin-name [pluginName]', 'Plugin name to install') - .option('-v, --plugin-version [pluginVersion]', 'Plugin version to install') - .option('-p, --plugin-path [pluginPath]', 'Path of the plugin you want to install') - .parse(process.argv) - -if (!program['pluginName'] && !program['pluginPath']) { - console.error('You need to specify a plugin name with the desired version, or a plugin path.') - process.exit(-1) -} - -if (program['pluginName'] && !program['pluginVersion']) { - console.error('You need to specify a the version of the plugin you want to install.') - process.exit(-1) -} - -if (program['pluginPath'] && !isAbsolute(program['pluginPath'])) { - console.error('Plugin path should be absolute.') - process.exit(-1) -} - -run() - .then(() => process.exit(0)) - .catch(err => { - console.error(err) - process.exit(-1) - }) - -async function run () { - await initDatabaseModels(true) - - const toInstall = program['pluginName'] || program['pluginPath'] - await PluginManager.Instance.install(toInstall, program['pluginVersion'], !!program['pluginPath']) -} diff --git a/scripts/plugin/uninstall.ts b/scripts/plugin/uninstall.ts deleted file mode 100755 index 7dcc234db..000000000 --- a/scripts/plugin/uninstall.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { initDatabaseModels } from '../../server/initializers/database' -import * as program from 'commander' -import { PluginManager } from '../../server/lib/plugins/plugin-manager' -import { isAbsolute } from 'path' - -program - .option('-n, --package-name [packageName]', 'Package name to install') - .parse(process.argv) - -if (!program['packageName']) { - console.error('You need to specify the plugin name.') - process.exit(-1) -} - -run() - .then(() => process.exit(0)) - .catch(err => { - console.error(err) - process.exit(-1) - }) - -async function run () { - await initDatabaseModels(true) - - const toUninstall = program['packageName'] - await PluginManager.Instance.uninstall(toUninstall) -} diff --git a/server/controllers/api/plugins.ts b/server/controllers/api/plugins.ts index f17e8cab9..8e59f27cf 100644 --- a/server/controllers/api/plugins.ts +++ b/server/controllers/api/plugins.ts @@ -21,6 +21,7 @@ import { import { PluginManager } from '../../lib/plugins/plugin-manager' import { InstallPlugin } from '../../../shared/models/plugins/install-plugin.model' import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model' +import { logger } from '../../helpers/logger' const pluginRouter = express.Router() @@ -46,7 +47,7 @@ pluginRouter.get('/:npmName/registered-settings', authenticate, ensureUserHasRight(UserRight.MANAGE_PLUGINS), asyncMiddleware(existingPluginValidator), - asyncMiddleware(getPluginRegisteredSettings) + getPluginRegisteredSettings ) pluginRouter.put('/:npmName/settings', @@ -101,7 +102,14 @@ function getPlugin (req: express.Request, res: express.Response) { async function installPlugin (req: express.Request, res: express.Response) { const body: InstallPlugin = req.body - await PluginManager.Instance.install(body.npmName) + const fromDisk = !!body.path + const toInstall = body.npmName || body.path + try { + await PluginManager.Instance.install(toInstall, undefined, fromDisk) + } catch (err) { + logger.warn('Cannot install plugin %s.', toInstall, { err }) + return res.sendStatus(400) + } return res.sendStatus(204) } @@ -114,10 +122,10 @@ async function uninstallPlugin (req: express.Request, res: express.Response) { return res.sendStatus(204) } -async function getPluginRegisteredSettings (req: express.Request, res: express.Response) { +function getPluginRegisteredSettings (req: express.Request, res: express.Response) { const plugin = res.locals.plugin - const settings = await PluginManager.Instance.getSettings(plugin.name) + const settings = PluginManager.Instance.getSettings(plugin.name) return res.json({ settings diff --git a/server/controllers/themes.ts b/server/controllers/themes.ts deleted file mode 100644 index 104c285ad..000000000 --- a/server/controllers/themes.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as express from 'express' -import { join } from 'path' -import { RegisteredPlugin } from '../lib/plugins/plugin-manager' -import { serveThemeCSSValidator } from '../middlewares/validators/themes' - -const themesRouter = express.Router() - -themesRouter.get('/:themeName/:themeVersion/css/:staticEndpoint(*)', - serveThemeCSSValidator, - serveThemeCSSDirectory -) - -// --------------------------------------------------------------------------- - -export { - themesRouter -} - -// --------------------------------------------------------------------------- - -function serveThemeCSSDirectory (req: express.Request, res: express.Response) { - const plugin: RegisteredPlugin = res.locals.registeredPlugin - const staticEndpoint = req.params.staticEndpoint - - if (plugin.css.includes(staticEndpoint) === false) { - return res.sendStatus(404) - } - - return res.sendFile(join(plugin.path, staticEndpoint)) -} diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index c5b139378..64818d036 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts @@ -3,7 +3,6 @@ Useful to avoid circular dependencies. */ -import * as bcrypt from 'bcrypt' import * as createTorrent from 'create-torrent' import { createHash, HexBase64Latin1Encoding, pseudoRandomBytes } from 'crypto' import { isAbsolute, join } from 'path' @@ -258,9 +257,6 @@ function promisify2WithVoid (func: (arg1: T, arg2: U, cb: (err: any) => vo const pseudoRandomBytesPromise = promisify1(pseudoRandomBytes) const createPrivateKey = promisify1(pem.createPrivateKey) const getPublicKey = promisify1(pem.getPublicKey) -const bcryptComparePromise = promisify2(bcrypt.compare) -const bcryptGenSaltPromise = promisify1(bcrypt.genSalt) -const bcryptHashPromise = promisify2(bcrypt.hash) const createTorrentPromise = promisify2(createTorrent) const execPromise2 = promisify2(exec) const execPromise = promisify1(exec) @@ -287,13 +283,11 @@ export { promisify0, promisify1, + promisify2, pseudoRandomBytesPromise, createPrivateKey, getPublicKey, - bcryptComparePromise, - bcryptGenSaltPromise, - bcryptHashPromise, createTorrentPromise, execPromise2, execPromise diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts index e1a2f9503..f818ce8f1 100644 --- a/server/helpers/custom-validators/video-channels.ts +++ b/server/helpers/custom-validators/video-channels.ts @@ -51,7 +51,9 @@ export { function processVideoChannelExist (videoChannel: VideoChannelModel, res: express.Response) { if (!videoChannel) { - `` + res.status(404) + .json({ error: 'Video channel not found' }) + .end() return false } diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 9148df2eb..1424949d0 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -1,12 +1,17 @@ import { Request } from 'express' import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' import { ActorModel } from '../models/activitypub/actor' -import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils' +import { createPrivateKey, getPublicKey, promisify1, promisify2, sha256 } from './core-utils' import { jsig, jsonld } from './custom-jsonld-signature' import { logger } from './logger' import { cloneDeep } from 'lodash' import { createVerify } from 'crypto' import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils' +import * as bcrypt from 'bcrypt' + +const bcryptComparePromise = promisify2(bcrypt.compare) +const bcryptGenSaltPromise = promisify1(bcrypt.genSalt) +const bcryptHashPromise = promisify2(bcrypt.hash) const httpSignature = require('http-signature') @@ -147,3 +152,5 @@ export { cryptPassword, signJsonLDObject } + +// --------------------------------------------------------------------------- diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index a06add6b8..a1634ded4 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts @@ -6,6 +6,7 @@ import { isPluginNameValid, isPluginTypeValid, isPluginVersionValid, isNpmPlugin import { PluginManager } from '../../lib/plugins/plugin-manager' import { isBooleanValid, isSafePath } from '../../helpers/custom-validators/misc' import { PluginModel } from '../../models/server/plugin' +import { InstallPlugin } from '../../../shared/models/plugins/install-plugin.model' const servePluginStaticDirectoryValidator = [ param('pluginName').custom(isPluginNameValid).withMessage('Should have a valid plugin name'), @@ -48,13 +49,25 @@ const listPluginsValidator = [ ] const installPluginValidator = [ - body('npmName').custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'), + body('npmName') + .optional() + .custom(isNpmPluginNameValid).withMessage('Should have a valid npm name'), + body('path') + .optional() + .custom(isSafePath).withMessage('Should have a valid safe path'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking installPluginValidator parameters', { parameters: req.body }) if (areValidationErrors(req, res)) return + const body: InstallPlugin = req.body + if (!body.path && !body.npmName) { + return res.status(400) + .json({ error: 'Should have either a npmName or a path' }) + .end() + } + return next() } ] diff --git a/server/models/server/plugin.ts b/server/models/server/plugin.ts index 60abaec65..226c08342 100644 --- a/server/models/server/plugin.ts +++ b/server/models/server/plugin.ts @@ -142,15 +142,17 @@ export class PluginModel extends Model { count: number, sort: string }) { + const { uninstalled = false } = options const query: FindAndCountOptions = { offset: options.start, limit: options.count, order: getSort(options.sort), - where: {} + where: { + uninstalled + } } if (options.type) query.where['type'] = options.type - if (options.uninstalled) query.where['uninstalled'] = options.uninstalled return PluginModel .findAndCountAll(query) diff --git a/server/tests/api/search/search-activitypub-video-channels.ts b/server/tests/api/search/search-activitypub-video-channels.ts index 8a008b8c6..d5f0a5457 100644 --- a/server/tests/api/search/search-activitypub-video-channels.ts +++ b/server/tests/api/search/search-activitypub-video-channels.ts @@ -67,6 +67,8 @@ describe('Test ActivityPub video channels search', function () { }) it('Should not find a remote video channel', async function () { + this.timeout(15000) + { const search = 'http://localhost:' + servers[ 1 ].port + '/video-channels/channel1_server3' const res = await searchVideoChannel(servers[ 0 ].url, search, servers[ 0 ].accessToken) diff --git a/server/tools/cli.ts b/server/tools/cli.ts index 2eec51aa4..67755022c 100644 --- a/server/tools/cli.ts +++ b/server/tools/cli.ts @@ -1,7 +1,8 @@ import { Netrc } from 'netrc-parser' import { getAppNumber, isTestInstance } from '../helpers/core-utils' import { join } from 'path' -import { getVideoChannel, root } from '../../shared/extra-utils' +import { root } from '../../shared/extra-utils/miscs/miscs' +import { getVideoChannel } from '../../shared/extra-utils/videos/video-channels' import { Command } from 'commander' import { VideoChannel, VideoPrivacy } from '../../shared/models/videos' @@ -64,7 +65,11 @@ function deleteSettings () { }) } -function getRemoteObjectOrDie (program: any, settings: Settings, netrc: Netrc) { +function getRemoteObjectOrDie ( + program: any, + settings: Settings, + netrc: Netrc +): { url: string, username: string, password: string } { if (!program['url'] || !program['username'] || !program['password']) { // No remote and we don't have program parameters: quit if (settings.remotes.length === 0 || Object.keys(netrc.machines).length === 0) { @@ -161,6 +166,13 @@ async function buildVideoAttributesFromCommander (url: string, command: Command, return videoAttributes } +function getServerCredentials (program: any) { + return Promise.all([ getSettings(), getNetrc() ]) + .then(([ settings, netrc ]) => { + return getRemoteObjectOrDie(program, settings, netrc) + }) +} + // --------------------------------------------------------------------------- export { @@ -172,6 +184,8 @@ export { writeSettings, deleteSettings, + getServerCredentials, + buildCommonVideoOptions, buildVideoAttributesFromCommander } diff --git a/server/tools/peertube-auth.ts b/server/tools/peertube-auth.ts index 1035d664a..d4ad56e47 100644 --- a/server/tools/peertube-auth.ts +++ b/server/tools/peertube-auth.ts @@ -1,8 +1,8 @@ import * as program from 'commander' import * as prompt from 'prompt' -import { getSettings, writeSettings, getNetrc } from './cli' -import { isHostValid } from '../helpers/custom-validators/servers' +import { getNetrc, getSettings, writeSettings } from './cli' import { isUserUsernameValid } from '../helpers/custom-validators/users' +import { getAccessToken, login } from '../../shared/extra-utils' const Table = require('cli-table') @@ -76,6 +76,14 @@ program } } }, async (_, result) => { + // Check credentials + try { + await getAccessToken(result.url, result.username, result.password) + } catch (err) { + console.error(err.message) + process.exit(-1) + } + await setInstance(result.url, result.username, result.password, program['default']) process.exit(0) diff --git a/server/tools/peertube-import-videos.ts b/server/tools/peertube-import-videos.ts index d7bb00e02..1f0350442 100644 --- a/server/tools/peertube-import-videos.ts +++ b/server/tools/peertube-import-videos.ts @@ -11,7 +11,7 @@ import * as prompt from 'prompt' import { remove } from 'fs-extra' import { sha256 } from '../helpers/core-utils' import { buildOriginallyPublishedAt, safeGetYoutubeDL } from '../helpers/youtube-dl' -import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getNetrc, getRemoteObjectOrDie, getSettings } from './cli' +import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials } from './cli' type UserInfo = { username: string @@ -36,27 +36,25 @@ command .option('-v, --verbose', 'Verbose mode') .parse(process.argv) -Promise.all([ getSettings(), getNetrc() ]) - .then(([ settings, netrc ]) => { - const { url, username, password } = getRemoteObjectOrDie(program, settings, netrc) +getServerCredentials(command) + .then(({ url, username, password }) => { + if (!program[ 'targetUrl' ]) { + console.error('--targetUrl field is required.') - if (!program[ 'targetUrl' ]) { - console.error('--targetUrl field is required.') + process.exit(-1) + } - process.exit(-1) - } + removeEndSlashes(url) + removeEndSlashes(program[ 'targetUrl' ]) - removeEndSlashes(url) - removeEndSlashes(program[ 'targetUrl' ]) + const user = { username, password } - const user = { username, password } - - run(url, user) - .catch(err => { - console.error(err) - process.exit(-1) - }) - }) + run(url, user) + .catch(err => { + console.error(err) + process.exit(-1) + }) + }) async function run (url: string, user: UserInfo) { if (!user.password) { diff --git a/server/tools/peertube-plugins.ts b/server/tools/peertube-plugins.ts new file mode 100644 index 000000000..d5e024383 --- /dev/null +++ b/server/tools/peertube-plugins.ts @@ -0,0 +1,162 @@ +import * as program from 'commander' +import { PluginType } from '../../shared/models/plugins/plugin.type' +import { getAccessToken } from '../../shared/extra-utils/users/login' +import { getMyUserInformation } from '../../shared/extra-utils/users/users' +import { installPlugin, listPlugins, uninstallPlugin } from '../../shared/extra-utils/server/plugins' +import { getServerCredentials } from './cli' +import { User, UserRole } from '../../shared/models/users' +import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model' +import { isAbsolute } from 'path' + +const Table = require('cli-table') + +program + .name('plugins') + .usage('[command] [options]') + +program + .command('list') + .description('List installed plugins') + .option('-u, --url ', 'Server url') + .option('-U, --username ', 'Username') + .option('-p, --password ', 'Password') + .option('-t, --only-themes', 'List themes only') + .option('-P, --only-plugins', 'List plugins only') + .action(() => pluginsListCLI()) + +program + .command('install') + .description('Install a plugin or a theme') + .option('-u, --url ', 'Server url') + .option('-U, --username ', 'Username') + .option('-p, --password ', 'Password') + .option('-P --path ', 'Install from a path') + .option('-n, --npm-name ', 'Install from npm') + .action((options) => installPluginCLI(options)) + +program + .command('uninstall') + .description('Uninstall a plugin or a theme') + .option('-u, --url ', 'Server url') + .option('-U, --username ', 'Username') + .option('-p, --password ', 'Password') + .option('-n, --npm-name ', 'NPM plugin/theme name') + .action(options => uninstallPluginCLI(options)) + +if (!process.argv.slice(2).length) { + program.outputHelp() +} + +program.parse(process.argv) + +// ---------------------------------------------------------------------------- + +async function pluginsListCLI () { + const { url, username, password } = await getServerCredentials(program) + const accessToken = await getAdminTokenOrDie(url, username, password) + + let type: PluginType + if (program['onlyThemes']) type = PluginType.THEME + if (program['onlyPlugins']) type = PluginType.PLUGIN + + const res = await listPlugins({ + url, + accessToken, + start: 0, + count: 100, + sort: 'name', + type + }) + const plugins: PeerTubePlugin[] = res.body.data + + const table = new Table({ + head: ['name', 'version', 'homepage'], + colWidths: [ 50, 10, 50 ] + }) + + for (const plugin of plugins) { + const npmName = plugin.type === PluginType.PLUGIN + ? 'peertube-plugin-' + plugin.name + : 'peertube-theme-' + plugin.name + + table.push([ + npmName, + plugin.version, + plugin.homepage + ]) + } + + console.log(table.toString()) + process.exit(0) +} + +async function installPluginCLI (options: any) { + if (!options['path'] && !options['npmName']) { + console.error('You need to specify the npm name or the path of the plugin you want to install.\n') + program.outputHelp() + process.exit(-1) + } + + if (options['path'] && !isAbsolute(options['path'])) { + console.error('Path should be absolute.') + process.exit(-1) + } + + const { url, username, password } = await getServerCredentials(options) + const accessToken = await getAdminTokenOrDie(url, username, password) + + try { + await installPlugin({ + url, + accessToken, + npmName: options['npmName'], + path: options['path'] + }) + } catch (err) { + console.error('Cannot install plugin.', err) + process.exit(-1) + return + } + + console.log('Plugin installed.') + process.exit(0) +} + +async function uninstallPluginCLI (options: any) { + if (!options['npmName']) { + console.error('You need to specify the npm name of the plugin/theme you want to uninstall.\n') + program.outputHelp() + process.exit(-1) + } + + const { url, username, password } = await getServerCredentials(options) + const accessToken = await getAdminTokenOrDie(url, username, password) + + try { + await uninstallPlugin({ + url, + accessToken, + npmName: options[ 'npmName' ] + }) + } catch (err) { + console.error('Cannot uninstall plugin.', err) + process.exit(-1) + return + } + + console.log('Plugin uninstalled.') + process.exit(0) +} + +async function getAdminTokenOrDie (url: string, username: string, password: string) { + const accessToken = await getAccessToken(url, username, password) + const resMe = await getMyUserInformation(url, accessToken) + const me: User = resMe.body + + if (me.role !== UserRole.ADMINISTRATOR) { + console.error('Cannot list plugins if you are not administrator.') + process.exit(-1) + } + + return accessToken +} diff --git a/server/tools/peertube-upload.ts b/server/tools/peertube-upload.ts index c00205e8f..d9f9a8ead 100644 --- a/server/tools/peertube-upload.ts +++ b/server/tools/peertube-upload.ts @@ -1,9 +1,9 @@ import * as program from 'commander' import { access, constants } from 'fs-extra' import { isAbsolute } from 'path' -import { getClient, login } from '../../shared/extra-utils' +import { getAccessToken } from '../../shared/extra-utils' import { uploadVideo } from '../../shared/extra-utils/' -import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getNetrc, getRemoteObjectOrDie, getSettings } from './cli' +import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getServerCredentials } from './cli' let command = program .name('upload') @@ -11,7 +11,6 @@ let command = program command = buildCommonVideoOptions(command) command - .option('-u, --url ', 'Server url') .option('-U, --username ', 'Username') .option('-p, --password ', 'Password') @@ -20,44 +19,28 @@ command .option('-f, --file ', 'Video absolute file path') .parse(process.argv) -Promise.all([ getSettings(), getNetrc() ]) - .then(([ settings, netrc ]) => { - const { url, username, password } = getRemoteObjectOrDie(program, settings, netrc) +getServerCredentials(command) + .then(({ url, username, password }) => { + if (!program[ 'videoName' ] || !program[ 'file' ]) { + if (!program[ 'videoName' ]) console.error('--video-name is required.') + if (!program[ 'file' ]) console.error('--file is required.') - if (!program[ 'videoName' ] || !program[ 'file' ]) { - if (!program[ 'videoName' ]) console.error('--video-name is required.') - if (!program[ 'file' ]) console.error('--file is required.') + process.exit(-1) + } - process.exit(-1) - } + if (isAbsolute(program[ 'file' ]) === false) { + console.error('File path should be absolute.') + process.exit(-1) + } - if (isAbsolute(program[ 'file' ]) === false) { - console.error('File path should be absolute.') - process.exit(-1) - } - - run(url, username, password).catch(err => { - console.error(err) - process.exit(-1) - }) - }) + run(url, username, password).catch(err => { + console.error(err) + process.exit(-1) + }) + }) async function run (url: string, username: string, password: string) { - const resClient = await getClient(url) - const client = { - id: resClient.body.client_id, - secret: resClient.body.client_secret - } - - const user = { username, password } - - let accessToken: string - try { - const res = await login(url, client, user) - accessToken = res.body.access_token - } catch (err) { - throw new Error('Cannot authenticate. Please check your username/password.') - } + const accessToken = await getAccessToken(url, username, password) await access(program[ 'file' ], constants.F_OK) diff --git a/server/tools/peertube.ts b/server/tools/peertube.ts index daa5586c3..e79a7e041 100644 --- a/server/tools/peertube.ts +++ b/server/tools/peertube.ts @@ -18,13 +18,10 @@ program .command('get-access-token', 'get a peertube access token', { noHelp: true }).alias('token') .command('watch', 'watch a video in the terminal ✩°。⋆').alias('w') .command('repl', 'initiate a REPL to access internals') + .command('plugins [action]', 'manage plugins on a local instance').alias('p') /* Not Yet Implemented */ program - .command('plugins [action]', - 'manage plugins on a local instance', - { noHelp: true } as program.CommandOptions - ).alias('p') .command('diagnostic [action]', 'like couple therapy, but for your instance', { noHelp: true } as program.CommandOptions diff --git a/shared/extra-utils/index.ts b/shared/extra-utils/index.ts index 9d0bbaa38..53ddaa681 100644 --- a/shared/extra-utils/index.ts +++ b/shared/extra-utils/index.ts @@ -11,6 +11,7 @@ export * from './server/follows' export * from './requests/requests' export * from './requests/check-api-params' export * from './server/servers' +export * from './server/plugins' export * from './videos/services' export * from './videos/video-playlists' export * from './users/users' diff --git a/shared/extra-utils/miscs/miscs.ts b/shared/extra-utils/miscs/miscs.ts index fb6430e4f..42250886c 100644 --- a/shared/extra-utils/miscs/miscs.ts +++ b/shared/extra-utils/miscs/miscs.ts @@ -8,7 +8,7 @@ import { pathExists, readFile } from 'fs-extra' import * as ffmpeg from 'fluent-ffmpeg' const expect = chai.expect -let webtorrent = new WebTorrent() +let webtorrent: WebTorrent.Instance function immutableAssign (target: T, source: U) { return Object.assign<{}, T, U>({}, target, source) @@ -27,6 +27,9 @@ function wait (milliseconds: number) { } function webtorrentAdd (torrent: string, refreshWebTorrent = false) { + const WebTorrent = require('webtorrent') + + if (!webtorrent) webtorrent = new WebTorrent() if (refreshWebTorrent === true) webtorrent = new WebTorrent() return new Promise(res => webtorrent.add(torrent, res)) diff --git a/shared/extra-utils/server/plugins.ts b/shared/extra-utils/server/plugins.ts new file mode 100644 index 000000000..6cd7cd17a --- /dev/null +++ b/shared/extra-utils/server/plugins.ts @@ -0,0 +1,125 @@ +import { makeGetRequest, makePostBodyRequest } from '../requests/requests' +import { PluginType } from '../../models/plugins/plugin.type' + +function listPlugins (parameters: { + url: string, + accessToken: string, + start?: number, + count?: number, + sort?: string, + type?: PluginType, + expectedStatus?: number +}) { + const { url, accessToken, start, count, sort, type, expectedStatus = 200 } = parameters + const path = '/api/v1/plugins' + + return makeGetRequest({ + url, + path, + token: accessToken, + query: { + start, + count, + sort, + type + }, + statusCodeExpected: expectedStatus + }) +} + +function getPlugin (parameters: { + url: string, + accessToken: string, + npmName: string, + expectedStatus?: number +}) { + const { url, accessToken, npmName, expectedStatus = 200 } = parameters + const path = '/api/v1/plugins/' + npmName + + return makeGetRequest({ + url, + path, + token: accessToken, + statusCodeExpected: expectedStatus + }) +} + +function getPluginSettings (parameters: { + url: string, + accessToken: string, + npmName: string, + expectedStatus?: number +}) { + const { url, accessToken, npmName, expectedStatus = 200 } = parameters + const path = '/api/v1/plugins/' + npmName + '/settings' + + return makeGetRequest({ + url, + path, + token: accessToken, + statusCodeExpected: expectedStatus + }) +} + +function getPluginRegisteredSettings (parameters: { + url: string, + accessToken: string, + npmName: string, + expectedStatus?: number +}) { + const { url, accessToken, npmName, expectedStatus = 200 } = parameters + const path = '/api/v1/plugins/' + npmName + '/registered-settings' + + return makeGetRequest({ + url, + path, + token: accessToken, + statusCodeExpected: expectedStatus + }) +} + +function installPlugin (parameters: { + url: string, + accessToken: string, + path?: string, + npmName?: string + expectedStatus?: number +}) { + const { url, accessToken, npmName, path, expectedStatus = 204 } = parameters + const apiPath = '/api/v1/plugins/install' + + return makePostBodyRequest({ + url, + path: apiPath, + token: accessToken, + fields: { npmName, path }, + statusCodeExpected: expectedStatus + }) +} + +function uninstallPlugin (parameters: { + url: string, + accessToken: string, + npmName: string + expectedStatus?: number +}) { + const { url, accessToken, npmName, expectedStatus = 204 } = parameters + const apiPath = '/api/v1/plugins/uninstall' + + return makePostBodyRequest({ + url, + path: apiPath, + token: accessToken, + fields: { npmName }, + statusCodeExpected: expectedStatus + }) +} + +export { + listPlugins, + installPlugin, + getPlugin, + uninstallPlugin, + getPluginSettings, + getPluginRegisteredSettings +} diff --git a/shared/extra-utils/server/servers.ts b/shared/extra-utils/server/servers.ts index 4c7d6862a..9167ebe5b 100644 --- a/shared/extra-utils/server/servers.ts +++ b/shared/extra-utils/server/servers.ts @@ -3,7 +3,7 @@ import { ChildProcess, exec, fork } from 'child_process' import { join } from 'path' import { root, wait } from '../miscs/miscs' -import { copy, readdir, readFile, remove } from 'fs-extra' +import { copy, pathExists, readdir, readFile, remove } from 'fs-extra' import { existsSync } from 'fs' import { expect } from 'chai' import { VideoChannel } from '../../models/videos' @@ -241,20 +241,22 @@ async function reRunServer (server: ServerInfo, configOverride?: any) { return server } -async function checkTmpIsEmpty (server: ServerInfo) { - return checkDirectoryIsEmpty(server, 'tmp') +function checkTmpIsEmpty (server: ServerInfo) { + return checkDirectoryIsEmpty(server, 'tmp', [ 'plugins-global.css' ]) } -async function checkDirectoryIsEmpty (server: ServerInfo, directory: string) { +async function checkDirectoryIsEmpty (server: ServerInfo, directory: string, exceptions: string[] = []) { const testDirectory = 'test' + server.internalServerNumber const directoryPath = join(root(), testDirectory, directory) - const directoryExists = existsSync(directoryPath) + const directoryExists = await pathExists(directoryPath) expect(directoryExists).to.be.true const files = await readdir(directoryPath) - expect(files).to.have.lengthOf(0) + const filtered = files.filter(f => exceptions.includes(f) === false) + + expect(filtered).to.have.lengthOf(0) } function killallServers (servers: ServerInfo[]) { diff --git a/shared/extra-utils/users/login.ts b/shared/extra-utils/users/login.ts index ddeb9df2a..f9bfb3cb3 100644 --- a/shared/extra-utils/users/login.ts +++ b/shared/extra-utils/users/login.ts @@ -1,6 +1,7 @@ import * as request from 'supertest' import { ServerInfo } from '../server/servers' +import { getClient } from '../server/clients' type Client = { id: string, secret: string } type User = { username: string, password: string } @@ -38,6 +39,23 @@ async function userLogin (server: Server, user: User, expectedStatus = 200) { return res.body.access_token as string } +async function getAccessToken (url: string, username: string, password: string) { + const resClient = await getClient(url) + const client = { + id: resClient.body.client_id, + secret: resClient.body.client_secret + } + + const user = { username, password } + + try { + const res = await login(url, client, user) + return res.body.access_token + } catch (err) { + throw new Error('Cannot authenticate. Please check your username/password.') + } +} + function setAccessTokensToServers (servers: ServerInfo[]) { const tasks: Promise[] = [] @@ -55,6 +73,7 @@ export { login, serverLogin, userLogin, + getAccessToken, setAccessTokensToServers, Server, Client, diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts index 1c39881d6..5fa8cde0c 100644 --- a/shared/extra-utils/users/users.ts +++ b/shared/extra-utils/users/users.ts @@ -1,11 +1,11 @@ import * as request from 'supertest' -import { makeGetRequest, makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' - -import { UserCreate, UserRole } from '../../index' +import { makePostBodyRequest, makePutBodyRequest, updateAvatarRequest } from '../requests/requests' import { NSFWPolicyType } from '../../models/videos/nsfw-policy.type' -import { ServerInfo, userLogin } from '..' import { UserAdminFlag } from '../../models/users/user-flag.model' import { UserRegister } from '../../models/users/user-register.model' +import { UserRole } from '../../models/users/user-role' +import { ServerInfo } from '../server/servers' +import { userLogin } from './login' type CreateUserArgs = { url: string, accessToken: string, diff --git a/shared/extra-utils/videos/video-channels.ts b/shared/extra-utils/videos/video-channels.ts index 3e79cf15a..053842331 100644 --- a/shared/extra-utils/videos/video-channels.ts +++ b/shared/extra-utils/videos/video-channels.ts @@ -1,8 +1,10 @@ import * as request from 'supertest' -import { VideoChannelCreate, VideoChannelUpdate } from '../../models/videos' +import { VideoChannelUpdate } from '../../models/videos/channel/video-channel-update.model' +import { VideoChannelCreate } from '../../models/videos/channel/video-channel-create.model' import { makeGetRequest, updateAvatarRequest } from '../requests/requests' -import { getMyUserInformation, ServerInfo } from '..' -import { User } from '../..' +import { ServerInfo } from '../server/servers' +import { User } from '../../models/users/user.model' +import { getMyUserInformation } from '../users/users' function getVideoChannelsList (url: string, start: number, count: number, sort?: string) { const path = '/api/v1/video-channels' diff --git a/shared/models/plugins/install-plugin.model.ts b/shared/models/plugins/install-plugin.model.ts index 03d87fe57..b1b46fa08 100644 --- a/shared/models/plugins/install-plugin.model.ts +++ b/shared/models/plugins/install-plugin.model.ts @@ -1,3 +1,4 @@ export interface InstallPlugin { - npmName: string + npmName?: string + path?: string }