Commit d8178347 authored by Gabriel Delavald's avatar Gabriel Delavald Committed by Bradley Hilton
Browse files

Refactor the dev-environment (#10)

* Remove server/site folders and tasks related

* Fixes todo-app and adds basic documentation

* Create command to create apps

* Improves safety on create-app name and adds documentation regarding it

* Creates UUID automatically and uses pascalcase for class name

* Checks for numbers on app name and some style changes

* Prevent numbers on app name

* Adds deploy command, still WIP

* Dynamically add set the required api version. Add a giphy app

* Switch to use carets for required api version

* Fix the Giphy execute preview

* Make the search trim so can empty (full of spaces) search doesn't happen

* Actually really use random for all empty spaces searches
parent 37f249be
import { IAppActivationBridge } from '@rocket.chat/apps-engine/server/bridges';
import { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp';
import { AppStatus } from '@rocket.chat/apps-ts-definition/AppStatus';
export class ServerAppActivationBridge implements IAppActivationBridge {
public async appAdded(app: ProxiedApp): Promise<void> {
console.log(`The App ${ app.getName() } (${ app.getID() }) has been added.`);
}
public async appUpdated(app: ProxiedApp): Promise<void> {
console.log(`The App ${ app.getName() } (${ app.getID() }) has been updated.`);
}
public async appRemoved(app: ProxiedApp): Promise<void> {
console.log(`The App ${ app.getName() } (${ app.getID() }) has been removed.`);
}
public async appStatusChanged(app: ProxiedApp, status: AppStatus): Promise<void> {
console.log(`The App ${ app.getName() } (${ app.getID() }) status has changed to: ${ status }`);
}
}
import { ServerAppActivationBridge } from './activation';
import { ServerCommandBridge } from './command';
import { ServerEnvironmentalVariableBridge } from './environmental';
import { ServerSettingBridge } from './settings';
import {
AppBridges,
IAppActivationBridge,
IAppCommandBridge,
IAppDetailChangesBridge,
IEnvironmentalVariableBridge,
IHttpBridge,
IListenerBridge,
IMessageBridge,
IPersistenceBridge,
IRoomBridge,
IServerSettingBridge,
IUserBridge,
} from '@rocket.chat/apps-engine/server/bridges';
export class ServerAppBridges extends AppBridges {
private readonly cmdBridge: ServerCommandBridge;
private readonly setsBridge: ServerSettingBridge;
private readonly envBridge: ServerEnvironmentalVariableBridge;
private readonly actsBridge: ServerAppActivationBridge;
constructor() {
super();
this.cmdBridge = new ServerCommandBridge();
this.setsBridge = new ServerSettingBridge();
this.envBridge = new ServerEnvironmentalVariableBridge();
this.actsBridge = new ServerAppActivationBridge();
}
public getCommandBridge(): ServerCommandBridge {
return this.cmdBridge;
}
public getServerSettingBridge(): IServerSettingBridge {
return this.setsBridge;
}
public getEnvironmentalVariableBridge(): IEnvironmentalVariableBridge {
return this.envBridge;
}
public getHttpBridge(): IHttpBridge {
throw new Error('Method not implemented.');
}
public getMessageBridge(): IMessageBridge {
throw new Error('Method not implemented.');
}
public getPersistenceBridge(): IPersistenceBridge {
throw new Error('Method not implemented.');
}
public getAppActivationBridge(): IAppActivationBridge {
return this.actsBridge;
}
public getRoomBridge(): IRoomBridge {
throw new Error('Method not implemented.');
}
public getUserBridge(): IUserBridge {
throw new Error('Method not implemented.');
}
public getAppDetailChangesBridge(): IAppDetailChangesBridge {
throw new Error('Method not implemented.');
}
public getListenerBridge(): IListenerBridge {
throw new Error('Method not implemented.');
}
}
import { IAppCommandBridge } from '@rocket.chat/apps-engine/server/bridges';
import { ISlashCommand, SlashCommandContext } from '@rocket.chat/apps-ts-definition/slashcommands';
export class ServerCommandBridge implements IAppCommandBridge {
private commands: Map<string, ISlashCommand>;
constructor() {
this.commands = new Map<string, ISlashCommand>();
}
public getCommands(): Array<string> {
return Array.from(this.commands.keys());
}
public doesCommandExist(command: string, appId: string): boolean {
console.log('Checking if the command exists:', command);
return this.commands.has(command);
}
public enableCommand(command: string, appId: string): void {
console.log(`Enabling the command "${command}" per request of the app: ${appId}`);
}
public disableCommand(command: string, appId: string): void {
console.log(`Disabling the command "${command}" per request of the app: ${appId}`);
}
public modifyCommand(command: ISlashCommand, appId: string): void {
throw new Error('Not implemented.');
}
// tslint:disable-next-line:max-line-length
public registerCommand(command: ISlashCommand, appId: string): void {
if (this.commands.has(command.command)) {
throw new Error(`Command "${command.command}" has already been registered.`);
}
this.commands.set(command.command, command);
console.log(`Registered the command "${command.command}".`);
}
public unregisterCommand(command: string, appId: string): void {
const removed = this.commands.delete(command);
if (removed) {
console.log(`Unregistered the command "${command}".`);
}
}
}
import { IEnvironmentalVariableBridge } from '@rocket.chat/apps-engine/server/bridges';
export class ServerEnvironmentalVariableBridge implements IEnvironmentalVariableBridge {
public getValueByName(envVarName: string, appId: string): Promise<string> {
throw new Error('Method not implemented.');
}
public isReadable(envVarName: string, appId: string): Promise<boolean> {
throw new Error('Method not implemented.');
}
public isSet(envVarName: string, appId: string): Promise<boolean> {
throw new Error('Method not implemented.');
}
}
import { IServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges';
import { ISetting } from '@rocket.chat/apps-ts-definition/settings';
export class ServerSettingBridge implements IServerSettingBridge {
public getAll(appId: string): Promise<Array<ISetting>> {
throw new Error('Method not implemented.');
}
public getOneById(id: string, appId: string): Promise<ISetting> {
throw new Error('Method not implemented.');
}
public hideGroup(name: string): Promise<void> {
throw new Error('Method not implemented.');
}
public hideSetting(id: string): Promise<void> {
throw new Error('Method not implemented.');
}
public isReadableById(id: string, appId: string): Promise<boolean> {
throw new Error('Method not implemented.');
}
public updateOne(setting: ISetting, appId: string): Promise<void> {
throw new Error('Method not implemented.');
}
}
import { AppConsole, ILoggerStorageEntry } from '@rocket.chat/apps-engine/server/logging';
import { AppLogStorage, IAppLogStorageFindOptions, IAppStorageItem } from '@rocket.chat/apps-engine/server/storage';
import * as Datastore from 'nedb';
export class ServerAppLogStorage extends AppLogStorage {
private db: Datastore;
constructor() {
super('nedb');
this.db = new Datastore({ filename: '.server-data/app-logs.nedb', autoload: true });
}
public find(query: { [field: string]: any; },
options?: IAppLogStorageFindOptions): Promise<Array<ILoggerStorageEntry>> {
throw new Error('Method not implemented.');
}
public storeEntries(appId: string, logger: AppConsole): Promise<ILoggerStorageEntry> {
return new Promise((resolve, reject) => {
const item = AppConsole.toStorageEntry(appId, logger);
this.db.insert(item, (err: Error, doc: ILoggerStorageEntry) => {
if (err) {
reject(err);
} else {
resolve(doc);
}
});
});
}
public getEntriesFor(appId: string): Promise<Array<ILoggerStorageEntry>> {
throw new Error('Method not implemented.');
}
}
import { AppManager } from '@rocket.chat/apps-engine/server/AppManager';
import { AppFabricationFulfillment } from '@rocket.chat/apps-engine/server/compiler';
import { ProxiedApp } from '@rocket.chat/apps-engine/server/ProxiedApp';
import { App } from '@rocket.chat/apps-ts-definition/App';
import { AppStatusUtils } from '@rocket.chat/apps-ts-definition/AppStatus';
import { IAppInfo } from '@rocket.chat/apps-ts-definition/metadata';
import * as AdmZip from 'adm-zip';
import * as fs from 'fs';
import * as path from 'path';
import * as socketIO from 'socket.io';
import { ServerAppBridges } from './bridges/bridges';
import { ServerAppLogStorage } from './logStorage';
import { ServerAppStorage } from './storage';
export class Orchestrator {
public bridges: ServerAppBridges;
public storage: ServerAppStorage;
public logStorage: ServerAppLogStorage;
public manager: AppManager;
private io: SocketIO.Server;
private folder: string;
constructor() {
this.bridges = new ServerAppBridges();
this.storage = new ServerAppStorage();
this.logStorage = new ServerAppLogStorage();
this.folder = 'dist';
this.manager = new AppManager(this.storage, this.logStorage, this.bridges);
}
public async loadAndUpdate(): Promise<boolean> {
const appsLoaded = await this.manager.load();
console.log(`!!!! Manager has finished loading ${ appsLoaded.length } Apps !!!!`);
const files = fs.readdirSync(this.folder)
.filter((file) => file.endsWith('.zip') && fs.statSync(path.join(this.folder, file)).isFile());
for (const file of files) {
const zipBase64 = fs.readFileSync(path.join(this.folder, file), 'base64');
const zip = new AdmZip(new Buffer(zipBase64, 'base64'));
const infoZip = zip.getEntry('app.json');
let info: IAppInfo;
if (infoZip && !infoZip.isDirectory) {
try {
info = JSON.parse(infoZip.getData().toString()) as IAppInfo;
} catch (e) {
throw new Error('Invalid App package. The "app.json" file is not valid json.');
}
}
try {
if (info && this.manager.getOneById(info.id)) {
console.log(`!!!! Updating the App ${ info.name } to v${ info.version } !!!!`);
this.handleAppFabFulfilled(await this.manager.update(zipBase64));
} else {
console.log(`!!!! Installing the App ${ info.name } v${ info.version } !!!!`);
this.handleAppFabFulfilled(await this.manager.add(zipBase64));
}
} catch (e) {
console.log('Got an error while working with:', file);
console.error(e);
throw e;
}
}
this.manager.get().forEach((rl: ProxiedApp) => {
if (AppStatusUtils.isEnabled(rl.getStatus())) {
console.log(`Successfully loaded: ${ rl.getName() } v${ rl.getVersion() }`);
} else if (AppStatusUtils.isDisabled(rl.getStatus())) {
console.log(`Failed to load: ${ rl.getName() } v${ rl.getVersion() }`);
} else {
console.log(`Neither failed nor succeeded in loading: ${ rl.getName() } v${ rl.getVersion() }`);
}
});
return true;
}
public setSocketServer(server: SocketIO.Server): void {
this.io = server;
this.io.on('connection', (socket) => {
socket.emit('status', { loaded: this.manager.areAppsLoaded() });
this.sendAppsInfo(socket);
socket.on('get/enabled', (fn) => {
fn(this.manager.get({ enabled: true }).map((rl) => rl.getInfo()));
});
socket.on('get/disabled', (fn) => {
fn(this.manager.get({ disabled: true }).map((rl) => rl.getInfo()));
});
// TODO: language additions
socket.on('get/commands', (fn) => {
fn(this.bridges.getCommandBridge().getCommands());
});
});
}
private sendAppsInfo(socket?: SocketIO.Socket): void {
const enabled = this.manager.get({ enabled: true }).map((rl) => rl.getInfo());
const disabled = this.manager.get({ disabled: true }).map((rl) => rl.getInfo());
if (socket) {
socket.emit('apps', { enabled, disabled });
} else {
this.io.emit('apps', { enabled, disabled });
}
}
private handleAppFabFulfilled(aff: AppFabricationFulfillment): void {
if (aff.getCompilerErrors().length !== 0) {
aff.getCompilerErrors().forEach((e) => console.error(e.message));
console.log(`!!!! Failure due to ${ aff.getCompilerErrors().length } errors !!!!`);
}
}
}
import { Orchestrator } from './orchestrator';
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import * as socketIO from 'socket.io';
const app = express();
let orch = new Orchestrator();
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'site')));
app.get('/loaded', (req, res) => {
res.json({ apps: orch.manager.get().map((rc) => rc.getName()) });
});
app.post('/load', (req, res) => {
if (req.body.appId) {
res.status(501).json({ success: false, err: 'Coming soon.' });
} else {
orch = new Orchestrator();
orch.loadAndUpdate()
.then(() => res.json({ success: true }))
.catch((err) => res.status(500).json({ success: false, err }));
}
});
app.post('/event', (req, res) => {
console.log(req.body, req.body.msg);
res.json({ success: true });
});
app.use((req, res, next) => {
let route = path.join('node_modules', req.url);
let route2 = path.join('.server-dist', 'site', req.url);
const ext = path.extname(req.url);
if (!ext) {
route += '.js';
route2 += '.js';
}
if (fs.existsSync(route2)) {
console.log('Loading:', route2);
fs.createReadStream(route2).pipe(res);
} else if (fs.existsSync(route)) {
console.log('Loading:', route);
fs.createReadStream(route).pipe(res);
} else {
res.status(404).json({ message: 'Not found.' });
}
});
const server = app.listen(3003, function _appListen() {
console.log('Example app listening on port 3003!');
console.log('http://localhost:3003/');
orch.loadAndUpdate()
.then(() => console.log('Completed the loading'))
.catch((err) => console.warn('Errored loadAndUpdate:', err));
});
orch.setSocketServer(socketIO.listen(server));
import { AppStorage, IAppStorageItem } from '@rocket.chat/apps-engine/server/storage';
import * as Datastore from 'nedb';
export class ServerAppStorage extends AppStorage {
private db: Datastore;
constructor() {
super('nedb');
this.db = new Datastore({ filename: '.server-data/apps.nedb', autoload: true });
this.db.ensureIndex({ fieldName: 'id', unique: true });
}
public create(item: IAppStorageItem): Promise<IAppStorageItem> {
return new Promise((resolve, reject) => {
item.createdAt = new Date();
item.updatedAt = new Date();
// tslint:disable-next-line
this.db.findOne({ $or: [{ id: item.id }, { 'info.nameSlug': item.info.nameSlug }] }, (err: Error, doc: IAppStorageItem) => {
if (err) {
reject(err);
} else if (doc) {
reject(new Error('App already exists.'));
} else {
this.db.insert(item, (err2: Error, doc2: IAppStorageItem) => {
if (err2) {
reject(err2);
} else {
resolve(doc2);
}
});
}
});
});
}
public retrieveOne(id: string): Promise<IAppStorageItem> {
return new Promise((resolve, reject) => {
this.db.findOne({ id }, (err: Error, doc: IAppStorageItem) => {
if (err) {
reject(err);
} else if (doc) {
resolve(doc);
} else {
reject(new Error(`Nothing found by the id: ${id}`));
}
});
});
}
public retrieveAll(): Promise<Map<string, IAppStorageItem>> {
return new Promise((resolve, reject) => {
this.db.find({}, (err: Error, docs: Array<IAppStorageItem>) => {
if (err) {
reject(err);
} else {
const items = new Map<string, IAppStorageItem>();
docs.forEach((i) => items.set(i.id, i));
resolve(items);
}
});
});
}
public update(item: IAppStorageItem): Promise<IAppStorageItem> {
return new Promise((resolve, reject) => {
this.db.update({ id: item.id }, item, {}, (err: Error, numOfUpdated: number) => {
if (err) {
reject(err);
} else {
this.retrieveOne(item.id).then((updated: IAppStorageItem) => resolve(updated))
.catch((err2: Error) => reject(err2));
}
});
});
}
public remove(id: string): Promise<{ success: boolean}> {
return new Promise((resolve, reject) => {
this.db.remove({ id }, (err: Error) => {
if (err) {
reject(err);
} else {
resolve({ success: true });
}
});
});
}
}
{
"compileOnSave": false,
"compilerOptions": {
"sourceMap": true,
"target": "es6",
"module": "commonjs",
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"lib": ["es2017", "dom"],
"outDir": "../.server-dist"
},
"atom": {
"rewriteTsconfig": false
}
}
import { AppServerCommunicator } from '@rocket.chat/apps-engine/client/AppServerCommunicator';
import { IAppInfo } from '@rocket.chat/apps-ts-definition/metadata/IAppInfo';
export class DevCommunicator extends AppServerCommunicator {
constructor(private readonly socket: SocketIO.Socket) {
super();
}
public getEnabledApps(): Promise<Array<IAppInfo>> {
return new Promise((resolve) => {
this.socket.emit('get/enabled', (enableds) => {
resolve(enableds);
});
});
}
public getDisabledApps(): Promise<Array<IAppInfo>> {
return new Promise((resolve) => {
this.socket.emit('get/disabled', (disableds) => {
resolve(disableds);
});
});
}
public getLanguageAdditions(): Promise<Map<string, Map<string, object>>> {
throw new Error('Method not implemented.');
}
public getSlashCommands(): Promise<Map<string, Array<string>>> {
return new Promise((resolve) => {
this.socket.emit('get/commands', (commands) => {
resolve(commands);
});
});
}
public getContextualBarButtons(): Promise<Map<string, Array<object>>> {
throw new Error('Method not implemented.');
}
}