Browse Source

WIP plugins: move plugin CLI in peertube script

Install/uninstall/list plugins remotely
Chocobozzz 4 years ago
parent
commit
8d2be0ed7b

+ 1 - 2
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",

+ 15 - 0
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

+ 0 - 39
scripts/plugin/install.ts

@@ -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'])
-}

+ 0 - 27
scripts/plugin/uninstall.ts

@@ -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)
-}

+ 12 - 4
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

+ 0 - 30
server/controllers/themes.ts

@@ -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))
-}

+ 1 - 7
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<T, U> (func: (arg1: T, arg2: U, cb: (err: any) => vo
 const pseudoRandomBytesPromise = promisify1<number, Buffer>(pseudoRandomBytes)
 const createPrivateKey = promisify1<number, { key: string }>(pem.createPrivateKey)
 const getPublicKey = promisify1<string, { publicKey: string }>(pem.getPublicKey)
-const bcryptComparePromise = promisify2<any, string, boolean>(bcrypt.compare)
-const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
-const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
 const createTorrentPromise = promisify2<string, any, any>(createTorrent)
 const execPromise2 = promisify2<string, any, string>(exec)
 const execPromise = promisify1<string, string>(exec)
@@ -287,13 +283,11 @@ export {
 
   promisify0,
   promisify1,
+  promisify2,
 
   pseudoRandomBytesPromise,
   createPrivateKey,
   getPublicKey,
-  bcryptComparePromise,
-  bcryptGenSaltPromise,
-  bcryptHashPromise,
   createTorrentPromise,
   execPromise2,
   execPromise

+ 3 - 1
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
   }

+ 8 - 1
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<any, string, boolean>(bcrypt.compare)
+const bcryptGenSaltPromise = promisify1<number, string>(bcrypt.genSalt)
+const bcryptHashPromise = promisify2<any, string | number, string>(bcrypt.hash)
 
 const httpSignature = require('http-signature')
 
@@ -147,3 +152,5 @@ export {
   cryptPassword,
   signJsonLDObject
 }
+
+// ---------------------------------------------------------------------------

+ 14 - 1
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()
   }
 ]

+ 4 - 2
server/models/server/plugin.ts

@@ -142,15 +142,17 @@ export class PluginModel extends Model<PluginModel> {
     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)

+ 2 - 0
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)

+ 16 - 2
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
 }

+ 10 - 2
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)

+ 16 - 18
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) {

+ 162 - 0
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 <url>', 'Server url')
+  .option('-U, --username <username>', 'Username')
+  .option('-p, --password <token>', '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 <url>', 'Server url')
+  .option('-U, --username <username>', 'Username')
+  .option('-p, --password <token>', 'Password')
+  .option('-P --path <path>', 'Install from a path')
+  .option('-n, --npm-name <npmName>', 'Install from npm')
+  .action((options) => installPluginCLI(options))
+
+program
+  .command('uninstall')
+  .description('Uninstall a plugin or a theme')
+  .option('-u, --url <url>', 'Server url')
+  .option('-U, --username <username>', 'Username')
+  .option('-p, --password <token>', 'Password')
+  .option('-n, --npm-name <npmName>', '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
+}

+ 19 - 36
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 <url>', 'Server url')
   .option('-U, --username <username>', 'Username')
   .option('-p, --password <token>', 'Password')
@@ -20,44 +19,28 @@ command
   .option('-f, --file <file>', 'Video absolute file path')
   .parse(process.argv)
 
-Promise.all([ getSettings(), getNetrc() ])
-       .then(([ settings, netrc ]) => {
-         const { url, username, password } = getRemoteObjectOrDie(program, settings, netrc)
-
-         if (!program[ 'videoName' ] || !program[ 'file' ]) {
-           if (!program[ 'videoName' ]) console.error('--video-name is required.')
-           if (!program[ 'file' ]) console.error('--file is required.')
+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.')
 
-           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)
 

+ 1 - 4
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

+ 1 - 0
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'

+ 4 - 1
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 <T, U> (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<WebTorrent.Torrent>(res => webtorrent.add(torrent, res))

+ 125 - 0
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
+}

+ 8 - 6
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[]) {

+ 19 - 0
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<any>[] = []
 
@@ -55,6 +73,7 @@ export {
   login,
   serverLogin,
   userLogin,
+  getAccessToken,
   setAccessTokensToServers,
   Server,
   Client,

+ 4 - 4
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,

+ 5 - 3
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'

+ 2 - 1
shared/models/plugins/install-plugin.model.ts

@@ -1,3 +1,4 @@
 export interface InstallPlugin {
-  npmName: string
+  npmName?: string
+  path?: string
 }