submit.ts 6.67 KB
Newer Older
Bradley Hilton's avatar
Bradley Hilton committed
1 2 3 4 5 6 7 8 9 10 11
import { Command, flags } from '@oclif/command';
import chalk from 'chalk';
import cli from 'cli-ux';
import * as FormData from 'form-data';
import * as fs from 'fs';
import * as fuzzy from 'fuzzy';
import * as inquirer from 'inquirer';
import fetch from 'node-fetch';
import { Response } from 'node-fetch';

import { AppCompiler, AppPackager, FolderDetails, VariousUtils } from '../misc';
12
import { CloudAuth } from '../misc/cloudAuth';
Bradley Hilton's avatar
Bradley Hilton committed
13 14 15 16 17 18

export default class Submit extends Command {
    public static description = 'submits an App to the Marketplace for review';

    public static flags = {
        help: flags.help({ char: 'h' }),
19
        update: flags.boolean({ description: 'submits an update instead of creating one' }),
Bradley Hilton's avatar
Bradley Hilton committed
20 21 22 23 24 25 26 27 28 29 30 31
        categories: flags.string({
            char: 'c',
            description: 'a comma separated list of the categories for the App',
        }),
    };

    public async run() {
        inquirer.registerPrompt('checkbox-plus', require('inquirer-checkbox-plus-prompt'));

        const { flags } = this.parse(Submit);

        //#region app packaging
32
        cli.action.start(`${chalk.green('packaging')} your app`);
Bradley Hilton's avatar
Bradley Hilton committed
33 34 35 36 37 38 39 40 41 42 43 44 45 46

        const fd = new FolderDetails(this);

        try {
            await fd.readInfoFile();
        } catch (e) {
            this.error(e.message);
            return;
        }

        const compiler = new AppCompiler(this, fd);
        const report = compiler.logDiagnostics();

        if (!report.isValid) {
47
            this.error('TypeScript compiler error(s) occurred');
Bradley Hilton's avatar
Bradley Hilton committed
48 49 50 51 52 53 54 55 56 57 58
            this.exit(1);
            return;
        }

        const packager = new AppPackager(this, fd);
        const zipName = await packager.zipItUp();

        cli.action.stop('packaged!');
        //#endregion

        //#region fetching categories
59
        cli.action.start(`${chalk.green('fetching')} the available categories`);
Bradley Hilton's avatar
Bradley Hilton committed
60 61 62 63 64 65 66

        const categories = await VariousUtils.fetchCategories();

        cli.action.stop('fetched!');
        //#endregion

        //#region asking for information
67
        const cloudAuth = new CloudAuth();
68
        const hasToken = await cloudAuth.hasToken();
69
        let email = '';
70
        if (!hasToken) {
71 72 73 74 75
            const cloudAccount: any = await inquirer.prompt([{
                type: 'confirm',
                name: 'hasAccount',
                message: 'Have you logged into our Publisher Portal?',
                default: false,
Bradley Hilton's avatar
Bradley Hilton committed
76 77
            }]);

78 79
            if (cloudAccount.hasAccount) {
                try {
80
                    cli.action.start(chalk.green('*') + ' ' + chalk.gray('waiting for authorization...'));
81
                    await cloudAuth.executeAuthFlow();
82
                    cli.action.stop(chalk.green('success!'));
83
                } catch (e) {
84
                    cli.action.stop(chalk.red('failed to authenticate.'));
85
                    return;
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
                }
            } else {
                const result: any = await inquirer.prompt([{
                    type: 'input',
                    name: 'email',
                    message: 'What is the publisher\'s email address?',
                    validate: (answer: string) => {
                        const regex = /^[^@\s]+@[^@\s]+\.[^@\s]+$/g;

                        return regex.test(answer);
                    },
                }]);

                email = result.email;
            }
Bradley Hilton's avatar
Bradley Hilton committed
101 102
        }

103 104 105 106 107 108 109 110 111 112 113
        if (typeof flags.update === 'undefined') {
            const isNewApp = await inquirer.prompt([{
                type: 'confirm',
                name: 'isNew',
                message: 'Is this a new App?',
                default: true,
            }]);

            flags.update = !(isNewApp as any).isNew;
        }

Bradley Hilton's avatar
Bradley Hilton committed
114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
        let selectedCategories = new Array<string>();
        if (flags.categories) {
            selectedCategories = flags.categories.split(',');
        } else {
            const result = await inquirer.prompt([{
                type: 'checkbox-plus',
                name: 'categories',
                message: 'Please select the categories which apply to this App?',
                pageSize: 10,
                highlight: true,
                searchable: true,
                validate: (answer: Array<string>) => {
                    if (answer.length === 0) {
                        return 'You must choose at least one color.';
                    }

                    return true;
                },
                // tslint:disable:promise-function-async
                source: (answersSoFar: object, input: string) => {
                    input = input || '';

                    return new Promise((resolve) => {
                        const fuzzyResult = fuzzy.filter(input, categories, {
                            extract: (item) => item.name,
                        });

                        const data = fuzzyResult.map((element) => {
                            return element.original;
                        });

                        resolve(data);
                    });
                },
            }] as any);
            selectedCategories = (result as any).categories;
        }

        const confirmSubmitting = await inquirer.prompt([{
            type: 'confirm',
            name: 'submit',
            message: 'Are you ready to submit?',
            default: false,
        }]);

        if (!(confirmSubmitting as any).submit) {
            return;
        }
        //#endregion

164
        cli.action.start(`${chalk.green('submitting')} your app`);
Bradley Hilton's avatar
Bradley Hilton committed
165 166 167 168 169

        const data = new FormData();
        data.append('app', fs.createReadStream(fd.mergeWithFolder(zipName)));
        data.append('categories', JSON.stringify(selectedCategories));

170 171 172 173 174 175
        if (email) {
            data.append('email', email);
        }

        const token = await cloudAuth.getToken();
        await this.asyncSubmitData(data, flags, fd, token);
Bradley Hilton's avatar
Bradley Hilton committed
176 177 178 179 180

        cli.action.stop('submitted!');
    }

    // tslint:disable:promise-function-async
181 182 183
    // tslint:disable-next-line:max-line-length
    private async asyncSubmitData(data: FormData, flags: { [key: string]: any }, fd: FolderDetails, token: string): Promise<any> {
        let url = 'https://marketplace-beta.rocket.chat/v1/apps';
184
        if (flags.update) {
185 186 187 188 189 190
            url += `/${fd.info.id}`;
        }

        const headers: { [key: string]: string } = {};
        if (token) {
            headers.Authorization = 'Bearer ' + token;
191
        }
192

193
        const res: Response = await fetch(url, {
Bradley Hilton's avatar
Bradley Hilton committed
194 195
            method: 'POST',
            body: data,
196
            headers,
Bradley Hilton's avatar
Bradley Hilton committed
197 198 199 200
        });

        if (res.status !== 200) {
            const result = await res.json();
201
            throw new Error(`Failed to submit the App. Error code ${result.code}: ${result.error}`);
Bradley Hilton's avatar
Bradley Hilton committed
202 203 204 205 206
        } else {
            return res.json();
        }
    }
}