Skip to content

Commit

Permalink
Merge pull request #3 from FabricLabs/simple-start
Browse files Browse the repository at this point in the history
latest
  • Loading branch information
naterchrdsn authored Apr 20, 2018
2 parents 1d98843 + 93dd371 commit 2a0b572
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 48 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ package-lock.json

# Optional REPL history
.node_repl_history

# sensitive configuration
config
config.json
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ finally "New configuration". Place the "API Token" into `config/index.json`:
### Discord
@naterchrdsn will need to fill this out. :)

## Documentation
Documentation can be generated by running `npm run make:docs` — this will output
an HTML-formatted copy of the API to `docs/`, which can be served with (if
installed!) `http-server docs` or simply opened in your browser.

[slack]: https://slack.com
[discord]: https://discordapp.com
[matrix]: https://matrix.org
52 changes: 42 additions & 10 deletions lib/doorman.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ Doorman.prototype.start = function configure () {

self.register({
name: 'help',
value: `Available ${self.config.trigger}triggers: ${Object.keys(self.triggers).map(x => '`' + x + '`').join(', ')}`
value: `Available triggers: ${Object.keys(self.triggers).map(x => '`' + self.config.trigger + x + '`').join(', ')}`
});

if (self.config.debug) {
this.scribe.log('[DEBUG]', 'triggers:', Object.keys(self.triggers));
}

self.emit('ready');

this.scribe.log('started!');

return this;
Expand All @@ -89,6 +91,8 @@ Doorman.prototype.enable = function enable (name) {
self.emit('user', {
id: [name, 'users', user.id].join('/'),
name: user.name,
online: user.online || false,
subscriptions: [],
'@data': user
});
});
Expand All @@ -97,10 +101,18 @@ Doorman.prototype.enable = function enable (name) {
self.emit('channel', {
id: [name, 'channels', channel.id].join('/'),
name: channel.name,
members: [],
'@data': channel
});
});

service.on('join', async function (join) {
self.emit('join', {
user: [name, 'users', join.user].join('/'),
channel: [name, 'channels', join.channel].join('/')
});
});

service.on('message', async function (msg) {
let now = Date.now();
let id = [now, msg.actor, msg.target, msg.object].join('/');
Expand All @@ -110,8 +122,8 @@ Doorman.prototype.enable = function enable (name) {
self.emit('message', {
id: full,
created: now,
actor: msg.actor,
target: [name, msg.target].join('/'),
actor: [name, 'users', msg.actor].join('/'),
target: [name, 'channels', msg.target].join('/'),
object: msg.object,
'@data': msg
});
Expand Down Expand Up @@ -142,19 +154,39 @@ Doorman.prototype.enable = function enable (name) {
*/
Doorman.prototype.use = function assemble (plugin) {
let self = this;
let Handler = Plugin.fromName(plugin);
let name = null;
let Handler = null;

if (typeof plugin === 'string') {
Handler = Plugin.fromName(plugin);
name = plugin;
} else if (plugin instanceof Function) {
Handler = plugin;
name = Handler.name.toLowerCase();
} else {
Handler = plugin;
}

self.scribe.log(`enabling plugin "${name}"...`, Handler);

if (!Handler) return false;
if (Handler instanceof Function) {
util.inherits(Handler, Plugin);

self.plugins[plugin] = new Handler(self.config[plugin]);
self.plugins[plugin].trust(self).start();
let handler = new Handler(self.config[name]);

handler.on('message', function (message) {
let parts = message.target.split('/');
self.services[parts[0]].send(parts[2], message.object);
});

if (self.plugins[plugin].triggers) {
for (let id in self.plugins[plugin].triggers) {
self.register(self.plugins[plugin].triggers[id]);
}
self.plugins[name] = handler;
self.plugins[name].trust(self).start();

if (self.plugins[name].triggers) {
Object.keys(self.plugins[name].triggers).forEach(trigger => {
self.register(self.plugins[name].triggers[trigger]);
});
}
} else {
Object.keys(Handler).forEach(name => {
Expand Down
47 changes: 42 additions & 5 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,49 @@

const util = require('util');
const Disk = require('./disk');
const Service = require('../lib/service');

/**
* Plugins are the developer-facing component of Doorman. Used to configure
* behavior by consumers, developers can rely on the Plugin prototype to provide
* basic functionality needed by an instanced plugin.
* @constructor
*/
function Plugin (doorman) {
this.doorman = doorman;
return this;
}

util.inherits(Plugin, require('events').EventEmitter);
util.inherits(Plugin, Service);

/**
* Static method for loading a plugin from disk.
* @param {String} name Name of the plugin to load.
* @return {Mixed} Loaded plugin, or `null`.
*/
Plugin.fromName = function (name) {
let disk = new Disk();
let path = `plugins/${name}`;
let real = `doorman-${name}`;
let fallback = `./node_modules/doorman/${path}.js`;
let plugin = null;

if (disk.exists(path + '.js') || disk.exists(path)) {
plugin = disk.get(path);
} else if (disk.exists(fallback)) {
try {
plugin = disk.get(fallback);
} catch (E) {
if (this.config && this.config.debug) {
console.warn('Error loading module (fallback):', E);
}
}
} else if (disk.exists(`node_modules/${real}`)) {
try {
plugin = require(real);
} catch (E) {
// console.warn('Error loading module:', E);
if (this.config && this.config.debug) {
console.warn('Error loading module (real):', E);
}
}
} else {
plugin = name;
Expand All @@ -35,18 +58,32 @@ Plugin.prototype.trust = function connect (fabric) {
return this;
};

Plugin.prototype.router = function handler (request) {
/**
* Route a request to its appropriate handler.
* @param {Mixed} request Temporarily mixed type.
* @return {Plugin} Chainable method.
*/
Plugin.prototype.route = function handler (request) {
this.emit('request', request);
return this;
};

/**
* Start the plugin. This method is generally overridden by the child.
* @return {Plugin} Chainable method.
*/
Plugin.prototype.start = function initialize () {
return this;
};

/**
* Attach the router to a particular message channel.
* @param {String} channel Name of channel.
* @return {Plugin} Chainable method.
*/
Plugin.prototype.subscribe = function (channel) {
if (!this.fabric) return new Error('No Fabric instance supplied. Failing.');
this.fabric.on(channel, this.router.bind(this));
this.fabric.on(channel, this.route.bind(this));
return this;
};

Expand Down
4 changes: 2 additions & 2 deletions lib/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Router.prototype.route = async function handle (msg) {
.filter(x => x.charAt(0) === this.config.trigger)
.map(x => x.substr(1));

for (var i in parts) {
for (let i in parts) {
let token = parts[i];
let command = token.toLowerCase();
let handler = this.handlers[command];
Expand All @@ -35,7 +35,7 @@ Router.prototype.route = async function handle (msg) {
result = handler.value;
break;
default:
result = await handler.value.apply(this.fabric, [msg]);
result = await handler.value.apply(this.fabric.plugins[command], [msg]);
break;
}

Expand Down
63 changes: 60 additions & 3 deletions lib/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@
const util = require('util');
const stream = require('stream');

/**
* The "Service" is a simple model for messaging systems in general. In most
* cases, you'll use this model as a prototype for implementing a new protocol
* for Doorman, the general-purpose bot framework.
*
* To implement a Service, you will typically need to implement the methods on
* this prototype. In general, `connect` and `send` are the highest-order
* methods, and by default the `fabric` property will serve as a readable stream
* that broadcasts all inserted data. You should follow this pattern when
* developing Services.
*
* @param {Object} config Configuration for this service.
* @property map The "map" is a hashtable of "key" => "value" pairs.
* @constructor
* @description Basic API for connecting Doorman to a new service provider.
*/
function Service (config) {
this.config = config || {};
this.connection = null;
Expand All @@ -17,14 +33,22 @@ Service.prototype.connect = function initialize () {
status: 'active'
};

this.bus = new stream.Transform();
this.fabric = new stream.Transform({
transform (chunk, encoding, callback) {
callback(null, chunk);
}
});
};

Service.prototype.ready = function ready () {
this.emit('ready');
};

/**
* Default route handler for an incoming message. Follows the Activity Streams
* 2.0 spec: https://www.w3.org/TR/activitystreams-core/
* @param {Object} message Message object.
* @return {Boolean} Message handled!
* @return {Service} Chainable method.
*/
Service.prototype.handler = function route (message) {
this.emit('message', {
Expand All @@ -39,11 +63,44 @@ Service.prototype.handler = function route (message) {
* Send a message to a channel.
* @param {String} channel Channel name to which the message will be sent.
* @param {String} message Content of the message to send.
* @return {Service} Chainable event.
* @return {Service} Chainable method.
*/
Service.prototype.send = function send (channel, message, extra) {
console.log('[SERVICE]', 'send:', channel, message, extra);
return this;
};

Service.prototype._registerUser = function registerUser (user) {
if (!user.id) return console.error('User must have an id.');
let id = `/users/${user.id}`;
this.map[id] = Object.assign({
subscriptions: []
}, this.map[id], user);
this.emit('user', this.map[id]);
};

Service.prototype._registerChannel = function registerChannel (channel) {
if (!channel.id) return console.error('Channel must have an id.');
let id = `/channels/${channel.id}`;
this.map[id] = Object.assign({
members: []
}, this.map[id], channel);
this.emit('channel', this.map[id]);
};

Service.prototype._getSubscriptions = async function getSubscriptions (id) {
let member = this.map[`/users/${id}`] || {};
return member.subscriptions || null;
};

Service.prototype._getMembers = async function getMembers (id) {
let channel = this.map[`/channels/${id}`] || {};
return channel.members || null;
};

Service.prototype._getPresence = async function getPresence (id) {
let member = this.map[`/users/${id}`] || {};
return member.presence || null;
};

module.exports = Service;
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"name": "doorman",
"version": "0.2.0-pre",
"description": "simple community management",
"main": "doorman.js",
"main": "lib/doorman.js",
"scripts": {
"start": "node doorman.js",
"test": "NODE_ENV=test mocha --recursive",
"coverage": "NODE_ENV=test istanbul cover _mocha -- --recursive",
"make:docs": "jsdoc doorman.js -d docs"
"make:docs": "jsdoc lib README.md -d docs"
},
"repository": {
"type": "git",
Expand All @@ -33,14 +33,15 @@
"homepage": "https://github.com/FabricLabs/doorman#readme",
"dependencies": {
"@slack/client": "^4.1.0",
"debug-trace": "^2.2.1",
"discord.js": "^11.1.0",
"matrix-js-sdk": "^0.9.2-cryptowraning.1"
"erm": "^0.0.1",
"marked": "^0.3.19",
"matrix-js-sdk": "^0.10.1"
},
"devDependencies": {
"chai": "^4.1.2",
"coveralls": "^3.0.0",
"istanbul": "^1.1.0-alpha.1",
"jsdoc": "^3.5.5",
"mocha": "^4.0.0",
"semistandard": "^11.0.0"
},
Expand Down
10 changes: 10 additions & 0 deletions plugins/erm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';
// # ER MAH GERWD
// ## `erm`, an example Doorman plugin
// Using [the Doorman plugin system](), it's easy to respond to messages with
// a simple API. First, let's import erm (https://www.npmjs.com/package/erm):
const erm = require('erm');

// Using the new Object destructuring feature in ES6, we can simply export the
// function made available by the `erm` module:
module.exports = { erm };
Loading

0 comments on commit 2a0b572

Please sign in to comment.