Skip to content

Commit

Permalink
v3.1.0-RC1
Browse files Browse the repository at this point in the history
Fixes several bugs in 3.0.0 including (but not limited to)

* Failed logins are reported as successful
* Split notification:mention event into notification:mention and notification:group_mention events
* Failure for a plugin to load was not reported, leading to confusion


Noteable updates in this merge:
* Misc test fixes from test audit
* update readme for version 3.0
* remove ignored codeclimate folder
* update travis configuration
* release 3.0.0
* fix crash bug on run and add test to catch regressions of crash bug of f6714c09c3c5
* implement `group_mention` notification type
* adjust configuration validation to include checking for plugin configurations - fixes #293
* detect when plugin fails to load and fail the bot start because of it
* detect failed nodebb login - resolves #294
* release 3.1.0-RC1
  • Loading branch information
AccaliaDeElementia committed Apr 7, 2016
1 parent 50a1a6c commit 54c2a73
Show file tree
Hide file tree
Showing 17 changed files with 282 additions and 191 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ before_script:
- git config credential.helper "store --file=.git/credentials"
- echo "https://${GITHUB_USERNAME}:${GITHUB_TOKEN}@github.com" > .git/credentials
script:
- npm run lint
- npm test
after_script:
- npm run buildDocs && npm run pushDocs
Expand Down
77 changes: 1 addition & 76 deletions docs/Development/plugin creation.md
Original file line number Diff line number Diff line change
@@ -1,78 +1,3 @@
# Plugin Creation

SockBot incorporates a plugin architecture to add functionality, some plugins are built in but any number of
other plugins can be created and loaded into SockBot to add additional functionality.

## Plugin Format

SockBot plugins are node modules that export a certain set of functions. A minimal set would be:
```
exports.prepare = function prepare(pluginConfig, sockBotConfig, events, browser) {};
exports.start = function start() {};
exports.stop = function stop() {};
```

Plugins are activated by having their require path included as a key in the configuration `plugins` key.
Bundled plugins and plugins installed via NPM may omit the path as node.js will find them correctly on the
bare name, other plugins should be specified by absolute path.

### Function `prepare(pluginConfig, sockBotConfig, events, browser)`

This function is the initial entry point for plugins, it is called when SockBot has read the configuration
for the bot and before it has logged in to discourse. The provided parameters give the plugin the configuration
state of SockBot as well as the [EventEmitter] used for communication within the bot, and a [browser] for
communicating with discourse.

This function is assumed to be synchronous and *should not* set up any periodic or delayed actions. Any such
actions desired should be initiated in `start()`.

#### Parameter `pluginConfig`

This parameter will be the value that was stored in the configuration file for the plugin. The format of this
object is to be determined by each individual plugin, however all plugins *should* accept the value `true`
to use the plugin with the plugins default configuration.

#### Parameter `sockBotConfig`

This parameter will be the complete [configuration] object for SockBot, including core and plugin
configuration. Once SockBot has logged into discourse this object will be populated with current user
information.

#### Parameter `events`

This parameter will be an [EventEmitter], augmented with several methods specific to SockBot. Events
originating within Discourse will be emitted by this object, allowing plugins to respond to Discourse
notifications and other events.

#### Parameter `browser`

This parameter will be a [browser] that is set up to communicate with Discourse. At the time of the
prepare() call the browser will not have yet authenticated with discourse.

[EventEmitter]: ../api/external/events.md#module_SockEvents
[browser]: ../api/lib/browser.md#module_browser
[configuration]: ../api/lib/config.md

### Function `start()`

This function is called after SockBot has successfully authenticated to Discourse. At the point of this call
the current user information has been stored in the `sockBotConfig` provided earlier in the `prepare()` call.
Additionally the earlier provided browser object has been authenticated with discourse and is fully ready.

Any periodic or automatic actions *should* be initiated by this function. These periodic or automatic actions
*must* be cancellable by a call to `stop()`.

No arguments are provided when this function is called.

### Function `stop()`

This function is called before SockBot stops, either for a configuration reload or for termination. Any
periodic or deferred actions created by the plugin *must* be canceled by this function. The plugin *must*
assume that the bot is stopping for termination.

If the bot is merely reloading configuration the new configuration will be communicated to the plugin by a
call to `prepare()` followed by a call to `start()` when the new configuration is ready to be used.

No arguments are provided when this function is called.
Coming soon: Plugin creation instructions for Sockbot 3.0!
6 changes: 3 additions & 3 deletions docs/api/lib/app.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Functions

<dl>
<dt><a href="#_buildMessage">_buildMessage(...message)</a> ⇒ <code>string</code></dt>
<dt><a href="#_buildMessage">_buildMessage(args)</a> ⇒ <code>string</code></dt>
<dd><p>Construct a stringified message to log</p>
</dd>
<dt><a href="#log">log(...message)</a></dt>
Expand All @@ -23,15 +23,15 @@

<a name="_buildMessage"></a>

## _buildMessage(...message) ⇒ <code>string</code>
## _buildMessage(args) ⇒ <code>string</code>
Construct a stringified message to log

**Kind**: global function
**Returns**: <code>string</code> - stringified message

| Param | Type | Description |
| --- | --- | --- |
| ...message | <code>\*</code> | Item to stringify and log |
| args | <code>Array.&lt;\*&gt;</code> | Item to stringify and log |

<a name="log"></a>

Expand Down
5 changes: 3 additions & 2 deletions docs/api/plugins/summoner.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ Example plugin, replies to mentions with random quips.
**License**: MIT

* [summoner](#module_summoner)
* [module.exports(forum)](#exp_module_summoner--module.exports) ⇒ <code>Plugin</code> ⏏
* [module.exports(forum, config)](#exp_module_summoner--module.exports) ⇒ <code>Plugin</code> ⏏
* [~handler(notification)](#module_summoner--module.exports..handler) ⇒ <code>Promise</code>
* [~activate()](#module_summoner--module.exports..activate)
* [~deactivate()](#module_summoner--module.exports..deactivate)

<a name="exp_module_summoner--module.exports"></a>

### module.exports(forum) ⇒ <code>Plugin</code> ⏏
### module.exports(forum, config) ⇒ <code>Plugin</code> ⏏
Plugin generation function.

Returns a plugin object bound to the provided forum provider
Expand All @@ -25,6 +25,7 @@ Returns a plugin object bound to the provided forum provider
| Param | Type | Description |
| --- | --- | --- |
| forum | <code>Provider</code> | Active forum Provider |
| config | <code>object</code> &#124; <code>Array</code> | Plugin configuration |

<a name="module_summoner--module.exports..handler"></a>

Expand Down
37 changes: 24 additions & 13 deletions lib/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ const debug = require('debug')('sockbot');
/**
* Construct a stringified message to log
*
* @param {...*} message Item to stringify and log
* @param {Array<*>} args Item to stringify and log
* @returns {string} stringified message
*/
exports._buildMessage = function _buildMessage() {
const parts = Array.prototype.slice.call(arguments);
parts.unshift(`[${new Date().toISOString()}]`);
return parts
exports._buildMessage = function _buildMessage(args) {
if (!args || args.length < 1) {
return '';
}
if (!Array.isArray(args)) {
args = Array.prototype.slice.apply(args);
}
args.unshift(`[${new Date().toISOString()}]`);
return args
.map((part) => typeof part === 'string' ? part : JSON.stringify(part, null, '\t'))
.join(' ');
};
Expand All @@ -30,7 +35,7 @@ exports._buildMessage = function _buildMessage() {
* @param {...*} message Message to log to stdout
*/
exports.log = function log() {
console.log(exports._buildMessage.apply(null, arguments)); // eslint-disable-line no-console
console.log(exports._buildMessage(arguments)); // eslint-disable-line no-console
};

/**
Expand All @@ -39,7 +44,7 @@ exports.log = function log() {
* @param {...*} message Message to log to stderr
*/
exports.error = function error() {
console.error(exports._buildMessage.apply(null, arguments)); // eslint-disable-line no-console
console.error(exports._buildMessage(arguments)); // eslint-disable-line no-console
};

/**
Expand Down Expand Up @@ -75,14 +80,18 @@ exports.relativeRequire = function relativeRequire(relativePath, module, require
*
* @param {Provider} forumInstance Provider instance to load plugins into
* @param {object} botConfig Bot configuration to load plugins with
* @returns {Promise} Resolves when plugins have been loaded
*/
exports.loadPlugins = function loadPlugins(forumInstance, botConfig) {
Object.keys(botConfig.plugins).map((name) => {
return Promise.all(Object.keys(botConfig.plugins).map((name) => {
exports.log(`Loading plugin ${name} for ${botConfig.core.username}`);
const plugin = exports.relativeRequire('plugins', name, require);
const pluginConfig = botConfig.plugins[name];
forumInstance.addPlugin(plugin, pluginConfig);
});
return forumInstance.addPlugin(plugin, pluginConfig).catch((err) => {
exports.error(`Plugin ${name} failed to load with error: ${err}`);
throw err;
});
}));
};

/**
Expand All @@ -99,9 +108,11 @@ exports.activateConfig = function activateConfig(botConfig) {
instance.on('error', exports.error);
instance.on('logExtended', utils.logExtended);
instance.Commands = commands.bindCommands(instance);
exports.loadPlugins(instance, botConfig);
exports.log(`${botConfig.core.username} ready for login`);
return instance.login()
return exports.loadPlugins(instance, botConfig)
.then(() => {
exports.log(`${botConfig.core.username} ready for login`);
})
.then(() => instance.login())
.then(() => {
exports.log(`${botConfig.core.username} login successful`);
return instance.activate();
Expand Down
21 changes: 16 additions & 5 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ exports.load = function load(filePath) {
})
.then((data) => data.map((cfg) => {
exports.basePath = path.posix.resolve(filePath, '..');
exports.validateConfig(cfg);
const config = utils.mergeObjects(true, defaultConfig, cfg);
exports.validateConfig(config);
return config;
}));
};
Expand All @@ -133,14 +133,25 @@ exports.load = function load(filePath) {
*/
exports.validateConfig = function validateConfig(config) {
const errors = [];
const checkIt = (key) => {
const checkSection = (key) => {
if (typeof config[key] !== 'object') {
errors.push(`Missing configuration section: ${key}`);
} else if (Object.keys(config[key]).length === 0) {
errors.push(`Configuration section ${key} has no configuration items`);
}
};
const checkCore = (key) => {
if (typeof config.core[key] !== 'string' || config.core[key].length < 1) {
errors.push(`A valid ${key} must be specified`);
}
};
checkIt('username');
checkIt('password');
checkIt('owner');
checkSection('core');
checkSection('plugins');
if (errors.length === 0) {
checkCore('username');
checkCore('password');
checkCore('owner');
}
if (errors.length > 0) {
throw new Error(errors.join('\n'));
}
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sockbot",
"version": "3.0.0",
"releaseName": "Alpha Alpaca",
"version": "3.1.0-RC1",
"releaseName": "Beta Badger",
"description": "A sockpuppet bot to use on http://what.thedailywtf.com.",
"repository": "https://github.com/SockDrawer/SockBot",
"license": "MIT",
Expand Down Expand Up @@ -47,7 +47,9 @@
"scripts": {
"start": "node lib/app.js",
"lint": "eslint .",
"pretest": "istanbul cover node_modules/mocha/bin/_mocha --print both -x 'external/**' -x 'build/**' --include-all-sources -- --recursive -R spec",
"preistanbulspec": "npm run lint",
"istanbulspec": "istanbul cover node_modules/mocha/bin/_mocha --print both -x 'external/**' -x 'build/**' --include-all-sources -- --recursive -R spec",
"pretest": "npm run istanbulspec",
"test": "istanbul check-coverage coverage/coverage.json",
"istanbul": "istanbul cover node_modules/mocha/bin/_mocha --print both -x 'external/**' -x 'build/**' --include-all-sources -- --recursive -R dot",
"mocha": "mocha --recursive -R dot",
Expand Down
2 changes: 1 addition & 1 deletion providers/nodebb/category.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ exports.bindCategory = function bindCategory(forum) {
* This constructor is intended for private use only, if you need top construct a category from payload data use
* `Category.parse()` instead.
*
* @private
* @public
* @class
*
* @param {*} payload Payload to construct the Category object out of
Expand Down
6 changes: 5 additions & 1 deletion providers/nodebb/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,15 @@ class Forum extends EventEmitter {
remember: 'off',
returnTo: this.url
}
}, (loginError) => {
}, (loginError, response, reason) => {
if (loginError) {
debug(`Login failed for reason: ${loginError}`);
return reject(loginError);
}
if (response.statusCode >= 400) {
debug(`Login failed for reason: ${reason}`);
return reject(reason);
}
debug('complete post login data');
return resolve();
});
Expand Down
14 changes: 11 additions & 3 deletions providers/nodebb/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const utils = require('../../lib/utils');
* @returns {Notification} A Notification class bound to the provided `forum` instance
*/
exports.bindNotification = function bindNotification(forum) {

const mentionTester = new RegExp(`(^|\\s)@${forum.username}(\\s|$)`, 'i');

/**
* Notification types enum
*
Expand All @@ -35,18 +38,23 @@ exports.bindNotification = function bindNotification(forum) {
* This constructor is intended to be private use only, if you need to construct a notification from payload
* data use `Notification.parse()` instead
*
* @private
* @public
* @class
*
* @param {*} payload Payload to construct the Notification object out of
*/
constructor(payload) {
payload = utils.parseJSON(payload);
const body = string(payload.bodyLong || '').unescapeHTML().s;
let type = 'notification';
if (/^\[\[notifications:user_posted_to/i.test(payload.bodyShort)) {
type = 'reply';
} else if (/^\[\[mentions:user_mentioned_you_in/i.test(payload.bodyShort)) {
type = 'mention';
if (mentionTester.test(body)) {
type = 'mention';
} else {
type = 'group_mention';
}
}

const subtype = (/^\[\[\w+:(\w+)/.exec(payload.bodyShort) || [])[1] || '';
Expand All @@ -55,7 +63,7 @@ exports.bindNotification = function bindNotification(forum) {
type: type,
subtype: subtype,
label: payload.bodyShort,
body: string(payload.bodyLong || '').unescapeHTML().s,
body: body,
id: payload.nid,
postId: payload.pid,
topicId: payload.tid,
Expand Down
2 changes: 1 addition & 1 deletion providers/nodebb/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ exports.bindPost = function bindPost(forum) {
* This constructor is intended to be private use only, if you need to construct a post from payload data use
* `User.parse()` instead
*
* @private
* @public
* @class
*
* @param {*} payload Payload to construct the Post object out of
Expand Down
2 changes: 1 addition & 1 deletion providers/nodebb/topic.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ exports.bindTopic = function bindTopic(forum) {
* This constructor is intended for private use only, if you need top construct a topic from payload data use
* `Topic.parse()` instead.
*
* @private
* @public
* @class
*
* @param {*} payload Payload to construct the User object out of
Expand Down
4 changes: 1 addition & 3 deletions providers/nodebb/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ exports.bindUser = function bindUser(forum) {
* This constructor is intended to be private use only, if you need to construct a user from payload data use
* `User.parse()` instead
*
* @private
* @public
* @class
*
* @param {*} payload Payload to construct the User object out of
Expand Down Expand Up @@ -184,7 +184,6 @@ exports.bindUser = function bindUser(forum) {
* @reject {Error} An Error that occured while processing
*/
follow() {
debug(`following user ${this.id}`);
return forum._emit('user.follow', {
uid: this.id
}).then(() => this);
Expand All @@ -202,7 +201,6 @@ exports.bindUser = function bindUser(forum) {
* @reject {Error} An Error that occured while processing
*/
unfollow() {
debug(`unfollowing user ${this.id}`);
return forum._emit('user.unfollow', {
uid: this.id
}).then(() => this);
Expand Down
Loading

0 comments on commit 54c2a73

Please sign in to comment.