@intocode-io/fireliff-cli
Version:
Command line interface for building LIFF app on Firebase
585 lines (528 loc) • 16.3 kB
JavaScript
import { mkdir, existsSync } from 'fs';
import { EOL } from 'os';
import * as path from 'path';
import './colors-set-theme';
import { FLIFFError } from './fliff-error';
import { FunctionsConfig } from './functions-config';
import { LIFFConfig } from './liff-config';
import { LIFFAddRequest } from './liff-add-request';
import { LIFFDeleteRequest } from './liff-delete-request';
import { LIFFGetRequest } from './liff-get-request';
import { LIFFUpdateRequest } from './liff-update-request';
import { OAuthIssueTokenRequest } from './oauth-issue-token-request';
import { OAuthRevokeTokenRequest } from './oauth-revoke-token-request';
const AddRequiredNameAndUrl =
`Command ${'fliff add'.prompt} required name and url options`.warn +
EOL +
`Try re-run with option ${'--name <viewName>'.input} AND ${
'--url <viewURL>'.input
}`.help;
const ConfigRequiredIdSecretOrName =
`Failed to configure channel. ${
'fliff config'.prompt
} required id, secret or token options.`.error +
EOL +
`Try re-run with option ${'--id <channelId>'.input} OR ${
'--secret <channelSecret>'.input
} OR ${'--token <channelToken>'.input}`.help;
const DeleteRequiredIdOrName =
`Command ${'fliff delete'.prompt} required LIFF ID or name option`.warn +
EOL +
`Try re-run with option ${'--id <liffId>'.input} OR ${
'--name <viewName>'.input
}`.help;
const FailedToAddLIFF = `Failed to add LIFF view`.error;
const FailedToInitPathExists = (initPath) =>
`${initPath} already exists`.error +
EOL +
`Please manually delete ${initPath} and try again`.help;
const FailedToRetrieveIdUsingName = `Failed to retrieve LIFF ID using view name`
.error;
const FailedToRetrieveNameUsingId = `Failed to retrieve view name using LIFF ID`
.error;
const FailedToSetView = `Failed to set view in Functions configuration`.error;
const FailedToUnsetViews = `Failed to unset view(s) in Functions configuration`
.error;
const RevokeTokenRequiredAccessToken =
`Command ${'fliff token --revoke'.prompt} required access token.`.warn +
EOL +
`Try re-run with access token ${'fliff token --revoke <accessToken>'.input}`
.help;
const IssueTokenRequiredChannelIdAndSecret =
`Command ${
'fliff token'.prompt
} required Channel ID and Secret to be configured first`.error +
EOL +
`Try run ${
'fliff config --id <channelId> --secret <channelSecret>'.input
} to configure before re-run ${'fliff token'.prompt} again.`.help;
const TokenRequiredIssueOrRevoke =
`Command ${'fliff token'.prompt} required issue or revoke options.`.error +
EOL +
`Try re-run with option ${'--issue'.input} OR ${'--revoke'.input}`.help;
const UpdateRequiredIdOrName =
`Command ${'fliff update'.prompt} required LIFF ID or name option`.warn +
EOL +
`Try re-run with option ${'--id <liffId>'.input} OR ${
'--name <viewName>'.input
}`.help;
const WarnPublicHostingConfig = (current, preferred) =>
`firebase.json currently set hosting.public to ${current.error}`.warn +
EOL +
`Please change hosting.public to ${preferred.info}`.warn;
export class FLIFF {
static get ErrorMessages() {
return {
AddRequiredNameAndUrl,
ConfigRequiredIdSecretOrName,
DeleteRequiredIdOrName,
FailedToAddLIFF,
FailedToInitPathExists,
FailedToRetrieveIdUsingName,
FailedToRetrieveNameUsingId,
FailedToSetView,
FailedToUnsetViews,
RevokeTokenRequiredAccessToken,
IssueTokenRequiredChannelIdAndSecret,
TokenRequiredIssueOrRevoke,
UpdateRequiredIdOrName,
WarnPublicHostingConfig,
};
}
constructor() {}
init(initPath) {
return new Promise((resolve, reject) => {
const fbjsonPath = path.resolve(initPath, '../firebase.json');
const distPath = `${path.basename(initPath)}/dist`;
mkdir(initPath, (errMakeDir) => {
if (errMakeDir) {
switch (errMakeDir.code) {
case 'EEXIST':
return reject(
new FLIFFError(
FLIFF.ErrorMessages.FailedToInitPathExists(initPath)
)
);
default:
return reject(errMakeDir);
}
}
const copy = require('recursive-copy');
copy(
path.resolve(__dirname, '../templates/web-views'),
initPath,
{
junk: false,
dot: true,
filter: [
'*',
'src/**/*',
'!.cache',
'!package-lock.json',
'!dist',
'!node_modules',
],
},
(errCopy, rsCopy) => {
if (errCopy) {
return reject(errCopy);
}
rsCopy.sort((a, b) =>
a.dest > b.dest ? 1 : b.dest > a.dest ? -1 : 0
);
const files = rsCopy.map((file) => file.dest);
let message = '';
if (existsSync(fbjsonPath)) {
const fbjson = require(fbjsonPath);
if (fbjson.hosting && fbjson.hosting.public !== distPath) {
message = FLIFF.ErrorMessages.WarnPublicHostingConfig(
fbjson.hosting.public,
distPath
);
}
}
return resolve({ files, message });
}
);
});
});
}
installNow(initPath) {
return new Promise((resolve, reject) => {
this.prompt = require('prompt');
this.prompt.message = ''; // Workaround: Remove annoying prompt: prefix
this.prompt.start();
this.prompt.get(
[
{
name: 'installNow',
message: 'Do you want to install node modules now? [yes/no]',
validator: /y[es]*|n[o]?/,
default: 'yes',
},
],
(errPrompt, rsPrompt) => {
if (errPrompt) {
return reject(errPrompt);
}
if (['yes', 'y'].indexOf(rsPrompt.installNow.toLowerCase()) > -1) {
const spawn = require('child_process').spawn;
const cmdInstall = spawn('npm', ['i', '--loglevel=error'], {
cwd: initPath,
});
console.log(
`Installing node modules in ${initPath}. Please wait...`
);
cmdInstall.stderr.on('data', (data) => reject(data.toString()));
cmdInstall.on('exit', (code) => {
if (code === 0) {
return resolve(true);
}
return reject(code);
});
}
}
);
});
}
async add(options) {
if (!options.name || !options.url) {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.AddRequiredNameAndUrl)
);
}
if (!options.type) {
options.type = 'full';
}
const name = options.name.toLowerCase().replace(/\s/g, '_');
const description = options.description
? options.description
: options.name;
const req = new LIFFAddRequest({
accessToken: FunctionsConfig.AccessToken,
});
const data = {
view: {
type: options.type,
url: options.url,
},
description,
};
let res;
if (options.ble) {
data.features = {
ble: options.ble.toLowerCase() == 'false' ? false : true,
};
}
try {
res = await req.send(data);
} catch (error) {
if (
error.response &&
error.response.data &&
error.response.data.message
) {
return Promise.reject(
new FLIFFError(error.response.data.message.error)
);
} else {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.FailedToAddLIFF)
);
}
}
try {
return await LIFFConfig.setView(name, res.data.liffId);
} catch (error) {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.FailedToSetView)
);
}
}
async config(options) {
let result = {};
if (!options.id && !options.secret && !options.token) {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.ConfigRequiredIdSecretOrName)
);
} else {
result[FunctionsConfig.SingleChannelGroup] = {};
}
if (options.id) {
try {
await FunctionsConfig.set(
`${FunctionsConfig.SingleChannelGroup}.${FunctionsConfig.ChannelIdName}`,
options.id
);
result[FunctionsConfig.SingleChannelGroup][
FunctionsConfig.ChannelIdName
] = options.id;
} catch (errId) {
return Promise.reject(errId);
}
}
if (options.secret) {
try {
await FunctionsConfig.set(
`${FunctionsConfig.SingleChannelGroup}.${FunctionsConfig.ChannelSecretName}`,
options.secret
);
result[FunctionsConfig.SingleChannelGroup][
FunctionsConfig.ChannelSecretName
] = options.secret;
} catch (errSecret) {
return Promise.reject(errSecret);
}
}
if (options.token) {
try {
await FunctionsConfig.set(
`${FunctionsConfig.SingleChannelGroup}.${FunctionsConfig.AccessTokenName}`,
options.token
);
result[FunctionsConfig.SingleChannelGroup][
FunctionsConfig.AccessTokenName
] = options.token;
} catch (errToken) {
return Promise.reject(errToken);
}
}
return result;
}
async token(options) {
if (!options.issue && options.revoke === undefined) {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.TokenRequiredIssueOrRevoke)
);
}
if (options.issue === true) {
if (!FunctionsConfig.ChannelId || !FunctionsConfig.ChannelSecret) {
return Promise.reject(
new FLIFFError(
FLIFF.ErrorMessages.IssueTokenRequiredChannelIdAndSecret
)
);
}
try {
const req = new OAuthIssueTokenRequest();
const res = await req.send(
FunctionsConfig.ChannelId,
FunctionsConfig.ChannelSecret
);
const tokenData =
res.status === 200
? {
accessToken: res.data.access_token,
expiresIn: res.data.expires_in,
type: res.data.token_type,
}
: false;
if (tokenData) {
if (options.save === true) {
await FunctionsConfig.set(
`${FunctionsConfig.SingleChannelGroup}.${FunctionsConfig.AccessTokenName}`,
tokenData.accessToken
);
return tokenData;
}
return tokenData;
} else {
return Promise.reject(new FLIFFError(res.statusText));
}
} catch (error) {
if (
error.response &&
error.response.data &&
error.response.data.error_description
) {
return Promise.reject(error.response.data.error_description);
} else {
return Promise.reject(error);
}
}
} else if (options.revoke !== undefined) {
if (options.revoke === null) {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.RevokeTokenRequiredAccessToken)
);
}
try {
const req = new OAuthRevokeTokenRequest();
const res = await req.send(options.revoke);
return res.status === 200
? true
: Promise.reject(new FLIFFError(res.statusText));
} catch (error) {
if (
error.response &&
error.response.data &&
error.response.data.error_description
) {
return Promise.reject(error.response.data.error_description);
} else {
return Promise.reject(error);
}
}
}
}
async delete(options) {
if (!options.id && !options.name) {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.DeleteRequiredIdOrName)
);
}
if (!options.id) {
options.id = await LIFFConfig.getViewIdByName(
options.name,
FunctionsConfig.config
);
if (typeof options.id !== 'string') {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.FailedToRetrieveIdUsingName)
);
}
}
if (!options.name) {
options.name = await LIFFConfig.getViewNameById(
options.id,
FunctionsConfig.config
);
if (typeof options.name !== 'string') {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.FailedToRetrieveNameUsingId)
);
}
}
try {
const req = new LIFFDeleteRequest({
accessToken: FunctionsConfig.AccessToken,
});
await req.send(options.id);
} catch (error) {
if (error.response && error.response.data) {
if (error.response.data.message !== 'not found') {
return Promise.reject(error.response.data.error);
}
} else {
return Promise.reject(error);
}
}
try {
const viewNames = await LIFFConfig.getViewNamesById(
options.id,
FunctionsConfig.config
);
await Promise.all(
viewNames.map((viewName) => LIFFConfig.unsetView(viewName))
);
return viewNames;
} catch (error) {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.FailedToUnsetViews)
);
}
}
async update(options) {
let data = {};
if (!options.id && !options.name) {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.UpdateRequiredIdOrName)
);
}
if (!options.id) {
options.id = await LIFFConfig.getViewIdByName(
options.name,
FunctionsConfig.config
);
if (typeof options.id !== 'string') {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.FailedToRetrieveIdUsingName)
);
}
}
if (!options.name) {
options.name = await LIFFConfig.getViewNameById(
options.id,
FunctionsConfig.config
);
if (typeof options.name !== 'string') {
return Promise.reject(
new FLIFFError(FLIFF.ErrorMessages.FailedToRetrieveNameUsingId)
);
}
}
if (options.type || options.url) {
data.view = {};
if (options.type) {
data.view.type = options.type;
}
if (options.url) {
data.view.url = options.url;
}
}
if (options.description) {
data.description = options.description;
}
if (options.ble) {
data.features = {
ble: options.ble.toLowerCase() == 'false' ? false : true,
};
}
try {
const req = new LIFFUpdateRequest({
accessToken: FunctionsConfig.AccessToken,
});
return await req.send(options.id, data);
} catch (error) {
if (
error.response &&
error.response.data &&
error.response.data.message
) {
return Promise.reject(error.response.data.message.error);
} else {
return Promise.reject(error);
}
}
}
async get(options) {
try {
const req = new LIFFGetRequest({
accessToken: FunctionsConfig.AccessToken,
});
const res = await req.send();
return res.data.apps.map((app) => {
const views = Object.keys(FunctionsConfig.config.views).filter(
(key) => {
return FunctionsConfig.config.views[key] === app.liffId;
}
);
const row = {
View: views.join(', '),
'LIFF ID': app.liffId,
Type: app.view.type,
URL: app.view.url,
};
if (options.detail) {
row.Description = app.description || '';
row.BLE = app.features && app.features.ble ? '\u2713' : '\u2613';
}
return row;
});
} catch (error) {
if (
error.response &&
error.response.data &&
error.response.data.message
) {
if (error.response.data.message === 'no apps') {
return 'LIFF app not found'.info;
} else {
return Promise.reject(error.response.data.message.error);
}
} else {
return Promise.reject(error);
}
}
}
}