3
0
mirror of https://github.com/reality/dbot.git synced 2024-12-25 20:22:39 +01:00

Merge reality's stuff

This commit is contained in:
Dafydd Francis 2013-01-30 22:10:30 +00:00
commit feb51aaceb
109 changed files with 3919 additions and 1453 deletions

6
.gitmodules vendored
View File

@ -1,3 +1,9 @@
[submodule "jsbot"] [submodule "jsbot"]
path = jsbot path = jsbot
url = git://github.com/reality/jsbot.git url = git://github.com/reality/jsbot.git
[submodule "modules/stats"]
path = modules/stats
url = git://github.com/SamStudio8/stats.git
[submodule "modules/github"]
path = modules/github
url = git://github.com/zuzak/dbot-github.git

18
LICENCE Normal file
View File

@ -0,0 +1,18 @@
Copyright (c) 2012-2013 Luke Slater (tinmachin3@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

111
README.md
View File

@ -3,8 +3,8 @@
## Introduction ## Introduction
Depressionbot is an IRC bot which aims to be the fanciest IRC bot around - On Depressionbot is an IRC bot which aims to be the fanciest IRC bot around - On
the general standard of software fanciness, dbot is rated as being '81% the same the general standard of software fanciness, dbot is statistically rated as being
as bathing in fine, fine grape juice.' '82% the same as bathing in fine, fine grape juice.'
Please note that this documentation is not complete and is a work in progress, Please note that this documentation is not complete and is a work in progress,
given I started it rather a long time after I began development of the project. given I started it rather a long time after I began development of the project.
@ -15,111 +15,14 @@ Requirements:
- Node JS - Node JS
- [JSBot](http://github.com/reality/JSBot "JSBot"), a Javascript library which - [JSBot](http://github.com/reality/JSBot "JSBot"), a Javascript library which
handles the IRC protocol. handles the IRC protocol
- Underscore JS library
- Various modules have their own requirements also. - Various modules have their own requirements also.
### JSBot ### External Modules
JSBot can be imported by running the following commands in the cloned repository: JSBot and externally developed modules can be imported by running the following
commands in the cloned repository:
git submodule init git submodule init
git submodule update git submodule update
## Modules:
### Command
This handles the command execution logic for DBot.
1. Does the input match a command key in *dbot.commands* ?
2. Is there a quote category which matches the first part of the input
(*~category*)?
3. Is there a command name similar to to the first part of the input (*~name*)
in *dbot.commands*?
This is the only module which is force loaded, even if it's not in the
configuration.
### Quotes
This is the original reason that DBot was created, stores and displays quotes.
Commands:
- _~q category_ - Display a random quote from a given category.
- _~qadd category=newquote_ - Add a new quote to the database.
- _~qstats_ - Show a list of the biggest quote categories.
- _~qsearch category needle_ - Search for a quote in a given category.
- _~rmlast [category]_ - Remove the last quote added to a given category, or the
last quote added.
- _~rm category quote_ - Remove a given quote from the given category.
- _~qcount category_ - Show the number of quotes stored in the given category.
- _~rq_ - Show a random quote from a random category.
- _~d_ - Show a quote from the category which matches the bot's name.
- _~link category_ - Create a link to the page on the web interface which displays the
given category's quotes.
- _~qprune_ - Delete empty quote categories.
Unfortunately, this module is fairly highly coupled with certain other areas of
the program. I am working on this, but note, for example, that one can still
access quotes with the *~category* syntax even if the quotes module isn't
loaded.
### Admin
Various administration functionality such as banning users, hot-reloading the
code and ordering him to talk. Note that commands added here are handled with
their own listener, rather than being part of the command logic which is handled
by the Command module. Functionality in this module can be slightly unsafe as
not much error checking on the input is performed.
TODO: Add summaries for each command in this module.
### Spelling
Will attempt to correct a users' spelling by using the levenshtein distance
algorithm. One corrects the spelling of their previous message by simply posting
a message with their correction and an asterisk:
> user: I am a tutrle.
> user: *turtle
user meant: I am a turtle.
The regular expression for this module also accepts two asterisks at the
beginning of the correction, or at the end; it also accepts several words as the
correction and deals with these fairly intelligently. Users may also attempt
to correct another users like so:
> userone: I am a tutrle.
> usertwo: userone: *turtle
> usertwo thinks userone meant: I am a turtle.
### JS
This module provides two commands which allow the execution of Javascript code.
For regular users, there is the *~js* command, which is completely sandboxed,
but can still be used for calculation and the like.
> ~js Array(16).join('wat'-1) + " Batman!";
'NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!'
This feature is fairly safe as the user doesn't have access to anything
dangerous, and is safe from infinite loops or locking DBot up because the code
which is run is killed if it does not finish within a short amount of time.
For administrators, the incredibly useful *~ajs* command is also available. The
input for this command is simply 'eval'-ed and therefore has full access to
DBot's memory. Of course, this is incredibly unsafe, but I find it rather fun;
remember to only give extremely trusted friends administrator access to DBot, as
there's nothing to stop them wiping the database or something similar - if
you're worried about that kind of thing, do not load this module.
However, it's useful for many things, such as administrative activity for
which there isn't a command in the admin module. For example, you could hot-add
a new administrator like this:
> ~ajs dbot.admin.push('batman');
2
You can also use this for debugging, or even adding new commands while DBot is
running.

5
VERSION Normal file
View File

@ -0,0 +1,5 @@
depressionbot version 0.4-dev
"the database is a grilled cheese"
_.each(dbot.modules, function(module) { "RESCORE EVERYTHING" });
"He called his bot depressionbot, and that's when he was happy."

View File

@ -12,6 +12,8 @@
} }
}, },
"admins": [ "batman" ], "admins": [ "batman" ],
"moduleNames": [ "ignore", "admin", "command", "dice", "js", "kick", "puns", "quotes", "spelling", "youare" ], "moderators": [ "whatever" ],
"language": "english" "moduleNames": [ "ignore", "admin", "command", "dice", "js", "kick", "quotes", "spelling", "youare", "stats", "users" ],
"language": "english",
"debugMode": true
} }

35
install.sh Executable file
View File

@ -0,0 +1,35 @@
#!/bin/bash
cat LICENCE
git submodule init
git submodule update
npm install underscore request sandbox express moment jade@0.25
cd public/
wget http://twitter.github.com/bootstrap/assets/bootstrap.zip
unzip bootstrap.zip
rm bootstrap.zip
mkdir d3
cd d3
wget http://d3js.org/d3.v3.zip
unzip d3.v3.zip
rm d3.v3.zip
cd ../..
if [ ! -f config.json ];
then
echo 'Creating configuration file...'
cp config.json.sample config.json
vim config.json
fi
read -p "Setup complete. Run depressionbot now? [y/N]"
if [[ ! $REPLY =~ ^[Yy]$ ]]
then
echo 'Okay. To run the bot, use "node run.js"'
exit
fi
node run.js

2
jsbot

@ -1 +1 @@
Subproject commit dbd987551bd8ce68655bbedb6bc5e98e90557708 Subproject commit 35910d9025fa3af15b24cecc3f6e7ee897aee4dc

57
modules/admin/README.md Normal file
View File

@ -0,0 +1,57 @@
## Admin
Administrator functionality.
### Description
Various administration functionality such as banning users, hot-reloading the
code and ordering him to talk. Note that commands added here are handled with
their own listener, rather than being part of the command logic which is handled
by the Command module. Functionality in this module can be slightly unsafe as
not everything is thoroughly sanity checked.
### Commands
#### join [#channel]
Join the given channel.
#### part [#channel]
Leave the given channel.
#### opme [#channel]
Gives the caller ops in a given channel if possible. If called without a
channel, it will attempt to give the caller ops in the current channel.
#### greload
Perform a git pull, and then execute the 'reload' command. Saves a lot of time
updating!
#### reload
Reload all of the modules currently in use by DBot. By using this, all module
functionality should be reloadable and replaceable without having to restart the
bot or interrupt the connection to the server.
#### say [#channel] [message]
Have DBot post the given message in the given channel (uses the server from
which you are sending the message). You may replace channel with '@' to have him
post the message in the current channel. Channel may also be replaced with a
nick on the server.
#### load [module]
Load a new module. This works by adding a module name to the roster and then
triggering a reload of all modules, at which point the new module is actually
loaded by the standard DBot process.
#### unload [module]
Unload a currently loaded module. This removes the module, and then triggers a
reload of all modules.
#### ban [user] [command]
Ban a user from using a command. Command may be replaced with '\*,' which will
ban a user from use of all commands. Users banned from all commands will still
be subject to module listeners.
#### unban [user] [command]
Unban a user from using a given command. If a user was previously banned using
the '\*' wildcard, they may also be unbanned from such by replacing command with
an asterisk here as well.

View File

@ -3,147 +3,12 @@
* Description: Set of commands which only one who is a DepressionBot * Description: Set of commands which only one who is a DepressionBot
* administrator can run - as such, it has its own command execution listener. * administrator can run - as such, it has its own command execution listener.
*/ */
var fs = require('fs'); var fs = require('fs'),
var sys = require('sys') _ = require('underscore')._;
var exec = require('child_process').exec;
var admin = function(dbot) { var admin = function(dbot) {
var commands = {
// Join a channel
'join': function(event) {
var channel = event.params[1];
if(event.allChannels.hasOwnProperty(channel)) {
event.reply("I'm already in that channel.");
} else {
dbot.instance.join(event, channel);
event.reply(dbot.t('join', {'channel': channel}));
}
},
// Leave a channel
'part': function(event) {
var channel = event.params[1];
if(!event.allChannels.hasOwnProperty(channel)) {
event.reply("I'm not in that channel.");
} else {
event.instance.part(event, channel);
event.reply(dbot.t('part', {'channel': channel}));
}
},
// Op admin caller in given channel
'opme': function(event) {
var channel = event.params[1];
// If given channel isn't valid just op in current one.
if(!event.allChannels.hasOwnProperty(channel)) {
channel = event.channel.name;
}
dbot.instance.mode(event, channel, ' +o ' + event.user);
},
// Do a git pull and reload
'greload': function(event) {
var child = exec("git pull", function (error, stdout, stderr) {
event.reply(dbot.t('gpull'));
commands.reload(event);
}.bind(this));
},
// Reload DB, translations and modules.
'reload': function(event) {
dbot.db = JSON.parse(fs.readFileSync('db.json', 'utf-8'));
dbot.reloadModules();
event.reply(dbot.t('reload'));
},
// Say something in a channel (TODO probably doesn't work.)
'say': function(event) {
var channel = event.params[1];
if(event.params[1] === "@") {
var channel = event.channel.name;
}
var message = event.params.slice(2).join(' ');
dbot.say(event.server, channel, message);
},
// Load new module
'load': function(event) {
var moduleName = event.params[1];
dbot.config.moduleNames.push(moduleName);
dbot.reloadModules();
event.reply(dbot.t('load_module', {'moduleName': moduleName}));
},
// Unload a loaded module
'unload': function(event) {
var moduleNames = dbot.config.moduleNames;
var moduleName = event.params[1];
if(moduleNames.include(moduleName)) {
var moduleDir = '../' + moduleName + '/';
var cacheKey = require.resolve(moduleDir + moduleName);
delete require.cache[cacheKey];
var moduleIndex = moduleNames.indexOf(moduleName);
moduleNames.splice(moduleIndex, 1);
dbot.reloadModules();
event.reply(dbot.t('unload_module', {'moduleName': moduleName}));
} else {
event.reply(dbot.t('unload_error', {'moduleName': moduleName}));
}
},
// Ban user from command or *
'ban': function(event) {
var username = event.params[1];
var command = event.params[2];
if(!dbot.db.bans.hasOwnProperty(command)) {
dbot.db.bans[command] = [ ];
}
dbot.db.bans[command].push(username);
event.reply(dbot.t('banned', {'user': username, 'command': command}));
},
// Unban a user from command or *
'unban': function(event) {
var username = event.params[1];
var command = event.params[2];
if(dbot.db.bans.hasOwnProperty(command) && dbot.db.bans[command].include(username)) {
dbot.db.bans[command].splice(dbot.db.bans[command].indexOf(username), 1);
event.reply(dbot.t('unbanned', {'user': username, 'command': command}));
} else {
event.reply(dbot.t('unban_error', {'user': username}));
}
},
// Lock quote category so quotes can't be removed
'lock': function(event) {
var category = event.params[1];
dbot.db.locks.push(category);
event.reply(dbot.t('qlock', {'category': category}));
}
};
return {
'name': 'admin',
'ignorable': false,
/**
* Run the appropriate admin command given the input (and user).
*/
'listener': function(event) {
var commandName = event.params[0];
if(commands.hasOwnProperty(commandName) && dbot.config.admins.include(event.user)) {
commands[commandName](event);
dbot.save();
}
},
'on': 'PRIVMSG'
};
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return admin(dbot); return new admin(dbot);
}; };

312
modules/admin/commands.js Normal file
View File

@ -0,0 +1,312 @@
var fs = require('fs'),
_ = require('underscore')._,
sys = require('sys'),
exec = require('child_process').exec;
var commands = function(dbot) {
var noChangeConfig = [ 'servers', 'name', 'moduleNames' ];
var getCurrentConfig = function(configKey) {
var defaultConfigPath = dbot.config;
var userConfigPath = dbot.db.config;
if(configKey) {
var configKey = configKey.split('.');
for(var i=0;i<configKey.length-1;i++) {
if(_.has(defaultConfigPath, configKey[i])) {
if(!_.has(userConfigPath, configKey[i])) {
userConfigPath[configKey[i]] = {};
}
userConfigPath = userConfigPath[configKey[i]];
defaultConfigPath = defaultConfigPath[configKey[i]];
} else {
return false;
}
}
}
var currentOption;
if(configKey.length != 1) {
configKey = _.last(configKey);
if(_.has(userConfigPath, configKey) && !_.isUndefined(userConfigPath[configKey])) {
currentOption = userConfigPath[configKey];
} else if(_.has(defaultConfigPath, configKey)) {
currentOption = defaultConfigPath[configKey];
}
} else {
currentOption = defaultConfigPath[configKey];
}
return {
'user': userConfigPath,
'default': defaultConfigPath,
'value': currentOption
};
};
var commands = {
// Join a channel
'join': function(event) {
var channel = event.params[1];
if(_.has(event.allChannels, channel)) {
event.reply(dbot.t('already_in_channel', {'channel': channel}));
} else {
dbot.instance.join(event, channel);
event.reply(dbot.t('join', {'channel': channel}));
}
},
// Leave a channel
'part': function(event) {
var channel = event.params[1];
if(!_.has(event.allChannels, channel)) {
event.reply(dbot.t('not_in_channel', {'channel': channel}));
} else {
event.instance.part(event, channel);
event.reply(dbot.t('part', {'channel': channel}));
}
},
// Op admin caller in given channel
'opme': function(event) {
var channel = event.params[1];
// If given channel isn't valid just op in current one.
if(!_.has(event.allChannels, channel)) {
channel = event.channel.name;
}
dbot.instance.mode(event, channel, ' +o ' + event.user);
},
// Do a git pull and reload
'greload': function(event) {
exec("git pull", function (error, stdout, stderr) {
exec("git submodule update", function (error, stdout, stderr) {
event.reply(dbot.t('gpull'));
commands.reload(event);
event.message = 'version';
event.action = 'PRIVMSG';
event.params = event.message.split(' ');
dbot.instance.emit(event);
}.bind(this));
}.bind(this));
},
// Display commit information for part of dbot
'version': function(event){
var cmd = "git log --pretty=format:'%h (%s): %ar' -n 1 -- ";
if(event.params[1]){
var input = event.params[1].trim();
if(_.has(dbot.modules, input.split("/")[0])){
cmd += "modules/"+input;
}
else{
cmd += input;
}
}
exec(cmd, function(error, stdout, stderr){
if(stdout.length > 0){
event.reply(stdout);
}
else{
event.reply("No version information or queried module not loaded");
}
}.bind(this));
},
'status': function(event) {
var moduleName = event.params[1];
if(_.has(dbot.status, moduleName)) {
var status = dbot.status[moduleName];
if(status === true) {
event.reply(moduleName + ' status: Shit looks good.');
} else {
event.reply(moduleName + ' status: Failed to load: ' + status);
}
} else {
event.reply('Either that module wasn\'t on the roster or shit is totally fucked.');
}
},
// Reload DB, translations and modules.
'reload': function(event) {
dbot.db = JSON.parse(fs.readFileSync('db.json', 'utf-8'));
dbot.reloadModules();
event.reply(dbot.t('reload'));
},
// Say something in a channel
'say': function(event) {
var channel = event.params[1];
if(event.params[1] === "@") {
var channel = event.channel.name;
}
var message = event.params.slice(2).join(' ');
dbot.say(event.server, channel, message);
},
// Load new module
'load': function(event) {
var moduleName = event.params[1];
if(!_.include(dbot.config.moduleNames, moduleName)) {
dbot.config.moduleNames.push(moduleName);
dbot.reloadModules();
if(dbot.status[moduleName] === true) {
event.reply(dbot.t('load_module', {'moduleName': moduleName}));
} else {
event.reply('Failed to load ' + moduleName + '. See \'status ' + moduleName + '\'.');
}
} else {
if(moduleName == 'web') {
event.reply(dbot.t('already_loaded_web'));
} else {
event.reply(dbot.t('already_loaded', {'moduleName': moduleName}));
}
}
},
// Unload a loaded module
'unload': function(event) {
var moduleNames = dbot.config.moduleNames;
var moduleName = event.params[1];
if(_.include(moduleNames, moduleName)) {
var moduleDir = '../' + moduleName + '/';
var cacheKey = require.resolve(moduleDir + moduleName);
delete require.cache[cacheKey];
dbot.config.moduleNames = _.without(dbot.config.moduleNames, moduleName);
dbot.reloadModules();
event.reply(dbot.t('unload_module', {'moduleName': moduleName}));
} else {
event.reply(dbot.t('unload_error', {'moduleName': moduleName}));
}
},
// Ban user from command or *
'ban': function(event) {
var username = event.params[1];
var command = event.params[2];
if(!_.has(dbot.db.bans, command)) {
dbot.db.bans[command] = [ ];
}
dbot.db.bans[command].push(username);
event.reply(dbot.t('banned', {'user': username, 'command': command}));
},
// Unban a user from command or *
'unban': function(event) {
var username = event.params[1];
var command = event.params[2];
if(_.has(dbot.db.bans, command) && _.include(dbot.db.bans[command], username)) {
_.reject(dbot.db.bans[command], function(bans) {
return bans == username;
}, this);
event.reply(dbot.t('unbanned', {'user': username, 'command': command}));
} else {
event.reply(dbot.t('unban_error', {'user': username}));
}
},
/*** Config options ***/
'setconfig': function(event) {
var configPathString = event.params[1],
configKey = _.last(configPathString.split('.')),
newOption = event.params[2];
if(!_.include(noChangeConfig, configKey)) {
var configPath = getCurrentConfig(configPathString);
if(configPath == false || _.isUndefined(configPath.value)) {
event.reply("Config key doesn't exist bro");
return;
}
var currentOption = configPath.value;
// Convert to boolean type if config item boolean
if(_.isBoolean(currentOption)) {
newOption = (newOption == "true");
}
if(_.isArray(currentOption)) {
event.reply("Config option is an array. Try 'pushconfig'.");
}
event.reply(configPathString + ": " + currentOption + " -> " + newOption);
configPath['user'][configKey] = newOption;
dbot.reloadModules();
} else {
event.reply("This config option cannot be altered while the bot is running.");
}
},
'pushconfig': function(event) {
var configPathString = event.params[1],
configKey = _.last(configPathString.split('.')),
newOption = event.params[2];
if(!_.include(noChangeConfig, configKey)) {
var configPath = getCurrentConfig(configPathString);
if(configPath == false || _.isUndefined(configPath.value)) {
event.reply("Config key doesn't exist bro");
return;
}
var currentArray = configPath.value;
if(!_.isArray(currentArray)) {
event.reply("Config option is not an array. Try 'setconfig'.");
return
}
event.reply(configPathString + ": " + currentArray + " << " + newOption);
currentArray.push(newOption);
dbot.reloadModules();
}
},
'showconfig': function(event) {
var configPathString = event.params[1];
var configPath = getCurrentConfig(configPathString);
if(configPathString) {
var configKey = _.last(configPathString.split('.'));
if(configKey == false) {
event.reply("Config path doesn't exist");
return;
}
if(_.isArray(configPath.value)) {
event.reply(configKey + ': ' + configPath.value);
} else if(_.isObject(configPath.value)) {
event.reply('Config keys in ' + configPathString + ': ' + Object.keys(configPath.value));
} else {
event.reply(configKey + ': ' + configPath.value);
}
} else {
event.reply('Config keys in root: ' + Object.keys(configPath['default']));
}
}
};
commands['greload'].access = 'admin';
commands['reload'].access = 'admin';
commands['unload'].access = 'admin';
commands['load'].access = 'admin';
commands['setconfig'].access = 'admin';
commands['showconfig'].access = 'moderator';
commands['join'].access = 'moderator';
commands['part'].access = 'moderator';
commands['opme'].access = 'moderator';
commands['say'].access = 'moderator';
commands['ban'].access = 'moderator';
commands['unban'].access = 'moderator';
return commands;
};
exports.fetch = function(dbot) {
return commands(dbot);
}

View File

@ -1,3 +1,6 @@
{ {
"dbKeys": [ "bans", "locks" ] "ignorable": false,
"dbKeys": [ "bans" ],
"dependencies": [ "command" ],
"help": "http://github.com/reality/depressionbot/blob/master/modules/admin/README.md"
} }

View File

@ -64,5 +64,21 @@
"spanish": "Cerrado la categoría: {category}", "spanish": "Cerrado la categoría: {category}",
"na'vi": "{category}ìri oel 'upxareti fmoli", "na'vi": "{category}ìri oel 'upxareti fmoli",
"welsh": "Categori wedi cloi: {category}" "welsh": "Categori wedi cloi: {category}"
},
"already_in_channel": {
"english": "I'm already in {channel}",
"na'vi": "Oel {channel}it tok li"
},
"not_in_channel": {
"english": "I'm not in {channel}",
"na'vi": "Oel {channel}it ke tok"
},
"already_loaded_web": {
"english": "WHY CAN'T I LOAD ALL THIS WEB? (web already loaded)",
"na'vi": "PELUN OEL KE TSUN OMUM FÌWETIT NÌWOTX (wetìri oe omum li)"
},
"already_loaded": {
"english": "{moduleName} is already loaded.",
"na'vi": "Oel omum teri {moduleName}it li."
} }
} }

67
modules/command/README.md Normal file
View File

@ -0,0 +1,67 @@
## Command
Handles the command execution logic for DBot.
### Description
Command flow:
1. Does the input match a command key in the loaded commands?
* If command not found and quotes is loaded, attempt to print quote of given
command name
2. Is the user banned from running the given command?
3. Is the user ignoring the command?
4. Is the channel ignoring the command?
5. Does the use have the access level to run the command?
6. Is the command set as disabled?
7. Apply regex to the command, pass into event object.
* If regex does not apply, show usage info.
8. Run the command.
This is the only module which is force loaded, even if it's not specified in
the configuration file.
### Commands
#### ~usage [command]
Show usage information for a given command.
#### ~help [command|module]
Link module help for a module given either the module name or the name of a
command belonging to a module.
### API
#### isBanned(user, command)
Return whether a user is currently banned from a given commands.
#### hasAccess(user, command)
Return whether a user has the access level (moderator, admin) to run a given
command.
#### isIgnoring(user, command)
Return whether a user is currently marked as ignoring a given command.
#### addHook(command, callback)
This API function allows you to hook functions into DBot commands. For example,
you may add a hook to post on Identica when a new quote is added to the database
with the ~qadd command. As a less useful example, here is how you might add a
hook to log to the console every time someone uses the reload command:
dbot.api.command.addHook('reload', function() {
console.log('Reload run!');
});
Hook arguments are populated by the return values of the functions they are
hooked into, and command hooks are not run if the command explicitly returns
'false.' For example, the ~qadd command returns *[ key, quote ]*, and the hook
function will be called with these variables given in the order they were
returned, so you would retrieve the key and the quote from a hook to ~qadd like
this:
dbot.api.command.addHook('~qadd', function(key, quote) { ...
The best place to add hooks to commands is in the 'onLoad' function of your
module, as this ensures it will be run while all other modules are loaded. If
the target command does not exist (for example if its module was not loaded),
the hook will not be added and no errors will be thrown.

78
modules/command/api.js Normal file
View File

@ -0,0 +1,78 @@
var _ = require('underscore')._;
var api = function(dbot) {
return {
'isBanned': function(user, command) {
var banned = false;
if(_.has(dbot.db.bans, command)) {
if(_.include(dbot.db.bans[command], user) || _.include(dbot.db.bans['*'], user)) {
banned = true;
}
}
return banned;
},
/**
* Does the user have the correct access level to use the command?
*/
'hasAccess': function(user, command) {
var access = true;
var accessNeeded = dbot.commands[command].access;
if(accessNeeded == 'admin') {
if(!_.include(dbot.config.admins, user)) {
access = false;
}
} else if(accessNeeded == 'moderator') {
if(!_.include(dbot.config.moderators, user) &&
!_.include(dbot.config.admins, user)) {
access = false;
}
}
return access;
},
/**
* Is item (user or channel) ignoring command?
*/
'isIgnoring': function(item, command) {
var module = dbot.commands[command].module;
return (_.has(dbot.db.ignores, item) &&
_.include(dbot.db.ignores[item], module));
},
/**
* Apply Regex to event message, store result. Return false if it doesn't
* apply.
*/
'applyRegex': function(commandName, event) {
var applies = false;
if(_.has(dbot.commands[commandName], 'regex')) {
var cRegex = dbot.commands[commandName].regex;
var q = event.message.valMatch(cRegex[0], cRegex[1]);
if(q) {
applies = true;
event.input = q;
}
} else {
applies = true;
}
return applies;
},
'addHook': function(command, callback) {
console.log('adding hook');
if(_.has(dbot.commands, command)) {
if(!_.has(dbot.commands[command], 'hooks')) {
dbot.commands[command].hooks = [];
}
dbot.commands[command].hooks.push(callback);
}
}
};
};
exports.fetch = function(dbot) {
return api(dbot);
};

View File

@ -4,86 +4,50 @@
* command and then runs that command, given the user isn't banned from or * command and then runs that command, given the user isn't banned from or
* ignoring that command. * ignoring that command.
*/ */
var _ = require('underscore')._;
var command = function(dbot) { var command = function(dbot) {
/** this.dbot = dbot;
* Is user banned from using command?
*/
var isBanned = function(user, command) {
var banned = false;
if(dbot.db.bans.hasOwnProperty(command)) {
if(dbot.db.bans[command].include(user) || dbot.db.bans['*'].include(user)) {
banned = true;
}
}
return banned;
};
/**
* Is user ignoring command?
*/
var isIgnoring = function(user, command) {
var module = dbot.commandMap[command];
var ignoring = false;
if(dbot.db.ignores.hasOwnProperty(user) && dbot.db.ignores[user].include(module)) {
ignoring = true;
}
return ignoring;
};
/**
* Apply Regex to event message, store result. Return false if it doesn't
* apply.
*/
var applyRegex = function(commandName, event) {
var applies = false;
if(dbot.commands[commandName].hasOwnProperty('regex')) {
var cRegex = dbot.commands[commandName].regex;
var q = event.message.valMatch(cRegex[0], cRegex[1]);
if(q) {
applies = true;
event.input = q;
}
} else {
applies = true;
}
return applies;
};
return {
'name': 'command',
'ignorable': false,
'commands': {
'~usage': function(event) {
var commandName = event.params[1];
if(dbot.usage.hasOwnProperty(commandName)) {
event.reply('Usage for ' + commandName + ': ' +
dbot.usage[commandName]);
} else {
event.reply('No usage information for ' + commandName);
}
}
},
/** /**
* Run the appropriate command given the input. * Run the appropriate command given the input.
*/ */
'listener': function(event) { this.listener = function(event) {
var commandName = event.params[0]; var commandName = event.params[0];
if(!dbot.commands.hasOwnProperty(commandName)) { if(!_.has(dbot.commands, commandName)) {
if(_.has(dbot.modules, 'quotes')) {
commandName = '~'; commandName = '~';
} else {
return;
}
} }
if(isBanned(event.user, commandName)) { if(this.api.isBanned(event.user, commandName)) {
event.reply(dbot.t('command_ban', {'user': event.user})); event.reply(dbot.t('command_ban', {'user': event.user}));
} else { } else {
if(!isIgnoring(event.user, commandName)) { if(!this.api.isIgnoring(event.user, commandName) &&
if(applyRegex(commandName, event)) { !this.api.isIgnoring(event.channel, commandName) &&
dbot.commands[commandName](event); this.api.hasAccess(event.user, commandName) &&
dbot.commands[commandName].disabled !== true) {
if(this.api.applyRegex(commandName, event)) {
try {
var command = dbot.commands[commandName];
var results = command.apply(dbot.modules[command.module], [event]);
if(_.has(command, 'hooks') && results !== false) {
_.each(command['hooks'], function(hook) {
hook.apply(hook.module, _.values(results));
}, this);
}
} catch(err) {
if(dbot.config.debugMode == true) {
event.reply('- Error in ' + commandName + ':');
event.reply('- Message: ' + err);
event.reply('- Top of stack: ' + err.stack.split('\n')[1].trim());
}
}
dbot.save(); dbot.save();
} else { } else {
if(commandName !== '~') { if(commandName !== '~') {
if(dbot.usage.hasOwnProperty(commandName)){ if(_.has(dbot.usage, commandName)) {
event.reply('Usage: ' + dbot.usage[commandName]); event.reply('Usage: ' + dbot.usage[commandName]);
} else { } else {
event.reply(dbot.t('syntax_error')); event.reply(dbot.t('syntax_error'));
@ -92,12 +56,10 @@ var command = function(dbot) {
} }
} }
} }
}, }.bind(this);
'on': 'PRIVMSG' this.on = 'PRIVMSG';
};
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return command(dbot); return new command(dbot);
}; };

View File

@ -0,0 +1,43 @@
var _ = require('underscore')._;
var commands = function(dbot) {
return {
'~usage': function(event) {
var commandName = event.params[1];
if(_.has(dbot.usage, commandName)) {
event.reply(dbot.t('usage', {
'command': commandName,
'usage': dbot.usage[commandName]
}));
} else {
event.reply(dbot.t('no_usage_info', {
'command': commandName
}));
}
},
'~help': function(event) {
var moduleName = event.params[1];
if(!_.has(dbot.modules, moduleName)) {
var moduleName = dbot.commands[moduleName].module;
}
if(moduleName && _.has(dbot.config[moduleName], 'help')) {
var help = dbot.config[moduleName].help;
event.reply(dbot.t('help_link', {
'module': moduleName,
'link': help
}));
} else {
if(!moduleName) {
moduleName = event.params[1];
}
event.reply(dbot.t('no_help', { 'module': moduleName }))
}
}
};
};
exports.fetch = function(dbot) {
return commands(dbot);
};

View File

@ -0,0 +1,5 @@
{
"ignorable": false,
"help": "http://github.com/reality/depressionbot/blob/master/modules/command/README.md",
"dbKeys": [ "ignores" ]
}

View File

@ -10,5 +10,21 @@
"spanish": "Sintaxis no válida. Iniciar incineración.", "spanish": "Sintaxis no válida. Iniciar incineración.",
"na'vi": "Ngeyä pamrel keyawr lu. Nga skxawng lu.", "na'vi": "Ngeyä pamrel keyawr lu. Nga skxawng lu.",
"welsh": "Cystrawen annilys. Cychwyn orfflosgiad" "welsh": "Cystrawen annilys. Cychwyn orfflosgiad"
},
"usage": {
"english": "Usage for {command}: {usage}.",
"na'vi": "Nga tsun sivar ìlä {command}: {usage}."
},
"no_usage_info": {
"english": "No usage information found for {command}.",
"na'vi": "Oel ke tsun sivar {comamnd}it"
},
"help_link": {
"english": "Help for {module}: {link}",
"na'vi": "{module}ä srungìl {link} it tok"
},
"no_help": {
"english": "No help found for {module}.",
"na'vi": "Fì{module}ìri oel ke tsun run srungit"
} }
} }

31
modules/dent/README.md Normal file
View File

@ -0,0 +1,31 @@
## Dent
Post dents.
### Description
Allows the posting of dents to Identica. Easily abused for posting status
messages to Twitter by linking the Identica account.
### Commands
#### ~dent [text]
Post the given text to Identica.
### Configuration
#### username
Identica username to post with.
#### password
Identica password to post with.
### API
#### post(content)
Post the given content to Identica.
### Hooks
#### ~qadd
Posts new quote additions.

7
modules/dent/config.json Normal file
View File

@ -0,0 +1,7 @@
{
"username": "youruserhere",
"password": "yourpasswordhere",
"dependencies": [ "command" ],
"ignorable": true,
"dentQuotes": false
}

47
modules/dent/dent.js Normal file
View File

@ -0,0 +1,47 @@
var request = require('request');
_ = require('underscore')._;
var dent = function(dbot) {
this.dbot = dbot;
this.api = {
'post': function(content) {
var username = dbot.config.dent.username,
password = dbot.config.dent.password,
info,
auth = "Basic " +
new Buffer(username + ":" + password).toString("base64");
request.post({
'url': 'http://identi.ca/api/statuses/update.json?status=' +
content,
'headers': {
'Authorization': auth
}
},
function(error, response, body) {
console.log(body);
}.bind(this));
}
};
this.commands = {
'~dent': function(event) {
this.api.post(event.input[1]);
event.reply('Dent posted (probably).');
}
};
this.commands['~dent'].regex = [/^~dent (.+)$/, 2];
this.onLoad = function() {
if(dbot.config.dent.dentQuotes === true && _.has(dbot.modules, 'quotes')) {
dbot.api.command.addHook('~qadd', function(key, text) {
this.api.post(key + ': ' + text);
}.bind(this));
}
}.bind(this);
};
exports.fetch = function(dbot) {
return new dent(dbot);
};

9
modules/dice/README.md Normal file
View File

@ -0,0 +1,9 @@
## Dice
Rolls virtual dice.
### Description
Rolls a virtual die and outputs the result to the channel.
### Commands
#### ~roll <die type>
Rolls a die. 1d6 will be rolled by default.

4
modules/dice/config.json Normal file
View File

@ -0,0 +1,4 @@
{
"ignorable": true,
"dependencies": [ "command" ]
}

View File

@ -92,14 +92,9 @@ var dice = function(dbot) {
} }
} }
}; };
this.commands = commands;
return {
'name': 'dice',
'commands': commands,
'ignorable': true
};
} }
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return dice(dbot); return new dice(dbot);
}; };

30
modules/event/README.md Normal file
View File

@ -0,0 +1,30 @@
## Event
Emit events for whatever you want man idk.
### Description
This is a library module designed for other modules to use to emit various
events at any point, and also to attach functions to said events. These are
similar to command hooks, however their advantage is that they may be called
anywhere in your code; they are particularly useful when you want to attach a
callback to a listener.
### API
#### addHook(eventName, callback)
This function will set a given callback to be executed every time the
emit API function is executed with the given event name. The arguments of your
callback are defined as an array in the emit call.
The best place to add hooks to commands is in the 'onLoad' function of your
module, as this ensures it will be run while all other modules are loaded so
nothing will be missed.
#### emit(eventName, [ arguments ])
This function executes all of the functions associated with the given eventName,
passing your given array of arguments.
For example, to emit an event when you detect a nick change:
dbot.api.event.emit('nick_changed', [ event.server, newNick ]);

28
modules/event/event.js Normal file
View File

@ -0,0 +1,28 @@
/**
* Module Name: event
* Description: Allow other modules to emit events and that
*/
var _ = require('underscore')._;
var event = function(dbot) {
this.dbot = dbot;
this.hooks = {};
this.api = {
'addHook': function(eventName, callback) {
if(!_.has(this.hooks, eventName)) this.hooks[eventName] = [];
this.hooks[eventName].push(callback);
},
'emit': function(eventName, args) {
if(_.has(this.hooks, eventName)) {
_.each(this.hooks[eventName], function(callback) {
callback.apply(callback.module, args);
});
}
}
};
}
exports.fetch = function(dbot) {
return new event(dbot);
};

1
modules/github Submodule

@ -0,0 +1 @@
Subproject commit ca25f8f94e205e2a6e3119227a6255eb80cd26df

26
modules/ignore/README.md Normal file
View File

@ -0,0 +1,26 @@
## Ignore
Ignore modules.
### Description
Commands with which users can choose to ignore listeners and commands from
certain modules persistently, by storing their choices in the database. This is
an interface for the JSBot ignoreTag functionality which actually implements
the ignoration.
### Configuration
All modules may return with them an 'ignorable' property, which defines whether
or not their functionality may be ignored by users.
### Commands
#### ~ignore [module]
Ignore a given module. If the user does not specify a module, or provides an
invalid one a list of modules which are available to ignore will be given.
#### ~unignore [module]
Unignore a previously ignored module. If the user does not specify a module, or
provides an invalid choice a list of modules which are currently ignored will be
given.

View File

@ -1,3 +1,6 @@
{ {
"dbKeys": [ "ignores" ] "ignorable": false,
"dependencies": [ "command" ],
"dbKeys": [ "ignores" ],
"help": "http://github.com/reality/depressionbot/blob/master/modules/ignore/README.md"
} }

View File

@ -2,34 +2,43 @@
* Module Name: Ignore * Module Name: Ignore
* Description: Handles commands in which users can choose to ignore listeners * Description: Handles commands in which users can choose to ignore listeners
* and commands from certain modules. It also populates the JSBot instance with * and commands from certain modules. It also populates the JSBot instance with
* this information, since that actually performs the ignorance. * this information, since that actually performs the ignorance. Also provides
* commands for moderators to choose the bot to ignore certain channels.
*/ */
var _ = require('underscore')._;
var ignore = function(dbot) { var ignore = function(dbot) {
var commands = { var commands = {
'~ignore': function(event) { '~ignore': function(event) {
var ignorableModules = [];
for(var i=0;i<dbot.modules.length;i++) {
if(dbot.modules[i].ignorable != null && dbot.modules[i].ignorable == true) {
ignorableModules.push(dbot.modules[i].name);
}
}
var module = event.params[1]; var module = event.params[1];
var ignorableModules = _.chain(dbot.modules)
.filter(function(module, name) {
return dbot.config[module].ignorable === true;
})
.pluck('name')
.value();
if(module === undefined) { if(_.isUndefined(module)) {
event.reply(dbot.t('ignore_usage', {'user': event.user, 'modules': ignorableModules.join(', ')})); event.reply(dbot.t('ignore_usage', {
'user': event.user,
'modules': ignorableModules.join(', ')
}));
} else { } else {
if(ignorableModules.include(module)) { if(_.include(ignorableModules, module)) {
if(dbot.db.ignores.hasOwnProperty(event.user) && dbot.db.ignores[event.user].include(module)) { if(_.has(dbot.db.ignores, event.user) && _.include(dbot.db.ignores[event.user], module)) {
event.reply(dbot.t('already_ignoring', { 'user': event.user })); event.reply(dbot.t('already_ignoring', { 'user': event.user }));
} else { } else {
if(dbot.db.ignores.hasOwnProperty(module)) { if(_.has(dbot.db.ignores, module)) {
dbot.db.ignores[event.user].push(module); dbot.db.ignores[event.user].push(module);
} else { } else {
dbot.db.ignores[event.user] = [module]; dbot.db.ignores[event.user] = [module];
} }
dbot.instance.ignoreTag(event.user, module); dbot.instance.ignoreTag(event.user, module);
event.reply(dbot.t('ignored', {'user': event.user, 'module': module})); event.reply(dbot.t('ignored', {
'user': event.user,
'module': module
}));
} }
} else { } else {
event.reply(dbot.t('invalid_ignore', { 'user': event.user })); event.reply(dbot.t('invalid_ignore', { 'user': event.user }));
@ -39,43 +48,91 @@ var ignore = function(dbot) {
'~unignore': function(event) { '~unignore': function(event) {
var ignoredModules = []; var ignoredModules = [];
if(dbot.db.ignores.hasOwnProperty(event.user)) { if(_.has(dbot.db.ignores, event.user)) {
ignoredModules = dbot.db.ignores[event.user]; ignoredModules = dbot.db.ignores[event.user];
} }
var module = event.params[1]; var module = event.params[1];
if(module === undefined) { if(_.isUndefined(module)) {
event.reply(dbot.t('unignore_usage', {'user': event.user, 'modules': ignoredModules.join(', ')})); event.reply(dbot.t('unignore_usage', {
} else { 'user': event.user,
if(ignoredModules.include(module) == false) { 'modules': ignoredModules.join(', ')
event.reply(dbot.t('invalid_unignore', {'user': event.user})); }));
} else { } else {
if(_.include(ignoredModules, module)) {
dbot.db.ignores[event.user].splice(dbot.db.ignores[event.user].indexOf(module), 1); dbot.db.ignores[event.user].splice(dbot.db.ignores[event.user].indexOf(module), 1);
dbot.instance.removeIgnore(event.user, module) dbot.instance.removeIgnore(event.user, module)
event.reply(dbot.t('unignored', {'user': event.user, 'module': module})); event.reply(dbot.t('unignored', {
'user': event.user,
'module': module
}));
} else {
event.reply(dbot.t('invalid_unignore', { 'user': event.user }));
} }
} }
},
'~ignorechannel': function(event) {
var channel = ((event.params[1] == '@') ? event.channel.name : event.params[1]);
var module = event.params[2];
// Ignoring the value of 'ignorable' at the moment
if(_.include(dbot.config.moduleNames, module)) {
if(!_.has(dbot.db.ignores, channel)) dbot.db.ignores[channel] = [];
if(!_.include(dbot.db.ignores[channel], module)) {
dbot.db.ignores[channel].push(module);
dbot.instance.ignoreTag(channel, module);
event.reply(dbot.t('ignoring_channel', {
'module': module,
'channel': channel
}));
} else {
event.reply(dbot.t('already_ignoring_channel', {
'module': module,
'channel': channel
}));
}
} else {
event.reply(dbot.t('module_not_exist', { 'module': module }));
}
},
'~unignorechannel': function(event) {
var channel = ((event.params[1] == '@') ? event.channel.name : event.params[1]);
var module = event.params[2];
if(!_.has(dbot.db.ignores, channel)) dbot.db.ignores[channel] = [];
if(_.include(dbot.db.ignores[channel], module)) {
dbot.db.ignores[channel] = _.without(dbot.db.ignores[channel], module);
dbot.instance.removeIgnore(channel, module);
event.reply(dbot.t('unignoring_channel', {
'module': module,
'channel': channel
}));
} else {
event.reply(dbot.t('not_ignoring_channel', {
'module': module,
'channel': channel
}));
}
} }
}; };
return { commands['~ignorechannel'].access = 'moderator';
'name': 'ignore', commands['~unignorechannel'].access = 'moderator';
'ignorable': false,
'commands': commands,
'onLoad': function() { this.commands = commands;
this.onLoad = function() {
dbot.instance.clearIgnores(); dbot.instance.clearIgnores();
for(var user in dbot.db.ignores) { _.each(dbot.db.ignores, function(ignores, item) {
if(dbot.db.ignores.hasOwnProperty(user)) { _.each(ignores, function(ignore) {
for(var i=0;i<dbot.db.ignores[user].length;i++) { dbot.instance.ignoreTag(item, ignore);
dbot.instance.ignoreTag(user, dbot.db.ignores[user][i]); }, this);
} }, this);
}
}
}
}; };
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return ignore(dbot); return new ignore(dbot);
}; };

View File

@ -40,5 +40,25 @@
"spanish": "{user}: Ya no ignoras {module}.", "spanish": "{user}: Ya no ignoras {module}.",
"na'vi": "{user}: Nga terìng mikyun {module}ne set", "na'vi": "{user}: Nga terìng mikyun {module}ne set",
"welsh": "{user}: Ddim yn anwybyddu {module} bellach" "welsh": "{user}: Ddim yn anwybyddu {module} bellach"
},
"ignoring_channel": {
"english": "Now ignoring {module} in {channel}",
"na'vi": "Oe ke stayawm {module}ur mì {channel}"
},
"already_ignoring_channel": {
"english": "Already ignoring {module} in {channel}",
"na'vi": "Oe ke stayawm {module}ur mì {channel} li"
},
"module_not_exist": {
"english": "{module} isn't loaded or doesn't exist.",
"na'vi": "Oel ke omum teri {module}it fu {module} ke fkeytok"
},
"unignoring_channel": {
"english": "No longer ignoring {module} in {channel}",
"na'vi": "Oel stayawm {module}ur mì {channel} set."
},
"not_ignoring_channel": {
"english": "{module} wasn't being ignored in {channel}",
"na'vi": "Oel stayawm {module}ur mì {channel} li."
} }
} }

40
modules/js/README.md Normal file
View File

@ -0,0 +1,40 @@
## JS
Run JavaScript.
### Description
This module provides two commands which allow the execution of Javascript code
from the bot.
### Commands
#### ~js [code]
For regular users, there is the *~js* command, which is completely sandboxed,
but can still be used for calculation and the like.
> ~js Array(16).join('wat'-1) + " Batman!";
'NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!'
This feature is fairly safe as the user doesn't have access to anything
dangerous, and is safe from infinite loops or locking DBot up because the code
which is run is killed if it does not finish within a short amount of time.
#### ~ajs [code]
For administrators, the incredibly useful *~ajs* command is also available. The
input for this command is simply 'eval'-ed and therefore has full access to
DBot's memory. Of course, this is incredibly unsafe, but I find it rather fun;
remember to only give extremely trusted friends administrator access to your
DBot instance, as there's nothing to stop them wiping the database or probably
even your hard drive - if you're worried about that kind of thing, do not load
this module.
However, it's useful for many things, such as administrative activity for
which there isn't a command in the admin module. For example, you could hot-add
a new administrator like this:
> ~ajs dbot.admin.push('batman');
2
You can also use it for debugging, or even adding new commands while DBot is
running.

10
modules/js/config.json Normal file
View File

@ -0,0 +1,10 @@
{
"commands": {
"~js": {
"disabled": true
}
},
"dependencies": [ "command" ],
"ignorable": true,
"help": "http://github.com/reality/depressionbot/blob/master/modules/js/README.md"
}

View File

@ -8,12 +8,11 @@ var vm = require('vm');
var sbox = require('sandbox'); var sbox = require('sandbox');
var js = function(dbot) { var js = function(dbot) {
var s = new sbox();
var commands = { var commands = {
// Run JS code sandboxed, return result to channel. // Run JS code sandboxed, return result to channel.
'~js': function(event) { '~js': function(event) {
try { try {
var s = new sbox();
s.run(event.input[1], function(output) { s.run(event.input[1], function(output) {
event.reply(output.result); event.reply(output.result);
}.bind(this)); }.bind(this));
@ -22,12 +21,10 @@ var js = function(dbot) {
// Run JS code un-sandboxed, with access to DBot memory (admin-only). // Run JS code un-sandboxed, with access to DBot memory (admin-only).
'~ajs': function(event) { '~ajs': function(event) {
if(dbot.config.admins.include(event.user) ) {
var ret = eval(event.input[1]); var ret = eval(event.input[1]);
if(ret !== undefined) { if(ret !== undefined) {
event.reply(ret); event.reply(ret);
} }
}
}, },
'jesus': function (event) { 'jesus': function (event) {
event.reply(event.user + ": s/(.)(.)(.)(..)/\4\2 \1\3/") event.reply(event.user + ": s/(.)(.)(.)(..)/\4\2 \1\3/")
@ -36,14 +33,13 @@ var js = function(dbot) {
commands['~js'].regex = [/^~js (.*)/, 2]; commands['~js'].regex = [/^~js (.*)/, 2];
commands['~ajs'].regex = [/^~ajs (.*)/, 2]; commands['~ajs'].regex = [/^~ajs (.*)/, 2];
commands['jesus'].regex = [/^jesus$/, 2]; commands['jesus'].regex = [/^jesus$/, 2];
commands['~ajs'].access = 'admin';
return { this.name = 'js';
'name': 'js', this.ignorable = true;
'ignorable': true, this.commands = commands;
'commands': commands
};
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return js(dbot); return new js(dbot);
}; };

17
modules/kick/README.md Normal file
View File

@ -0,0 +1,17 @@
## Kick
Kicking and kicking-related accessories.
### Description
This module counts the number of times people are kicked from and kick people
from channels, and provides commands for viewing this data. It also has the bot
attempt to rejoin the channel if it is kicked.
### Commands
#### ~kickcount [username]
Show the number of times a given user has been kicked and has kicked other
people.
#### ~kickstats
Show a list of top kickers and kickees.

View File

@ -1,3 +1,6 @@
{ {
"dbKeys": [ "kicks", "kickers" ] "dbKeys": [ "kicks", "kickers" ],
"dependencies": [ "command" ],
"help": "http://github.com/reality/depressionbot/blob/master/modules/kick/README.md",
"ignorable": true
} }

View File

@ -1,3 +1,5 @@
var _ = require('underscore')._;
var kick = function(dbot) { var kick = function(dbot) {
var commands = { var commands = {
// Give the number of times a given user has been kicked and has kicked // Give the number of times a given user has been kicked and has kicked
@ -5,27 +7,35 @@ var kick = function(dbot) {
'~kickcount': function(event) { '~kickcount': function(event) {
var username = event.params[1]; var username = event.params[1];
if(!dbot.db.kicks.hasOwnProperty(username)) { if(!_.has(dbot.db.kicks, username)) {
var kicks = '0'; var kicks = '0';
} else { } else {
var kicks = dbot.db.kicks[username]; var kicks = dbot.db.kicks[username];
} }
if(!dbot.db.kickers.hasOwnProperty(username)) { if(!_.has(dbot.db.kickers, username)) {
var kicked = '0'; var kicked = '0';
} else { } else {
var kicked = dbot.db.kickers[username]; var kicked = dbot.db.kickers[username];
} }
event.reply(dbot.t('user_kicks', {'user': username, 'kicks': kicks, 'kicked': kicked})); event.reply(dbot.t('user_kicks', {
'user': username,
'kicks': kicks,
'kicked': kicked
}));
}, },
// Output a list of the people who have been kicked the most and those // Output a list of the people who have been kicked the most and those
// who have kicked other people the most. // who have kicked other people the most.
'~kickstats': function(event) { '~kickstats': function(event) {
var orderedKickLeague = function(list, topWhat) { var orderedKickLeague = function(list, topWhat) {
var kickArr = Object.prototype.sort(list, function(key, obj) { return obj[key]; }); var kickArr = _.chain(list)
kickArr = kickArr.slice(kickArr.length - 10).reverse(); .pairs()
.sortBy(function(kick) { return kick[1] })
.reverse()
.first(10)
.value();
var kickString = "Top " + topWhat + ": "; var kickString = "Top " + topWhat + ": ";
for(var i=0;i<kickArr.length;i++) { for(var i=0;i<kickArr.length;i++) {
@ -39,38 +49,36 @@ var kick = function(dbot) {
event.reply(orderedKickLeague(dbot.db.kickers, 'Kickers')); event.reply(orderedKickLeague(dbot.db.kickers, 'Kickers'));
} }
}; };
this.commands = commands;
return { this.listener = function(event) {
'name': 'kick',
'ignorable': false,
'commands': commands,
'listener': function(event) {
if(event.kickee == dbot.config.name) { if(event.kickee == dbot.config.name) {
dbot.instance.join(event, event.channel); dbot.instance.join(event, event.channel);
event.reply(dbot.t('kicked_dbot', { 'botname': dbot.config.name })); event.reply(dbot.t('kicked_dbot', { 'botname': dbot.config.name }));
dbot.db.kicks[dbot.config.name] += 1; dbot.db.kicks[dbot.config.name] += 1;
} else { } else {
if(!dbot.db.kicks.hasOwnProperty(event.kickee)) { if(!_.has(dbot.db.kicks, event.kickee)) {
dbot.db.kicks[event.kickee] = 1; dbot.db.kicks[event.kickee] = 1;
} else { } else {
dbot.db.kicks[event.kickee] += 1; dbot.db.kicks[event.kickee] += 1;
} }
if(!dbot.db.kickers.hasOwnProperty(event.user)) { if(!_.has(dbot.db.kickers, event.user)) {
dbot.db.kickers[event.user] = 1; dbot.db.kickers[event.user] = 1;
} else { } else {
dbot.db.kickers[event.user] += 1; dbot.db.kickers[event.user] += 1;
} }
event.reply(event.kickee + '-- (' + dbot.t('user_kicks', event.reply(event.kickee + '-- (' + dbot.t('user_kicks', {
{'user': event.kickee, 'kicks': dbot.db.kicks[event.kickee], 'kicked': dbot.db.kickers[event.kickee]}) + ')'); 'user': event.kickee,
'kicks': dbot.db.kicks[event.kickee],
'kicked': dbot.db.kickers[event.kickee]
}) + ')');
} }
},
on: 'KICK'
}; };
this.on = 'KICK';
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return kick(dbot); return new kick(dbot);
}; };

25
modules/link/README.md Normal file
View File

@ -0,0 +1,25 @@
## Link
Retrieves page titles.
### Description
This module stores the last posted link in each channel, and provides a command
for retrieving the title of a given link or the last posted link in the channel.
### Configuration
#### autoTitle: false
If this is set to true, the bot will automatically post the titles of links as
they are posted in the channel.
### Commands
#### ~title [link]
If called with a link, the bot will attempt to find and return the title of that
page. If called without a link, the bot will attempt the same on the last link
which was posted in the current channel.
#### ~ud [headword]
Returns the first [Urban Dictionary](http://www.urbandictionary.com) definition for the headword provided.
#### ~xkcd <comic ID>
Returns a link to the [xkcd](http://xkcd.com) comic specified, or the latest one if a comic is not given.

6
modules/link/config.json Normal file
View File

@ -0,0 +1,6 @@
{
"autoTitle": false,
"dependencies": [ "command" ],
"ignorable": true,
"help": "http://github.com/reality/depressionbot/blob/master/modules/link/README.md"
}

View File

@ -3,50 +3,85 @@
* Description: Stores recent channel links, with commands to retrieve * Description: Stores recent channel links, with commands to retrieve
* information about links. * information about links.
*/ */
var request = require('request'); var request = require('request'),
_ = require('underscore')._;
var link = function(dbot) { var link = function(dbot) {
var urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig; this.urlRegex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
var links = {}; this.links = {};
this.fetchTitle = function(event, link) {
var commands = {
'~title': function(event) {
var link = links[event.channel.name];
if(event.params[1] !== undefined) {
var urlMatches = event.params[1].match(urlRegex);
if(urlMatches !== null) {
link = urlMatches[0];
}
}
request(link, function(error, response, body) { request(link, function(error, response, body) {
if(!error && response.statusCode == 200) { if(!error && response.statusCode == 200) {
body = body.replace(/(\r\n|\n\r|\n)/gm, " "); body = body.replace(/(\r\n|\n\r|\n)/gm, " ");
var title = body.valMatch(/<title>(.*)<\/title>/, 2); var title = body.valMatch(/<title>(.*)<\/title>/, 2);
if(title) { if(title) {
event.reply(title[1]); event.reply(title[1]);
} else {
event.reply('no title found');
} }
} }
}); });
}
}; };
return { var commands = {
'name': 'link', '~title': function(event) {
'ignorable': true, var link = this.links[event.channel.name];
'commands': commands, if(!_.isUndefined(event.params[1])) {
var urlMatches = event.params[1].match(this.urlRegex);
'listener': function(event) {
var urlMatches = event.message.match(urlRegex);
if(urlMatches !== null) { if(urlMatches !== null) {
links[event.channel.name] = urlMatches[0]; link = urlMatches[0];
} }
}
this.fetchTitle(event, link);
}, },
'on': 'PRIVMSG'
'~xkcd': function(event) {
var comicId = event.params[1];
if(comicId){
comicId = comicId + "/";
} else {
comicId = "";
}
var link = "http://xkcd.com/"+comicId+"info.0.json";
request(link, function(error, response, body) {
if (response.statusCode == "200") {
data = JSON.parse(body);
event.reply(dbot.t("xkcd",data));
} else {
event.reply(dbot.t("no-hits"));
}
});
},
'~ud': function(event) {
var query = event.input[1];
var reqUrl = 'http://api.urbandictionary.com/v0/define?term=' + encodeURI(query);
request(reqUrl, function(error, response, body) {
try {
var result = JSON.parse(body);
if(_.has(result, 'result_type') && result.result_type != 'no_results') {
event.reply(query + ': ' + result.list[0].definition.split('\n')[0]);
} else {
event.reply(event.user + ': No definition found.');
}
} catch(err) { }
});
}
}; };
commands['~ud'].regex = [/~ud (.+)/, 2];
this.commands = commands;
this.listener = function(event) {
var urlMatches = event.message.match(this.urlRegex);
if(urlMatches !== null) {
this.links[event.channel.name] = urlMatches[0];
if(dbot.config.link.autoTitle == true) {
this.fetchTitle(event, urlMatches[0]);
}
}
}.bind(this);
this.on = 'PRIVMSG';
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return link(dbot); return new link(dbot);
}; };

12
modules/link/strings.json Normal file
View File

@ -0,0 +1,12 @@
{
"title_not_found": {
"english": "No page title found.",
"na'vi": "Oel ke tsun run 'upxare atxin."
},
"xkcd": {
"english": "xkcd {num}: {title} https://xkcd.com/{num}"
},
"no-hits": {
"english": "No hits."
}
}

35
modules/poll/README.md Normal file
View File

@ -0,0 +1,35 @@
## Poll
Pollers gonna poll.
### Description
This module allows creation of and voting in polls, with associated
functionality.
Note that while in terms of the interface all votes are anonymous, users'
voting choices are stored in the database for the purpose of users being
able to change their votes. Therefore an admin can technically go delving in
the database to see users' voting choices.
### Commands
#### ~newpoll [pollname] options=[each,poll,option] [Poll Description]
Creates a new poll with the given name, options and descriptions. From this
point people will be able to use the ~vote command to cast their vote in the
poll.
#### ~addoption [pollname] [newoption]
Using this command you can add a given option to a poll you are the creator of.
#### ~rmoption [pollname] [optiontoremove]
Using this command you can remove a given option from a poll you are the creator
of.
#### ~vote [pollname] [option]
Cast your vote for the given option in the given poll. If you have already cast
your vote in the given poll, your vote will be changed to the new option you
have provided.
#### ~pdesc [pollname]
Show the full description for a given poll name along with its available voting
options.

197
modules/poll/commands.js Normal file
View File

@ -0,0 +1,197 @@
var _ = require('underscore')._;
var commands = function(dbot) {
var polls = dbot.db.polls;
var commands = {
'~newpoll': function(event) {
var name = event.input[1].toLowerCase(),
options = event.input[2].toLowerCase().split(','),
description = event.input[3];
if(_.has(polls, name)) {
event.reply(dbot.t('poll_exists', { 'name': name }));
} else {
polls[name] = {
'name': name,
'description': description,
'owner': dbot.api.users.resolveUser(event.server, event.user),
'votes': {},
'votees': {}
};
for(var i=0;i<options.length;i++) {
polls[name]['votes'][options[i]] = 0;
}
event.reply(dbot.t('poll_created', {
'name': name,
'description': description,
'url': dbot.t('url', {
'host': dbot.config.web.webHost,
'port': dbot.config.web.webPort,
'path': 'polls/' + name
})
}));
}
},
'~addoption': function(event) {
var name = event.input[1].toLowerCase(),
option = event.input[2].toLowerCase(),
user = dbot.api.users.resolveUser(event.server, event.user);
if(_.has(polls, name)) {
if(polls[name].owner === user) {
if(!_.has(polls[name].votes, option)) {
polls[name]['votes'][option] = 0;
event.reply(dbot.t('option_added', {
'user': event.user,
'name': name,
'option': option
}));
} else {
event.reply(dbot.t('option_exists', {
'option': option,
'name': name,
'user': event.user
}));
}
} else {
event.reply(dbot.t('not_poll_owner', {
'user': event.user,
'name': name
}));
}
} else {
event.reply(dbot.t('poll_unexistent', {'name': name}));
}
},
'~rmoption': function(event) {
var name = event.input[1].toLowerCase(),
option = event.input[2].toLowerCase(),
user = dbot.api.users.resolveUser(event.server, event.user);
if(_.has(polls, name)) {
if(polls[name].owner === user) {
if(_.has(polls[name].votes, option)) {
delete polls[name]['votes'][option];
event.reply(dbot.t('option_removed', {
'user': event.user,
'name': name,
'option': option
}));
} else {
event.reply(dbot.t('invalid_vote', { 'vote': option }));
}
} else {
event.reply(dbot.t('not_poll_owner', { 'name': name }));
}
} else {
event.reply(dbot.t('poll_unexistent', { 'name': name }));
}
},
'~vote': function(event) {
var name = event.input[1].toLowerCase(),
vote = event.input[2].toLowerCase(),
user = dbot.api.users.resolveUser(event.server, event.user);
if(_.has(polls, name)) {
if(_.has(polls[name].votes, vote)) {
if(_.has(polls[name].votees, user)) {
var oldVote = polls[name].votees[user];
polls[name].votes[oldVote]--;
polls[name].votes[vote]++;
polls[name].votees[user] = vote;
event.reply(dbot.t('changed_vote', {
'vote': vote,
'poll': name,
'count': polls[name].votes[vote],
'user': event.user
}));
} else {
polls[name].votes[vote]++;
polls[name].votees[user] = vote;
event.reply(dbot.t('voted', {
'vote': vote,
'poll': name,
'count': polls[name].votes[vote],
'user': event.user
}));
}
} else {
event.reply(dbot.t('invalid_vote', { 'vote': vote }));
}
} else {
event.reply(dbot.t('poll_unexistent', { 'name': name }));
}
},
'~pdesc': function(event) {
var name = event.input[1].toLowerCase();
if(_.has(polls, name)) {
var options = _.keys(polls[name].votes);
var optionString = " Choices: ";
for(var i=0;i<options.length;i++) {
optionString += options[i] + ', ';
}
optionString = optionString.slice(0, -2) + '.';
event.reply(dbot.t('poll_describe', {
'name': name,
'description': polls[name].description,
'url': dbot.t('url', {
'host': dbot.config.web.webHost,
'port': dbot.config.web.webPort,
'path': 'polls/' + name
})
}) + optionString);
} else {
event.reply(dbot.t('poll_unexistent', { 'name': name }));
}
},
'~count': function(event) {
var name = event.input[1].toLowerCase();
if(_.has(polls, name)) {
var order;
var votesArr = [];
var order = _.chain(polls[name].votes)
.pairs()
.sortBy(function(option) { return option[1] })
.reverse()
.value();
var orderString = "";
for(var i=0;i<order.length;i++) {
orderString += order[i][0] +
" (" + order[i][1] + "), ";
}
orderString = orderString.slice(0, -2);
event.reply(dbot.t('count', {
'poll': name,
'description': polls[name].description,
'places': orderString
}));
} else {
event.reply(dbot.t('poll_unexistent', {'name': name}));
}
}
};
commands['~newpoll'].regex = [/~newpoll ([^ ]+) options=([^ ]+) (.+)/, 4];
commands['~addoption'].regex = [/~addoption ([^ ]+) ([^ ]+)/, 3];
commands['~rmoption'].regex = [/~rmoption ([^ ]+) ([^ ]+)/, 3];
commands['~vote'].regex = [/~vote ([^ ]+) ([^ ]+)/, 3];
commands['~pdesc'].regex = [/~pdesc ([^ ]+)/, 2];
commands['~count'].regex = [/~count ([^ ]+)/, 2];
return commands;
};
exports.fetch = function(dbot) {
return commands(dbot);
}

6
modules/poll/config.json Normal file
View File

@ -0,0 +1,6 @@
{
"help": "http://github.com/reality/depressionbot/blob/master/modules/poll/README.md",
"dbKeys": [ "polls" ],
"ignorable": true,
"dependencies": [ "users", "command" ]
}

45
modules/poll/pages.js Normal file
View File

@ -0,0 +1,45 @@
var _ = require('underscore')._;
var pages = function(dbot) {
var polls = dbot.db.polls;
var pages = {
// Shows the results of a poll
'/polls/:key': function(req, res) {
var key = req.params.key.toLowerCase();
if(_.has(dbot.db.polls, key)) {
var totalVotes = _.reduce(dbot.db.polls[key].votes,
function(memo, option) {
return memo += option;
}, 0);
res.render('polls', {
'name': dbot.config.name,
'description': dbot.db.polls[key].description,
'votees': Object.keys(dbot.db.polls[key].votees),
'options': dbot.db.polls[key].votes,
locals: {
'totalVotes': totalVotes,
'url_regex': RegExp.prototype.url_regex()
}
});
} else {
res.render('error', {
'name': dbot.config.name,
'message': 'No polls under that key.'
});
}
},
// Lists all of the polls
'/polls': function(req, res) {
res.render('polllist', {
'name': dbot.config.name,
'polllist': Object.keys(dbot.db.polls)
});
},
};
return pages;
};
exports.fetch = function(dbot) {
return pages(dbot);
};

View File

@ -1,229 +1,25 @@
var poll = function(dbot) { var poll = function(dbot) {
var polls = dbot.db.polls; this.internalAPI = {
var commands = { 'updatePollNicks': function(server, oldNick) {
'~newpoll': function(event) { var newNick = dbot.api.users.resolveUser(server, oldNick);
var av = event.input[1] != undefined; _.each(dbot.db.polls, function(poll) {
var name = event.input[2]; if(poll.owner === oldNick) {
var options = event.input[3].split(','); poll.owner = newNick;
var description = event.input[4];
if(name === undefined || name === 'help') {
event.reply(dbot.t('newpoll_usage'));
} else {
if(polls.hasOwnProperty(name)) {
event.reply(dbot.t('poll_exists', {'name': name}));
} else {
if(av) {
polls[name] = {
'av': av,
'name': name,
'description': description,
'owner': event.user,
'votes': {},
'options': []
};
for(var i=0;i<options.length;i++) {
polls[name].options.push(options[i]);
} }
} else { if(_.has(poll.votees, oldNick)) {
polls[name] = { poll.votees[newNick] = poll.votees[oldNick];
'av': av, delete poll.votees[oldNick];
'name': name,
'description': description,
'owner': event.user,
'votes': {},
'votees': {}
};
for(var i=0;i<options.length;i++) {
polls[name]['votes'][options[i]] = 0;
}
}
event.reply(dbot.t('poll_created', {'name': name, 'description': description,
'url': dbot.t('url', {'host': dbot.config.web.webHost,
'port': dbot.config.web.webPort, 'path': 'polls/' + name})}));
}
}
},
'~addoption': function(event) {
var name = event.input[1];
var option = event.input[2];
if(polls.hasOwnProperty(name)) {
if(polls[name].owner === event.user) {
if(!polls[name].votes.hasOwnProperty(name)) {
polls[name]['votes'][option] = 0;
event.reply(dbot.t('option_added', {'user': event.user,
'name': name, 'option': option}));
} else {
event.reply(dbot.t('option_exists', {'option': option,
'name': name, 'user': event.user}));
}
} else {
event.reply(dbot.t('not_poll_owner', {'user': event.user,
'name': name}));
}
} else {
event.reply(dbot.t('poll_unexistent', {'name': name}));
}
},
'~rmoption': function(event) {
var name = event.input[1];
var option = event.input[2];
if(polls.hasOwnProperty(name)) {
if(polls[name].owner === event.user) {
if(polls[name].votes.hasOwnProperty(option)) {
delete polls[name]['votes'][option];
event.reply(dbot.t('option_removed', {'user': event.user,
'name': name, 'option': option}));
} else {
event.reply(dbot.t('invalid_vote', {'vote': option}));
}
} else {
event.reply(dbot.t('not_poll_owner', {'name': name}));
}
} else {
event.reply(dbot.t('poll_unexistent', {'name': name}));
}
},
'~vote': function(event) {
var name = event.input[1];
var vote = event.input[2];
if(polls.hasOwnProperty(name)) {
if(polls[name].av) {
var prefs = vote.split(',');
prefs = prefs.uniq();
var valid = true;
prefs.each(function(pref) {
valid = valid && polls[name].options.indexOf(pref) != -1;
});
if(valid){
if(polls[name].votes.hasOwnProperty(event.user)) {
polls[name].votes[event.user] = prefs;
event.reply(dbot.t('av_changed_vote', {'vote': prefs.join(','), 'poll': name, 'user': event.user}));
} else {
polls[name].votes[event.user] = prefs;
event.reply(dbot.t('av_voted', {'vote': prefs.join(','), 'poll': name, 'user': event.user}));
}
} else {
event.reply(dbot.t('invalid_vote', {'vote': vote}));
}
} else {
if(polls[name].votes.hasOwnProperty(vote)) {
if(polls[name].votees.hasOwnProperty(event.user)) {
var oldVote = polls[name].votees[event.user];
polls[name].votes[oldVote]--;
polls[name].votes[vote]++;
polls[name].votees[event.user] = vote;
event.reply(dbot.t('changed_vote', {'vote': vote, 'poll': name,
'count': polls[name].votes[vote], 'user': event.user}));
} else {
polls[name].votes[vote]++;
polls[name].votees[event.user] = vote;
event.reply(dbot.t('voted', {'vote': vote, 'poll': name,
'count': polls[name].votes[vote], 'user': event.user}));
}
} else {
event.reply(dbot.t('invalid_vote', {'vote': vote}));
}
}
} else {
event.reply(dbot.t('poll_unexistent', {'name': name}));
}
},
'~pdesc': function(event) {
var name = event.input[1];
if(polls.hasOwnProperty(name)) {
event.reply(dbot.t('poll_describe', {'name': name, 'description': polls[name].description,
'url': dbot.t('url', {'host': dbot.config.web.webHost, 'port':
dbot.config.web.webPort, 'path': 'polls/' + name})}));
} else {
event.reply(dbot.t('poll_unexistent', {'name': name}));
}
},
'~count': function(event) {
var name = event.input[1];
if(polls.hasOwnProperty(name)) {
var order;
if(polls[name].av) {
var finished = false;
var rounds = [];
var eliminated = [];
var voted;
for(var roundn = 0; roundn < polls[name].options.length; roundn++) {
var roundLoser;
// Populate candidates for this round
rounds[roundn] = {};
polls[name].options.each(function (option) {
if(eliminated.indexOf(option) == -1)
rounds[roundn][option] = 0;
});
// Count votes
polls[name].votes.withAll(function (name, vote) {
voted = false;
vote.each(function (pref) {
if(!voted && rounds[roundn].hasOwnProperty(pref)) {
rounds[roundn][pref]++;
voted = true;
}
});
});
// Find the loser
var min = polls[name].votes.length() + 1;
rounds[roundn].withAll(function (option, count) {
if(count < min) {
roundLoser = option;
min = count;
}
});
// Eliminate loser
eliminated.push(roundLoser);
}
order = eliminated.reverse().join(', ')
} else {
var votesArr = [];
polls[name].votes.withAll(function(option, count) {
votesArr.push([option, count]);
});
votesArr = votesArr.sort(function(a, b) { return b[1] - a[1]; });
order = votesArr.map(function(vote) { return vote[0]; });
}
event.reply(dbot.t('count', {'poll': name, 'description': polls[name].description, 'places': order}));
} else {
event.reply(dbot.t('poll_unexistent', {'name': name}));
} }
}, this);
} }
}; };
commands['~newpoll'].regex = [/~newpoll (av )?([^ ]+) options=([^ ]+) (.+)/, 5];
commands['~addoption'].regex = [/~addoption ([^ ]+) ([^ ]+)/, 3];
commands['~rmoption'].regex = [/~rmoption ([^ ]+) ([^ ]+)/, 3];
commands['~vote'].regex = [/~vote ([^ ]+) ([^ ]+)/, 3];
commands['~pdesc'].regex = [/~pdesc ([^ ]+)/, 2];
commands['~count'].regex = [/~count ([^ ]+)/, 2];
return { this.onLoad = function() {
'name': 'poll', dbot.api.command.addHook('~setaliasparent', this.internalAPI.updatePollNicks);
'ignorable': true, dbot.api.command.addHook('~mergeusers', this.internalAPI.updatePollNicks);
'commands': commands }.bind(this);
};
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return poll(dbot); return new poll(dbot);
} }

View File

@ -17,7 +17,7 @@
"na'vi": "sìpawm sna'o '{name}' ngìyop ({description}). Nga tìpe'unit Pe'eiun - {url}" "na'vi": "sìpawm sna'o '{name}' ngìyop ({description}). Nga tìpe'unit Pe'eiun - {url}"
}, },
"poll_describe": { "poll_describe": {
"english": "{name}: {description} - {url}" "english": "{name}: {description} - {url}."
}, },
"changed_vote": { "changed_vote": {
"english": "{user} changed their vote in {poll} to '{vote}' ({count}).", "english": "{user} changed their vote in {poll} to '{vote}' ({count}).",

71
modules/profile/api.js Normal file
View File

@ -0,0 +1,71 @@
var _ = require('underscore')._;
var api = function(dbot) {
return {
/**
* Create a profile for a new primary user on a given server.
* If the server does not already exist, create it.
*/
"createProfile": function(server, primary){
var primaryLower = primary.toLowerCase();
if(!_.has(this.profiles, server)){
this.profiles[server] = {};
}
if(!_.has(this.profiles[server], primaryLower)){
this.profiles[server][primaryLower] = {
"profile": {},
"preferences": {}
};
this.profiles[server][primaryLower].profile.primary = primary;
}
// Ensure all profiles have the keys specified by config.json
//TODO(samstudio8) Currently only handles "top-level"
_.defaults(this.profiles[server][primaryLower].profile, this.config.schema.profile);
_.defaults(this.profiles[server][primaryLower].preferences, this.config.schema.preferences);
},
/**
* Given a server and "new" alias, resolve this alias to the user's
* new primary name and move profile data pertaining to the alias to
* the new primary name.
*/
'renameProfile': function(server, alias){
if(!_.has(this.profiles, server)) return;
var profiles = dbot.db.profiles[server];
if(_.has(profiles, alias)){
var primary = dbot.api.users.resolveUser(server, alias, true);
var primaryLower = primary.toLowerCase();
alias = alias.trim().toLowerCase();
profiles[primaryLower] = profiles[alias];
profiles[primaryLower].profile.primary = primary;
delete profiles[alias];
}
},
/**
* Given a server and a primary username which has been converted to a
* secondary alias find and remove the profile for the alias.
*/
'mergeProfile': function(server, mergeFromPrimary){
if(!_.has(this.profiles, server)) return;
var profiles = dbot.db.profiles[server];
mergeFromPrimary = mergeFromPrimary.toLowerCase();
var mergeToPrimary = dbot.api.users.resolveUser(server, mergeFromPrimary, true).toLowerCase();
if(!_.has(profiles, mergeToPrimary)
|| !_.has(profiles, mergeFromPrimary)) return;
// Remove the profile of the alias
delete profiles[mergeFromPrimary];
},
}
};
exports.fetch = function(dbot) {
return api(dbot);
};

View File

@ -0,0 +1,57 @@
var _ = require('underscore')._;
var commands = function(dbot){
var commands = {
"~getprop": function(event){
if(event.params[1]){
var primary = dbot.api.users.resolveUser(event.server, event.user);
var res = dbot.db.profiles[event.server][primary.toLowerCase()].profile[event.params[1]];
if(res){
event.reply(res);
}
else{
event.reply("Nope.");
}
}
},
"~setprop": function(event){
if(event.input[1] && event.input[2]){
if(_.has(this.config.schema.profile, event.input[1])){
var primary = dbot.api.users.resolveUser(event.server, event.user);
dbot.db.profiles[event.server][primary.toLowerCase()].profile[event.input[1]] = event.input[2];
event.reply("Property set, maybe?");
}
else{
event.reply("Invalid property. Go home.");
}
}
},
"~profile": function(event){
if(event.params[1]){
var primary = dbot.api.users.resolveUser(event.server, event.params[1]);
if(_.has(dbot.db.profiles[event.server], primary.toLowerCase())){
event.reply("http://"+dbot.config.web.webHost+":"+dbot.config.web.webPort+"/profile/"+event.server+"/"+primary.toLowerCase());
}
else{
event.reply("No profile found for "+event.params[1]);
}
}
else{
event.message = '~profile ' + event.user;
event.action = 'PRIVMSG';
event.params = event.message.split(' ');
dbot.instance.emit(event);
}
}
};
commands['~setprop'].regex = [/~setprop ([^ ]+) (.+)/, 3];
return commands;
};
exports.fetch = function(dbot){
return commands(dbot);
};

View File

@ -0,0 +1,21 @@
{
"ignorable": false,
"dbKeys": [ "profiles" ],
"help": "https://github.com/reality/depressionbot/blob/master/modules/profile/README.md",
"schema": {
"profile": {
"primary": null,
"name": null,
"tagline": null,
"avatar": null,
"bio": null,
"favourites": {
"colour": null
}
},
"preferences": {
"timezone": null
}
},
"dependencies": [ "quotes", "users", "command" ]
}

67
modules/profile/pages.js Normal file
View File

@ -0,0 +1,67 @@
var pages = function(dbot) {
var _ = require('underscore')._;
var connections = dbot.instance.connections;
return {
'/profile/:connection/:user': function(req, res) {
var connection = req.params.connection;
var user = dbot.cleanNick(req.params.user);
var primary = dbot.api.users.resolveUser(connection, user, true);
//var profile = dbot.api.profile.getProfile(primary);
var profile = dbot.db.profiles[connection][primary.toLowerCase()].profile;
var stats = dbot.api.stats.getUserChansStats(connection, primary.toLowerCase(), [
"lines", "words", "lincent", "wpl", "in_mentions"]
);
res.render('profile', {
'name': dbot.config.name,
'connection': connection,
'primary': primary,
'profile': profile,
'stats': stats.channels,
});
},
'/profile/:connection': function(req, res) {
var connection = req.params.connection;
var profiles = dbot.db.profiles[connection];
// TODO: Clean up
_.each(profiles, function(profile) {
if(_.has(dbot.db.quoteArrs, profile.profile.primary) && !profile.profile.avatar) {
var category = dbot.db.quoteArrs[profile.profile.primary];
var avatar = _.find(category, function(quote) {
return quote.match(/(\.jpg|\.png|\.jpeg)$/i);
});
if(avatar) profile.profile.avatar = avatar;
}
});
var nicks = [];
for (var p in profiles) {
if (profiles.hasOwnProperty(p) && profiles[p].profile.avatar) {
nicks.push(p);
}
}
nicks.sort(function(a, b) {
var x = profiles[a].profile.primary.toLowerCase();
var y = profiles[b].profile.primary.toLowerCase();
if(x > y) return 1;
if(x < y) return -1;
return 0;
});
res.render('profile_grid', {
'name': dbot.config.name,
'connection': connection,
'nicks': nicks,
'profiles': profiles,
});
}
}
};
exports.fetch = function(dbot) {
return pages(dbot);
};

View File

@ -0,0 +1,32 @@
var _ = require('underscore')._;
var profile = function(dbot) {
this.profiles = dbot.db.profiles;
/**
* Iterate over known user profiles and ensure they contain all the
* required properties as defined in the configuation.
*/
this.onLoad = function(){
var api = this.api;
var schema = this.config.schema;
// Ensure all known users have a profile
_.each(dbot.api.users.getAllUsers(), function(server, serverName){
_.each(server, function(primary, primaryi){
api.createProfile(serverName, primary);
});
});
dbot.save();
// Add API Hooks
dbot.api.command.addHook('~setaliasparent', this.api.renameProfile);
dbot.api.command.addHook('~mergeusers', this.api.mergeProfile);
dbot.api.event.addHook('new_user', this.api.createProfile);
};
};
exports.fetch = function(dbot) {
return new profile(dbot);
};

View File

@ -1,25 +0,0 @@
var puns = function(dbot) {
var name = 'puns';
var dbot = dbot;
return {
'name': name,
'ignorable': true,
'listener': function(event) {
event.user = dbot.cleanNick(event.user);
if(dbot.config.moduleNames.include('quotes') &&
dbot.db.quoteArrs.hasOwnProperty(event.user)) {
event.message = '~q ' + event.user;
event.action = 'PRIVMSG';
event.params = event.message.split(' ');
dbot.instance.emit(event);
}
},
'on': 'JOIN'
};
}
exports.fetch = function(dbot) {
return puns(dbot);
};

73
modules/quotes/README.md Normal file
View File

@ -0,0 +1,73 @@
## Quotes
Stores and displays quotes.
### Description
This is the original reason that DBot was created, stores and displays quotes.
### Configuration
#### rmLimit: 10
Amount of quotes which can be removed before admin approval is required.
### Commands
#### ~q [category]
Display a random quote from a given category.
#### ~qadd [category] = [quote]
Add a new quote to the database.
#### ~qstats
Show a list of the biggest quote categories.
#### ~qsearch [category] = [needle]
Search a category for quotes including the given text.
#### ~rmlast [category]
Remove the last quote added to a given category.
#### ~rmstatus
Show how many quotes are currently in the removal cache, and whether they will
be randomly removed.
#### ~rm [category] = [quote]
Remove a given quote from the given category.
#### ~qcount [category]
Show the number of quotes stored in the given category, or if called without a
category it will show the total number of quotes in the database.
#### ~rq
Show a random quote from the database.
#### ~link [category]
Show a link to the page on the web interface which shows this category's quotes.
### Admin-only Commands
#### ~rmconfirm
Confirm that the quotes currently in the removal cache are okay to be removed,
and permanently delete them.
#### ~rmdeny
Re-instate the quotes that are currently in the removal cache back into the main
quote database.
### API
#### getQuote(event, category)
Returns a random quote from the given category.
### Removal Spam Protection
When quotes are removed using either the ~rm or ~rmlast commands, the quotes are
removed from the main database, but are stored in a removal cache which is cleared
out ten minutes from the last time a quote was removed from the database. If the
number of quotes removed from the database reaches a certain limit (as per rmLimit
in config, default 10) then the counter is removed and the cache will not be deleted
automatically. In such a case, a DBot admin needs to either run the ~rmconfim command
to have the removal cache cleared, or ~rmdeny to re-instate all of the quotes in
the removal cache back into the main quote database. This is to stop mass
removal from the database without limiting the user interface.

226
modules/quotes/commands.js Normal file
View File

@ -0,0 +1,226 @@
var _ = require('underscore')._;
var commands = function(dbot) {
var quotes = dbot.db.quoteArrs;
var commands = {
// Alternative syntax to ~q
'~': function(event) {
commands['~q'].bind(this)(event);
},
'~rmstatus': function(event) {
var rmCacheCount = this.rmCache.length;
if(rmCacheCount < dbot.config.quotes.rmLimit) {
event.reply(dbot.t('quote_cache_auto_remove',
{ 'count': rmCacheCount }));
} else {
event.reply(dbot.t('quote_cache_manual_remove',
{ 'count': rmCacheCount }));
}
},
'~rmconfirm': function(event) {
var rmCacheCount = this.rmCache.length;
this.rmCache.length = 0;
event.reply(dbot.t('quote_cache_cleared',
{ 'count': rmCacheCount }));
},
'~rmdeny': function(event) {
var rmCache = this.rmCache;
var rmCacheCount = rmCache.length;
for(var i=0;i<rmCacheCount;i++) {
if(!_.has(quotes, rmCache[i].key)) {
quotes[rmCache[i].key] = [];
}
quotes[rmCache[i].key].push(rmCache[i].quote);
}
rmCache.length = 0;
event.reply(dbot.t('quote_cache_reinstated',
{ 'count': rmCacheCount }));
},
// Retrieve quote from a category in the database.
'~q': function(event) {
var key = event.input[1].trim().toLowerCase();
var quote = this.api.getQuote(event, event.input[1]);
if(quote) {
event.reply(key + ': ' + quote);
} else {
event.reply(dbot.t('category_not_found', {'category': key}));
}
},
// Shows a list of the biggest categories
'~qstats': function(event) {
var qSizes = _.chain(quotes)
.pairs()
.sortBy(function(category) { return category[1].length })
.reverse()
.first(10)
.value();
var qString = dbot.t('large_categories');
for(var i=0;i<qSizes.length;i++) {
qString += qSizes[i][0] + " (" + qSizes[i][1].length + "), ";
}
event.reply(qString.slice(0, -2));
},
// Search a given category for some text.
// TODO fix
'~qsearch': function(event) {
var haystack = event.input[1].trim().toLowerCase();
var needle = event.input[2];
if(_.has(quotes, haystack)) {
var matches = _.filter(quotes[haystack], function(quote) {
return _.indexOf(quote, needle) != -1;
}, this);
if(matches.length == 0) {
event.reply(dbot.t('no_results'));
} else {
event.reply(dbot.t('search_results', {
'category': haystack,
'needle': needle,
'quote': matches.random(),
'matches': matches.length
}));
}
} else {
event.reply(dbot.t('empty_category'));
}
},
'~rmlast': function(event) {
if(this.rmAllowed === true || _.include(dbot.config.admins, event.user)) {
var key = event.input[1].trim().toLowerCase();
if(_.has(quotes, key)) {
var quote = quotes[key].pop();
if(quotes[key].length == 0) {
delete quotes[key];
}
this.internalAPI.resetRemoveTimer(event, key, quote);
event.reply(dbot.t('removed_from', {
'quote': quote,
'category': key
}));
} else {
event.reply(dbot.t('no_quotes', {'category': q[1]}));
}
} else {
event.reply(dbot.t('rmlast_spam'));
}
},
'~rm': function(event) {
if(this.rmAllowed == true || _.include(dbot.config.admins, event.user)) {
var key = event.input[1].trim().toLowerCase();
var quote = event.input[2];
if(_.has(quotes, key)) {
var category = quotes[key];
var index = category.indexOf(quote);
if(index !== -1) {
category.splice(index, 1);
if(category.length === 0) {
delete quotes[key];
}
this.internalAPI.resetRemoveTimer(event, key, quote);
event.reply(dbot.t('removed_from', {'category': key, 'quote': quote}));
} else {
event.reply(dbot.t('q_not_exist_under', {'category': key, 'quote': quote}));
}
} else {
event.reply(dbot.t('category_not_found', {'category': key}));
}
} else {
event.reply(dbot.t('rmlast_spam'));
}
},
'~qcount': function(event) {
var input = event.message.valMatch(/^~qcount ([\d\w\s-]*)/, 2);
if(input) { // Give quote count for named category
var key = input[1].trim().toLowerCase();
if(_.has(quotes, key)) {
event.reply(dbot.t('quote_count', {
'category': key,
'count': quotes[key].length
}));
} else {
event.reply(dbot.t('no_quotes', { 'category': key }));
}
} else { // Give total quote count
var totalQuoteCount = _.reduce(quotes, function(memo, category) {
return memo + category.length;
}, 0);
event.reply(dbot.t('total_quotes', { 'count': totalQuoteCount }));
}
},
'~qadd': function(event) {
var key = event.input[1].toLowerCase();
var text = event.input[2];
if(!_.isArray(quotes[key])) {
quotes[key] = [];
}
if(_.include(quotes[key], text)) {
event.reply(dbot.t('quote_exists'));
} else {
quotes[key].push(text);
this.rmAllowed = true;
event.reply(dbot.t('quote_saved', {
'category': key,
'count': quotes[key].length
}));
return { 'key': key, 'text': text };
}
return false;
},
'~rq': function(event) {
var category = _.keys(quotes)[_.random(0, _.size(quotes) -1)];
event.reply(category + ': ' + this.internalAPI.interpolatedQuote(event.server, event.channel.name, category));
},
'~link': function(event) {
var key = event.params[1].trim().toLowerCase();
if(_.has(quotes, key)) {
event.reply(dbot.t('quote_link', {
'category': key,
'url': dbot.t('url', {
'host': dbot.config.web.webHost,
'port': dbot.config.web.webPort,
'path': 'quotes/' + key
})
}));
} else {
event.reply(dbot.t('category_not_found', { 'category': key }));
}
},
};
commands['~'].regex = [/^~([\d\w\s-]*)/, 2];
commands['~q'].regex = [/^~q ([\d\w\s-]*)/, 2];
commands['~qsearch'].regex = [/^~qsearch ([\d\w\s-]+?)[ ]?=[ ]?(.+)$/, 3];
commands['~rm'].regex = [/^~rm ([\d\w\s-]+?)[ ]?=[ ]?(.+)$/, 3];
commands['~rmlast'].regex = [/^~rmlast ([\d\w\s-]*)/, 2];
commands['~qadd'].regex = [/^~qadd ([\d\w\s-]+?)[ ]?=[ ]?(.+)$/, 3];
commands['~rmconfirm'].access = 'moderator';
commands['~rmdeny'].access = 'moderator';
return commands;
};
exports.fetch = function(dbot) {
return commands(dbot);
};

View File

@ -1,3 +1,7 @@
{ {
"dbKeys": [ "quoteArrs" ] "dbKeys": [ "quoteArrs" ],
"dependencies": [ "command", "users" ],
"rmLimit": 10,
"ignorable": true,
"help": "http://github.com/reality/depressionbot/blob/master/modules/quotes/README.md"
} }

29
modules/quotes/pages.js Normal file
View File

@ -0,0 +1,29 @@
var _ = require('underscore')._;
var pages = function(dbot) {
return {
// Lists quotes in a category
'/quotes/:key': function(req, res) {
var key = req.params.key.toLowerCase();
if(_.has(dbot.db.quoteArrs, key)) {
res.render('quotes', { 'name': dbot.config.name, 'quotes': dbot.db.quoteArrs[key], locals: { 'url_regex': RegExp.prototype.url_regex() } });
} else {
res.render('error', { 'name': dbot.config.name, 'message': 'No quotes under that key.' });
}
},
// Show quote list.
'/quotes': function(req, res) {
res.render('quotelist', { 'name': dbot.config.name, 'quotelist': Object.keys(dbot.db.quoteArrs) });
},
// Load random quote category page
'/rq': function(req, res) {
var rCategory = Object.keys(dbot.db.quoteArrs).random();
res.render('quotes', { 'name': dbot.config.name, 'quotes': dbot.db.quoteArrs[rCategory], locals: { 'url_regex': RegExp.prototype.url_regex() } });
}
}
};
exports.fetch = function(dbot) {
return pages(dbot);
};

View File

@ -1,19 +1,25 @@
var quotes = function(dbot) { var _ = require('underscore')._;
var name = 'quotes';
var quotes = dbot.db.quoteArrs;
var addStack = [];
var rmAllowed = true;
var quotes = function(dbot) {
dbot.sessionData.rmCache = [];
this.quotes = dbot.db.quoteArrs,
this.addStack = [],
this.rmAllowed = true,
this.rmCache = dbot.sessionData.rmCache,
this.rmTimer;
this.internalAPI = {
// Retrieve a random quote from a given category, interpolating any quote // Retrieve a random quote from a given category, interpolating any quote
// references (~~QUOTE CATEGORY~~) within it // references (~~QUOTE CATEGORY~~) within it
var interpolatedQuote = function(key, quoteTree) { 'interpolatedQuote': function(server, channel, key, quoteTree) {
if(quoteTree !== undefined && quoteTree.indexOf(key) != -1) { if(!_.isUndefined(quoteTree) && quoteTree.indexOf(key) != -1) {
return ''; return '';
} else if(quoteTree === undefined) { } else if(_.isUndefined(quoteTree)) {
quoteTree = []; quoteTree = [];
} }
var quoteString = quotes[key].random(); var index = _.random(0, this.quotes[key].length - 1);
var quoteString = this.quotes[key][index];
// Parse quote interpolations // Parse quote interpolations
var quoteRefs = quoteString.match(/~~([\d\w\s-]*)~~/g); var quoteRefs = quoteString.match(/~~([\d\w\s-]*)~~/g);
@ -21,204 +27,67 @@ var quotes = function(dbot) {
while(quoteRefs && (thisRef = quoteRefs.shift()) !== undefined) { while(quoteRefs && (thisRef = quoteRefs.shift()) !== undefined) {
var cleanRef = dbot.cleanNick(thisRef.replace(/^~~/,'').replace(/~~$/,'').trim()); var cleanRef = dbot.cleanNick(thisRef.replace(/^~~/,'').replace(/~~$/,'').trim());
if (quotes.hasOwnProperty(cleanRef)) { if(cleanRef === '-nicks-') {
var randomNick = dbot.api.users.getRandomChannelUser(server, channel);
quoteString = quoteString.replace("~~" + cleanRef + "~~", randomNick);
quoteTree.pop();
} else if(_.has(this.quotes, cleanRef)) {
quoteTree.push(key); quoteTree.push(key);
quoteString = quoteString.replace("~~" + cleanRef + "~~", quoteString = quoteString.replace("~~" + cleanRef + "~~",
interpolatedQuote(cleanRef, quoteTree.slice())); this.internalAPI.interpolatedQuote(server, channel, cleanRef, quoteTree.slice()));
quoteTree.pop(); quoteTree.pop();
} }
} }
return quoteString; return quoteString;
}.bind(this),
'resetRemoveTimer': function(event, key, quote) {
this.rmAllowed = false;
setTimeout(function() {
this.rmAllowed = true;
}.bind(this), 5000);
this.rmCache.push({
'key': key,
'quote': quote
});
clearTimeout(this.rmTimer);
if(this.rmCache.length < dbot.config.quotes.rmLimit) {
this.rmTimer = setTimeout(function() {
this.rmCache.length = 0; // lol what
}.bind(this), 600000);
} else {
_.each(dbot.config.admins, function(admin) {
dbot.say(event.server, admin, dbot.t('rm_cache_limit'));
});
}
}.bind(this)
}; };
var commands = { this.api = {
// Alternative syntax to ~q 'getQuote': function(event, category) {
'~': function(event) { var key = category.trim().toLowerCase();
commands['~q'](event);
},
// Retrieve quote from a category in the database.
'~q': function(event) {
var key = event.input[1].trim().toLowerCase();
var altKey; var altKey;
if(key.split(' ').length > 0) { if(key.split(' ').length > 0) {
altKey = key.replace(/ /g, '_'); altKey = key.replace(/ /g, '_');
} }
if(key.charAt(0) !== '_') { // lol if(key.charAt(0) !== '_') { // lol
if(quotes.hasOwnProperty(key)) { if(_.has(this.quotes, key)) {
event.reply(key + ': ' + interpolatedQuote(key)); return this.internalAPI.interpolatedQuote(event.server, event.channel.name, key);
} else if(quotes.hasOwnProperty(altKey)) { } else if(_.has(this.quotes, altKey)) {
event.reply(altKey + ': ' + interpolatedQuote(altKey)); return this.internalAPI.interpolatedQuote(event.server, event.channel.name, altKey);
} else { } else {
event.reply(dbot.t('category_not_found', {'category': key})); return false;
} }
} }
},
// Shows a list of the biggest categories
'~qstats': function(event) {
var qSizes = Object.prototype.sort(quotes, function(key, obj) { return obj[key].length });
qSizes = qSizes.slice(qSizes.length - 10).reverse();
var qString = dbot.t('large_categories');
for(var i=0;i<qSizes.length;i++) {
qString += qSizes[i][0] + " (" + qSizes[i][1] + "), ";
} }
event.reply(qString.slice(0, -2));
},
// Search a given category for some text.
'~qsearch': function(event) {
var haystack = event.input[1].trim().toLowerCase();
var needle = event.input[2];
if(quotes.hasOwnProperty(haystack)) {
var matches = [];
quotes[haystack].each(function(quote) {
if(quote.indexOf(needle) != -1) {
matches.push(quote);
}
}.bind(this));
if(matches.length == 0) {
event.reply(dbot.t('no_results'));
} else {
event.reply(dbot.t('search_results', {'category': haystack, 'needle': needle,
'quote': matches.random(), 'matches': matches.length}));
}
} else {
event.reply(dbot.t('empty_category'));
}
},
'~rmlast': function(event) {
if(rmAllowed == true || dbot.config.admins.include(event.user)) {
var key = event.input[1].trim().toLowerCase();
if(quotes.hasOwnProperty(key)) {
if(!dbot.db.locks.include(key) || dbot.config.admins.include(event.user)) {
var quote = quotes[key].pop();
if(quotes[key].length === 0) {
delete quotes[key];
}
rmAllowed = false;
event.reply(dbot.t('removed_from', {'quote': quote, 'category': key}));
} else {
event.reply(dbot.t('locked_category', {'category': q[1]}));
}
} else {
event.reply(dbot.t('no_quotes', {'category': q[1]}));
}
} else {
event.reply(dbot.t('rmlast_spam'));
}
},
'~rm': function(event) {
if(rmAllowed == true || dbot.config.admins.include(event.user)) {
var key = event.input[1].trim().toLowerCase();
var quote = event.input[2];
if(quotes.hasOwnProperty(key)) {
if(!dbot.db.locks.include(key)) {
var category = quotes[key];
var index = category.indexOf(quote);
if(index !== -1) {
category.splice(index, 1);
if(category.length === 0) {
delete quotes[key];
}
event.reply(dbot.t('removed_from', {'category': key, 'quote': quote}));
} else {
event.reply(dbot.t('q_not_exist_under', {'category': key, 'quote': quote}));
}
} else {
event.reply(dbot.t('locked_category', {'category': key}));
}
} else {
event.reply(dbot.t('category_not_found', {'category': key}));
}
} else {
event.reply(dbot.t('rmlast_spam'));
}
},
'~qcount': function(event) {
var input = event.message.valMatch(/^~qcount ([\d\w\s-]*)/, 2);
if(input) { // Give quote count for named category
var key = input[1].trim().toLowerCase();
if(quotes.hasOwnProperty(key)) {
event.reply(dbot.t('quote_count', {'category': key, 'count': quotes[key].length}));
} else {
event.reply(dbot.t('no_quotes', {'category': key}));
}
} else { // Give total quote count
var totalQuoteCount = 0;
for(var category in quotes) {
if(quotes.hasOwnProperty(category)) {
totalQuoteCount += quotes[category].length;
}
}
event.reply(dbot.t('total_quotes', {'count': totalQuoteCount}));
}
},
'~qadd': function(event) {
var key = event.input[1].toLowerCase();
var text = event.input[2];
if(!Object.isArray(quotes[key])) {
quotes[key] = [];
}
if(quotes[key].include(text)) {
event.reply(dbot.t('quote_exists'));
} else {
quotes[key].push(text);
rmAllowed = true;
event.reply(dbot.t('quote_saved', {'category': key, 'count': quotes[key].length}));
}
},
'~rq': function(event) {
var rQuote = Object.keys(quotes).random();
event.reply(rQuote + ': ' + interpolatedQuote(rQuote));
},
'~link': function(event) {
var key = event.params[1].trim().toLowerCase();
if(quotes.hasOwnProperty(key)) {
event.reply(dbot.t('quote_link', {'category': key,
'url': dbot.t('url', {'host': dbot.config.web.webHost,
'port': dbot.config.web.webPort, 'path': 'quotes/' + key})}));
} else {
event.reply(dbot.t('category_not_found', {'category': key}));
}
},
}; };
commands['~'].regex = [/^~([\d\w\s-]*)/, 2]; this.listener = function(event) {
commands['~q'].regex = [/^~q ([\d\w\s-]*)/, 2]; if(event.action == 'PRIVMSG') {
commands['~qsearch'].regex = [/^~qsearch ([\d\w\s-]+?)[ ]?=[ ]?(.+)$/, 3];
commands['~rm'].regex = [/^~rm ([\d\w\s-]+?)[ ]?=[ ]?(.+)$/, 3];
commands['~rmlast'].regex = [/^~rmlast ([\d\w\s-]*)/, 2];
commands['~qadd'].regex = [/^~qadd ([\d\w\s-]+?)[ ]?=[ ]?(.+)$/, 3];
return {
'name': 'quotes',
'ignorable': true,
'commands': commands,
'onLoad': function() {
dbot.timers.addTimer(1000 * 60 * 3, function() {
rmAllowed = true;
});
},
'listener': function(event) {
// Reality Once listener
if((dbot.db.ignores.hasOwnProperty(event) &&
dbot.db.ignores[event.user].include(name)) == false) {
if(event.user == 'reality') { if(event.user == 'reality') {
var once = event.message.valMatch(/^I ([\d\w\s,'-]* once)/, 2); var once = event.message.valMatch(/^I ([\d\w\s,'-]* once)/, 2);
} else { } else {
@ -226,31 +95,21 @@ var quotes = function(dbot) {
} }
if(once) { if(once) {
if((dbot.db.bans.hasOwnProperty('~qadd') && event.message = '~qadd realityonce=reality ' + once[1];
dbot.db.bans['~qadd'].include(event.user)) || event.action = 'PRIVMSG';
dbot.db.bans['*'].include(event.user)) { event.params = event.message.split(' ');
event.reply(dbot.t('command_ban', {'user': event.user})); dbot.instance.emit(event);
} else {
if(!dbot.db.quoteArrs.hasOwnProperty('realityonce')) {
dbot.db.quoteArrs['realityonce'] = [];
} }
if(dbot.db.quoteArrs['realityonce'].include('reality ' + once[1] + '.')) { } else if(event.action == 'JOIN') {
event.reply(event.user + ': reality has already done that once.'); var userQuote = this.api.getQuote(event, event.user)
} else { if(userQuote) {
dbot.db.quoteArrs['realityonce'].push('reality ' + once[1] + '.'); event.reply(event.user + ': ' + this.api.getQuote(event, event.user));
addStack.push('realityonce');
rmAllowed = true;
event.reply('\'reality ' + once[1] + '.\' saved.');
} }
} }
} }.bind(this);
} this.on = ['PRIVMSG', 'JOIN'];
},
'on': 'PRIVMSG'
};
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return quotes(dbot); return new quotes(dbot);
}; };

View File

@ -106,5 +106,25 @@
"spanish" : "{category} ({needle}): '{quote}' [{matches} resultados]", "spanish" : "{category} ({needle}): '{quote}' [{matches} resultados]",
"na'vi": "{category} ({needle}): '{quote}' [kum a{matches}]", "na'vi": "{category} ({needle}): '{quote}' [kum a{matches}]",
"welsh": "{category} ({needle}): '{quote}' [{matches} canlyniad]" "welsh": "{category} ({needle}): '{quote}' [{matches} canlyniad]"
},
"quote_cache_auto_remove": {
"english": "There are {count} quotes in the removal cache, which will be automatically cleared.",
"na'vi": "{count}a 'upxarel sngelit tok, Oel 'ayku sngelit lukenga."
},
"quote_cache_manual_remove": {
"english": "There are {count} quotes in the removal cache, which must be manually cleared.",
"na'vi": "{count}a 'upxarel sngelit tok slä oel ke 'ayku sngelit tafral nga zene 'aivku"
},
"quote_cache_cleared": {
"english": "{count} quotes cleared from the removal cache.",
"na'vi": "Oel 'aìmku {count}a 'upxareti ta sngel."
},
"quote_cache_reinstated": {
"english": "{count} quotes reinstated from the removal cache.",
"na'vi": "{count}a 'upxare tolätxaw ta sngel."
},
"rm_cache_limit": {
"english": "Attention: Too many quotes removed, rmCache must be cleared or reinstated manually with ~rmconfirm or ~rmdeny.",
"na'vi": "Oel zerok 'upxareti apxay set, sweylu txo nga 'aivku upxareti ìlä ~rmconfirm fu ~rmdeny."
} }
} }

21
modules/regex/README.md Normal file
View File

@ -0,0 +1,21 @@
## Regex
Apply regex and that.
### Description
Allows you to run regex replaces on both your own and others messages. One may
run a regex on their own last message like so:
> user: I like turtles
> user: s/turtles/pizza/
One may run a regex on another user's last message simple by hilighting the nick
before the pattern:
> batman: I like TURTLES
> user: batman: s/turtles/pizza/i
Note: As this is JS regex, the second part of the regex is actually just a
string and therefore some regex features aren't available (such as lookaheads).
On a related note, the regex flags available for use are limited to i and g.

View File

@ -0,0 +1,4 @@
{
"ignorable": true,
"help": "http://github.com/reality/depressionbot/blob/master/modules/regex/README.md"
}

40
modules/regex/regex.js Normal file
View File

@ -0,0 +1,40 @@
var _ = require('underscore')._;
var regex = function(dbot) {
this.last = {};
this.listener = function(event) {
var q = event.message.valMatch(/^([\d\w\s]*)?:? ?s\/(.+)\/(.+)?\/([ig]*)?$/, 5);
if(q) {
var flags = q[4],
toMatch = new RegExp(q[2], flags),
replaceWith = q[3],
last,
replacement;
if(!replaceWith) replaceWith = "";
if(q[1] != null) {
var user = q[1];
last = this.last[event.channel.name][user];
replacement = last.replace(toMatch, replaceWith);
if(replacement != last) event.reply(event.user + " thinks " + user + " meant: " + replacement);
} else {
last = this.last[event.channel.name][event.user];
replacement = last.replace(toMatch, replaceWith);
if(replacement != last) event.reply(event.user + " meant: " + replacement);
}
} else {
if(_.has(this.last, event.channel.name)) {
this.last[event.channel.name][event.user] = event.message;
} else {
this.last[event.channel.name] = { };
this.last[event.channel.name][event.user] = event.message;
}
}
}.bind(this);
this.on = [ 'PRIVMSG' ];
};
exports.fetch = function(dbot) {
return new regex(dbot);
};

19
modules/report/README.md Normal file
View File

@ -0,0 +1,19 @@
## Report
Report users
### Description
This module provides a command which allows users to report other users in a
channel to the operators of the channel, as well as posting an alert in the
administrative channel. It can be done either anonymously or publicly in the
channel.
### Commands
#### ~report [#channel] [username] [reason for reporting]
Report a user in a channel for a reason. This command can either be run publicly
in a channel or anonymously in a PM to the bot. The result of using this command
will be that all of the users which are currently marked as operators in the
reporting channel will receive a PM telling them a user has been reported, by
whom, in which channel and why. If there is an administrative channel for the
reporting channel (e.g. ##channel), the report will be posted there as well.

View File

@ -0,0 +1,5 @@
{
"ignorable": true,
"dependencies": [ "command", "users" ],
"help": "http://github.com/reality/depressionbot/blob/master/modules/report/README.md"
}

View File

@ -1,3 +1,5 @@
var _ = require('underscore')._;
var report = function(dbot) { var report = function(dbot) {
var commands = { var commands = {
'~report': function(event) { '~report': function(event) {
@ -5,47 +7,37 @@ var report = function(dbot) {
var nick = event.input[2]; var nick = event.input[2];
var reason = event.input[3]; var reason = event.input[3];
if(event.allChannels.hasOwnProperty(channelName)) { if(_.has(event.allChannels, channelName)) {
var channel = event.allChannels[channelName]; var channel = event.allChannels[channelName];
if(channel.nicks.hasOwnProperty(nick)) { if(dbot.api.users.isChannelUser(event.server, nick, channelName, true)) {
var ops = []; var nick = dbot.api.users.resolveUser(event.server, nick, true);
for(var possibOps in channel.nicks) { var ops = _.filter(channel.nicks, function(user) {
if(channel.nicks[possibOps].op == true) { return user.op;
ops.push(possibOps); });
}
}
// Does the channel have an admin channel? _.each(ops, function(user) {
if(event.allChannels.hasOwnProperty('#' + channelName)) { dbot.say(event.server, user.name, dbot.t('report', {
ops.push('#' + channelName); 'reporter': event.user,
} 'reported': nick,
'channel': channelName,
'reason': reason
}));
}, this);
for(var i=0;i<ops.length;i++) { event.reply(dbot.t('reported', { 'reported': nick }));
dbot.say(event.server, ops[i],
'Attention: ' + event.user + ' has reported ' +
nick + ' in ' + channelName + '. The reason ' +
'given was: "' + reason + '."');
}
event.reply('Thank you, ' + nick + ' has been reported the channel administrators.');
} else { } else {
event.reply('User is not in that channel.'); event.reply(dbot.t('user_not_found', { 'reported': nick,
'channel': channelName }));
} }
} else { } else {
event.reply('I am not in that channel.'); event.reply(dbot.t('not_in_channel', { 'channel': channelName }));
} }
} }
}; };
commands['~report'].regex = [/^~report ([^ ]+) ([^ ]+) (.+)$/, 4]; commands['~report'].regex = [/^~report ([^ ]+) ([^ ]+) (.+)$/, 4];
this.commands = commands;
return {
'name': 'report',
'ignorable': true,
'commands': commands
};
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return report(dbot); return new report(dbot);
}; };

View File

@ -0,0 +1,18 @@
{
"report": {
"english": "Attention: {reporter} has reported {reported} in {channel}. The reason given was: \"{reason}.\"",
"na'vi": "{reporter}ìl fpìl futa {reported} fe' lu taluna {reason}."
},
"reported": {
"english": "Thank you, {reported} has been reported to the channel administrators.",
"na'vi": "Irayo si ngari, fìtsengìri ayeyktan omum teri {reported}it set."
},
"user_not_found": {
"english": "{reported} isn't a known user in {channel}.",
"na'vi": "Oel ke omum {reported}it mì {channel}."
},
"not_in_channel": {
"english": "I am not present in {channel}.",
"na'vi": "Oel {channel}it ke tok."
}
}

View File

@ -0,0 +1,22 @@
## Spelling
Fix your spelling.
### Description
Will attempt to correct a users' spelling by using the levenshtein distance
algorithm. One corrects the spelling of their previous message by simply posting
a message with their correction and an asterisk:
> user: I am a tutrle.
> user: *turtle
user meant: I am a turtle.
The regular expression for this module also accepts two asterisks at the
beginning of the correction, or at the end; it also accepts several words as the
correction and deals with these fairly intelligently. Users may also attempt
to correct another users like so:
> userone: I am a tutrle.
> usertwo: userone: *turtle
> usertwo thinks userone meant: I am a turtle.

View File

@ -0,0 +1,4 @@
{
"ignorable": true,
"help": "http://github.com/reality/depressionbot/blob/master/modules/spelling/README.md"
}

View File

@ -1,8 +1,84 @@
var spelling = function(dbot) { var _ = require('underscore')._;
var last = {};
var allGroupings = function(arr) {
if (arr.length == 0) {
return []; /* short-circuit the empty-array case */
}
var groupings = [];
for(var n=1;n<=arr.length;n++) {
for(var i=0;i<(arr.length-(n-1));i++) {
groupings.push(arr.slice(i, i+n));
}
}
return groupings;
}
var distance = function(s1, s2) {
// Calculate Levenshtein distance between two strings
//
// version: 1109.2015
// discuss at: http://phpjs.org/functions/levenshtein
// + original by: Carlos R. L. Rodrigues (http://www.jsfromhell.com)
// + bugfixed by: Onno Marsman
// + revised by: Andrea Giammarchi (http://webreflection.blogspot.com)
// + reimplemented by: Brett Zamir (http://brett-zamir.me)
// + reimplemented by: Alexander M Beedie
if (s1 == s2) {
return 0;
}
var s1_len = s1.length;
var s2_len = s2.length;
if (s1_len === 0) {
return s2_len; }
if (s2_len === 0) {
return s1_len;
}
// BEGIN STATIC
var split = false;
try {
split = !('0')[0];
} catch (e) {
split = true; // Earlier IE may not support access by string index
}
// END STATIC
if (split) {
s1 = s1.split(''); s2 = s2.split('');
}
var v0 = new Array(s1_len + 1);
var v1 = new Array(s1_len + 1);
var s1_idx = 0,
s2_idx = 0,
cost = 0;
for (s1_idx = 0; s1_idx < s1_len + 1; s1_idx++) { v0[s1_idx] = s1_idx;
}
var char_s1 = '',
char_s2 = '';
for (s2_idx = 1; s2_idx <= s2_len; s2_idx++) { v1[0] = s2_idx;
char_s2 = s2[s2_idx - 1];
for (s1_idx = 0; s1_idx < s1_len; s1_idx++) {
char_s1 = s1[s1_idx]; cost = (char_s1 == char_s2) ? 0 : 1;
var m_min = v0[s1_idx + 1] + 1;
var b = v1[s1_idx] + 1;
var c = v0[s1_idx] + cost;
if (b < m_min) { m_min = b;
}
if (c < m_min) {
m_min = c;
} v1[s1_idx + 1] = m_min;
}
var v_tmp = v0;
v0 = v1;
v1 = v_tmp; }
return v0[s1_len];
};
var spelling = function(dbot) {
this.last = {};
this.internalAPI = {};
this.internalAPI.correct = function (event, correction, candidate, output_callback) {
var rawCandidates = allGroupings(this.last[event.channel.name][candidate].split(' '));
var correct = function (event, correction, candidate, output_callback) {
var rawCandidates = last[event.channel.name][candidate].split(' ').allGroupings();
var candidates = []; var candidates = [];
for(var i=0;i<rawCandidates.length;i++) { for(var i=0;i<rawCandidates.length;i++) {
candidates.push(rawCandidates[i].join(' ')); candidates.push(rawCandidates[i].join(' '));
@ -11,20 +87,20 @@ var spelling = function(dbot) {
var winnerDistance = Infinity; var winnerDistance = Infinity;
for(var i=0;i<candidates.length;i++) { for(var i=0;i<candidates.length;i++) {
var distance = String.prototype.distance(correction.toLowerCase(), candidates[i].toLowerCase()); var d = distance(correction.toLowerCase(), candidates[i].toLowerCase());
if((distance < winnerDistance) && (distance > 0)) { if((d < winnerDistance) && (d > 0)) {
winner = candidates[i]; winner = candidates[i];
winnerDistance = distance; winnerDistance = d;
} }
} }
if(winnerDistance < Math.ceil(winner.length * 1.33)) { if(winnerDistance < Math.ceil(winner.length * 1.33)) {
if(winner !== correction) { if(winner !== correction) {
var fix = last[event.channel.name][candidate].replace(winner, correction); var fix = this.last[event.channel.name][candidate].replace(winner, correction);
if (/^.ACTION/.test(fix)) { if (/^.ACTION/.test(fix)) {
fix = fix.replace(/^.ACTION/, '/me'); fix = fix.replace(/^.ACTION/, '/me');
} }
last[event.channel.name][candidate] = fix; this.last[event.channel.name][candidate] = fix;
var output = { var output = {
'fix': fix, 'fix': fix,
'correcter': event.user, 'correcter': event.user,
@ -33,36 +109,31 @@ var spelling = function(dbot) {
output_callback(output); output_callback(output);
} }
} }
} }.bind(this);
return { this.listener = function(event) {
'name': 'spelling',
'ignorable': true,
'listener': function(event) {
var q = event.message.valMatch(/^(?:\*\*?([\d\w\s']*)|([\d\w\s']*)\*\*?)$/, 3); var q = event.message.valMatch(/^(?:\*\*?([\d\w\s']*)|([\d\w\s']*)\*\*?)$/, 3);
var otherQ = event.message.valMatch(/^([\d\w\s]*): (?:\*\*?([\d\w\s']*)|([\d\w\s']*)\*\*?)$/, 4); var otherQ = event.message.valMatch(/^([\d\w\s]*)[:|,] (?:\*\*?([\d\w\s']*)|([\d\w\s']*)\*\*?)$/, 4);
if(q) { if(q) {
correct(event, q[1] || q[2], event.user, function (e) { this.internalAPI.correct(event, q[1] || q[2], event.user, function (e) {
event.reply(dbot.t('spelling_self', e)); event.reply(dbot.t('spelling_self', e));
}); });
} else if(otherQ) { } else if(otherQ) {
correct(event, otherQ[2] || otherQ[3], otherQ[1], function (e) { this.internalAPI.correct(event, otherQ[2] || otherQ[3], otherQ[1], function (e) {
event.reply(dbot.t('spelling_other', e)); event.reply(dbot.t('spelling_other', e));
}); });
} else { } else {
if(last.hasOwnProperty(event.channel.name)) { if(_.has(this.last, event.channel.name)) {
last[event.channel.name][event.user] = event.message; this.last[event.channel.name][event.user] = event.message;
} else { } else {
last[event.channel.name] = { }; this.last[event.channel.name] = { };
last[event.channel.name][event.user] = event.message; this.last[event.channel.name][event.user] = event.message;
} }
} }
}, }.bind(this);
'on': 'PRIVMSG' this.on = 'PRIVMSG';
}
} }
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return spelling(dbot); return new spelling(dbot);
}; };

1
modules/stats Submodule

@ -0,0 +1 @@
Subproject commit ea795e4d17aa500923468366e73a10f6fbc94ade

32
modules/timers/README.md Normal file
View File

@ -0,0 +1,32 @@
## Timers
Timers for fun and profit.
### Description
This is a utility module which allows other modules to more easily use timers to
execute functionality, as well as providing simple cron-type functionality for
timers.
### API
#### addTimer(interval, callback, [firstDate])
Execute the given callback every time *interval* (in ms) passes.
The firstDate parameter is a Date object used to sync a timer to a given point in
time, allowing for cron-type functionality. For example, if you wanted to call a
given function every day at 00:00, you would do the following:
dbot.api.timers.addTimer(myCallback, 86400000, new Date([midnight tonight]));
This works like so:
1. Create a one-time timeout to be executed at the given firstDate (00:00, as
above).
2. Upon this timeout, your callback is executed. Then addTimer is called again
without the firstDate parameter, thus syncing a daily timer to be executed every
day at 00:00.
The best place to create timers is in your module's onLoad function. Using this,
you may essentially create persistent jobs to be run at regular intervals while
your module is loaded..

54
modules/timers/timers.js Normal file
View File

@ -0,0 +1,54 @@
/**
* Module Name: Timers
* Description: Persistent timers and shit
*/
var _ = require('underscore')._;
var timers = function(dbot) {
this.timers = dbot.db.timers;
this.runningTimeouts = [];
this.runningIntervals = [];
this.api = {
'addTimer': function(timeout, callback, firstDate) {
var now = new Date().getTime();
if(firstDate) {
console.log('Setting first timer to run at ' + firstDate);
firstTimeout = firstDate.getTime() - now;
this.runningTimeouts.push(setTimeout(function() {
console.log('Running first timer at ' + new Date().toUTCString());
this.runningIntervals.push(this.api.addTimer(timeout, callback));
try {
callback();
} catch(err) {
console.log('Callback failed: ' + err);
}
}.bind(this), firstTimeout));
} else {
this.runningIntervals.push(setInterval(function() {
console.log('Running subsequent timer at ' + new Date().toUTCString());
try {
callback();
} catch(err) {
console.log('Callback failed: ' + err);
}
}.bind(this), timeout));
}
}
};
this.onDestroy = function() {
for(var i=0;i<this.runningTimeouts.length;i++) {
console.log('destroying ' +this.runningTimeouts[i]);
clearTimeout(this.runningTimeouts[i]);
}
for(i=0;i<this.runningIntervals.length;i++) {
console.log('destroying ' +this.runningIntervals[i]);
clearInterval(this.runningIntervals[i]);
}
}.bind(this);
};
exports.fetch = function(dbot) {
return new timers(dbot);
};

56
modules/users/README.md Normal file
View File

@ -0,0 +1,56 @@
## Users
Track users.
### Description
This module tracks users and their aliases through nick changes and all that
kind of thing. It's mainly a utility module for other modules to use. It's
also totally !insaned.
### Commands
#### ~alias [user]
If an alias is provided, this command will return the primary user for which
this is an alias for. If a primary user is provided, it will return a
confirmation of this fact and a count of how many aliases belong to the user.
#### ~setaliasparent [newparent]
Set a nick which is currently serving as an alias to the primary user, while
setting what was previously the primary user as an alias of the new primary
user. Requires moderator level access by default.
#### ~mergeusers [primaryuser] [secondaryuser]
This command merges two nicks which are recorded as primary users into one user.
The secondary user and all of their aliases will be merged under primaryuser.
Requires moderator level access by default.
### API
#### resolveUser(server, nick, [useLowerCase])
This resolves a given nick to its primary user and returns it.
Note that if the useLowerCase argument is set to true, it will do a lower-case
search, however it will return the username in its properly capitalised form, so
remember to lower case the return value if you are using lower case values as
keys.
#### resolveUser(server, user)
Return whether a user is known either as an alias or a primary user.
#### isPrimaryUser(server, nick)
Return whether a nick is known as a primary user.
#### getAliases(server, user)
Return a list of aliases for a given primary user.
#### isOnline(server, user, channel, useLowerCase)
Return whether a user is online in a given channel on the given server.
### Events
#### nick_changed(server, newNick)
This is executed when a new alias is added for a user.
#### new_user(server, nick)
This is executed when a new primary user is added to the known users DB.

108
modules/users/api.js Normal file
View File

@ -0,0 +1,108 @@
var _ = require('underscore')._;
var api = function(dbot) {
var escapeRegexen = function(str) {
return (str+'').replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1");
};
var api = {
'resolveUser': function(server, nick, useLowerCase) {
var knownUsers = this.getServerUsers(server);
var user = nick;
if(!useLowerCase) {
if(!_.include(knownUsers.users, nick) && _.has(knownUsers.aliases, nick)) {
user = knownUsers.aliases[nick];
}
} else {
// this is retarded
user = user.toLowerCase();
var toMatch = new RegExp("^" + escapeRegexen(user) + "$", "i");
var resolvedUser = _.find(knownUsers.users, function(nick) {
return nick.match(toMatch) !== null;
}, this);
if(!resolvedUser) {
resolvedUser = _.find(knownUsers.aliases, function(nick, alias) {
if(alias.match(toMatch) !== null) return nick;
}, this);
if(!_.isUndefined(resolvedUser)) user = resolvedUser;
}
else{
user = resolvedUser;
}
}
return user;
},
'getRandomChannelUser': function(server, channel) {
var channelUsers = this.getServerUsers(server).channelUsers[channel];
if(!_.isUndefined(channelUsers)) {
return channelUsers[_.random(0, channelUsers.length - 1)];
} else {
return false;
}
},
'getServerUsers': function(server) {
return dbot.db.knownUsers[server].users;
},
'getAllUsers': function() {
return _.reduce(dbot.db.knownUsers, function(memo, server, name) {
memo[name] = server.users;
return memo;
}, {}, this);
},
'isKnownUser': function(server, nick) {
var knownUsers = this.getServerUsers(server);
return (_.include(knownUsers.users, nick) || _.has(knownUsers.aliases, nick));
},
'isPrimaryUser': function(server, nick) {
var knownUsers = this.getServerUsers(server);
return _.include(knownUsers.users, nick);
},
'getAliases': function(server, nick) {
var knownUsers = this.getServerUsers(server);
return _.chain(knownUsers.aliases)
.keys()
.filter(function(user) {
return knownUsers.aliases[user] == nick;
}, this)
.value();
},
'isOnline': function(server, user, channel, useLowerCase) {
var user = this.api.resolveUser(server, user, useLowerCase);
var possiNicks = [user].concat(this.api.getAliases(server, user));
if(!_.has(dbot.instance.connections[server].channels, channel)) return false;
var onlineNicks = dbot.instance.connections[server].channels[channel].nicks;
return _.any(onlineNicks, function(nick) {
nick = nick.name;
return _.include(possiNicks, nick);
}, this);
},
'isChannelUser': function(server, user, channel, useLowerCase) {
var knownUsers = this.getServerUsers(server);
var user = this.api.resolveUser(server, user, useLowerCase);
if(!_.has(knownUsers.channelUsers, channel)) {
return false;
}
return _.include(knownUsers.channelUsers[channel], user);
}
};
return api;
};
exports.fetch = function(dbot) {
return api(dbot);
};

112
modules/users/commands.js Normal file
View File

@ -0,0 +1,112 @@
var _ = require('underscore')._;
var commands = function(dbot) {
var commands = {
'~alias': function(event) {
var knownUsers = this.getServerUsers(event.server),
alias = event.params[1].trim();
if(_.include(knownUsers.users, alias)) {
var aliases = this.api.getAliases(event.server, alias);
var aliasCount = aliases.length;
if(aliasCount != 0) {
var aliases = _.first(aliases, 10);
var including = 'including: ';
for(var i=0;i<aliases.length;i++) {
including += aliases[i] + ', ';
}
including = including.slice(0, -2) + '.';
event.reply(dbot.t('primary', {
'user': alias,
'count': aliasCount
}) + including);
} else {
event.reply(dbot.t('primary', {
'user': alias,
'count': aliasCount
}));
}
} else if(_.has(knownUsers.aliases, alias)) {
event.reply(dbot.t('alias', {
'alias': alias,
'user': knownUsers.aliases[alias]
}));
} else {
event.reply(dbot.t('unknown_alias', { 'alias': alias }));
}
},
'~setaliasparent': function(event) {
var knownUsers = this.getServerUsers(event.server);
var newParent = event.params[1];
if(_.has(knownUsers.aliases, newParent)) {
var newAlias = knownUsers.aliases[newParent];
// Replace user entry
knownUsers.users = _.without(knownUsers.users, newAlias);
knownUsers.users.push(newParent);
// Replace channels entries with new primary user
this.updateChannels(event, newAlias, newParent);
// Remove alias for new parent & add alias for new alias
delete knownUsers.aliases[newParent];
knownUsers.aliases[newAlias] = newParent;
// Update aliases to point to new primary user
this.updateAliases(event, newAlias, newParent);
event.reply(dbot.t('aliasparentset', {
'newParent': newParent,
'newAlias': newAlias
}));
return {
'server': event.server,
'alias': newAlias
};
} else {
event.reply(dbot.t('unknown_alias', { 'alias': newParent }));
}
return false;
},
'~mergeusers': function(event) {
var knownUsers = this.getServerUsers(event.server);
var primaryUser = event.params[1];
var secondaryUser = event.params[2];
if(_.include(knownUsers.users, primaryUser) && _.include(knownUsers.users, secondaryUser)) {
knownUsers.users = _.without(knownUsers.users, secondaryUser);
knownUsers.aliases[secondaryUser] = primaryUser;
this.updateAliases(event, secondaryUser, primaryUser);
this.updateChannels(event, secondaryUser, primaryUser);
event.reply(dbot.t('merged_users', {
'old_user': secondaryUser,
'new_user': primaryUser
}));
return {
'server': event.server,
'secondary': secondaryUser
};
} else {
event.reply(dbot.t('unprimary_error'));
}
return false;
}
};
commands['~setaliasparent'].access = 'moderator';
commands['~mergeusers'].access = 'moderator';
return commands;
};
exports.fetch = function(dbot) {
return commands(dbot);
};

View File

@ -0,0 +1,6 @@
{
"ignorable": false,
"dependencies": [ "command", "event" ],
"dbKeys": [ "knownUsers" ],
"help": "https://github.com/reality/depressionbot/blob/master/modules/users/README.md"
}

102
modules/users/pages.js Normal file
View File

@ -0,0 +1,102 @@
var pages = function(dbot) {
var _ = require('underscore')._;
var connections = dbot.instance.connections;
return {
'/connections': function(req, res) {
var connections = Object.keys(dbot.instance.connections);
res.render('connections', { 'name': dbot.config.name, 'connections': connections });
},
'/channels/:connection': function(req, res) {
var connection = req.params.connection;
if(dbot.instance.connections.hasOwnProperty(connection)) {
var channels = Object.keys(dbot.instance.connections[connection].channels);
res.render('channels', { 'name': dbot.config.name, 'connection': connection, 'channels': channels});
} else {
res.render_core('error', { 'name': dbot.config.name, 'message': 'No such connection.' });
}
},
'/users/:connection/:channel': function(req, res) {
var connection = req.params.connection;
var channel = _.unescape(req.params.channel);
var connections = dbot.instance.connections;
if(connections.hasOwnProperty(connection) &&
connections[connection].channels.hasOwnProperty(channel)) {
//TODO(samstudio8): Stats API Functionality
var chanData = dbot.api.stats.getChanStats(connection, channel, ["week"]);
var chanFreq = [];
var chanFreqLabel = [];
if(chanData){
var cur_ptr;
for(var i=0; i <= 6; i++){
cur_ptr = ((i+1)+chanData.fields.week.raw.ptr) % 7;
for(var j=0; j <= 23; j++){
chanFreq.push(chanData.fields.week.raw[cur_ptr][j]);
}
chanFreqLabel.push("'"+chanData.fields.week.raw[cur_ptr].name+"'");
}
}
else{
for (var i = 0; i < 168; i++) chanFreq[i] = 0;
chanFreqLabel = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
}
var userData = { "active": [], "inactive": [], "offline": []};
var reply = dbot.api.stats.getChanUsersStats(connection, channel, [
"lines", "words", "lincent", "wpl", "in_mentions"
]);
_.each(reply.users, function(user, userName){
if(userName != dbot.config.name){
if(user.online){
if(user.active){
userData.active.push(user);
}
else{
userData.inactive.push(user);
}
}
else{
userData.offline.push(user);
}
}
});
var userSort = function(a, b){
var x = a.display.toLowerCase();
var y = b.display.toLowerCase();
if(x > y) return 1;
if(x < y) return -1;
return 0;
}
userData.active.sort(userSort);
userData.inactive.sort(userSort);
userData.offline.sort(userSort);
var userDataSorted = (userData.active.concat(userData.inactive)).concat(userData.offline);
res.render('users', {
'name': dbot.config.name,
'connection': connection,
'channel': channel,
'userStats': userDataSorted,
'chanFreq': chanFreq,
'chanFreqLen': chanFreq.length,
"locals": {
'chanFreqLabel': chanFreqLabel,
}});
} else {
res.render_core('error', { 'name': dbot.config.name, 'message': 'No such connection or channel.' });
}
},
};
};
exports.fetch = function(dbot) {
return pages(dbot);
};

View File

@ -0,0 +1,26 @@
{
"alias": {
"english": "{alias} is an alias of {user}",
"na'vi": "ayfko syaw {user} {alias} nìteng"
},
"primary": {
"english": "{user} is a primary user with {count} aliases, ",
"na'vi": "{user} lu txin ulte {count}a stxo lu poru, "
},
"unknown_alias": {
"english": "{alias} does not currently exist as an alias or known user.",
"na'vi": "{alias} ke fkeytok nìfkrr"
},
"aliasparentset": {
"english": "{newParent} is now the parent user, and {newAlias} is an alias.",
"na'vi": "{newParent} lu sa'sem set ulte {newAlias} lu stxo set nìteng."
},
"unprimary_error": {
"english": "One of those users isn't currently recorded as a primary user.",
"na'vi": "fo sute txin ke lu."
},
"merged_users": {
"english": "{old_user} and their aliases have been merged into {new_user}.",
"na'vi": "{old_user} ulte stxo alahe {new_user} lu set."
}
}

99
modules/users/users.js Normal file
View File

@ -0,0 +1,99 @@
/**
* Name: Users
* Description: Track known users
*/
var _ = require('underscore')._;
var users = function(dbot) {
this.knownUsers = dbot.db.knownUsers;
this.getServerUsers = function(server) {
var knownUsers = this.knownUsers;
if(!_.has(knownUsers, server)) {
knownUsers[server] = { 'users': [], 'aliases': {}, 'channelUsers': {} };
}
if(!_.has(knownUsers[server], 'channelUsers')) {
knownUsers[server].channelUsers = {};
}
return knownUsers[server];
};
this.updateAliases = function(event, oldUser, newUser) {
var knownUsers = this.getServerUsers(event.server);
_.each(knownUsers.aliases, function(user, alias) {
if(user == oldUser) {
knownUsers.aliases[alias] = newUser;
}
}, this);
};
this.updateChannels = function(event, oldUser, newUser) {
var channelUsers = this.getServerUsers(event.server).channelUsers;
channelUsers = _.each(channelUsers, function(channel, channelName) {
channelUsers[channelName] = _.without(channel, oldUser);
channelUsers[channelName].push(newUser);
}, this);
};
this.listener = function(event) {
var knownUsers = this.getServerUsers(event.server);
var nick = event.user;
if(event.action == 'JOIN') {
if(!_.has(knownUsers.channelUsers, event.channel.name)) {
knownUsers.channelUsers[event.channel.name] = [];
}
var channelUsers = knownUsers.channelUsers[event.channel.name];
if(this.api.isKnownUser(event.server, nick)) {
nick = this.api.resolveUser(event.server, nick);
} else {
knownUsers.users.push(nick);
dbot.api.event.emit('new_user', [ event.server, nick ]);
}
if(!_.include(channelUsers, nick)) {
channelUsers.push(nick);
}
} else if(event.action == 'NICK') {
var newNick = event.params.substr(1);
if(!this.api.isKnownUser(newNick)) {
knownUsers.aliases[newNick] = this.api.resolveUser(event.server, event.user);
dbot.api.event.emit('nick_change', [ event.server, newNick ]);
}
}
}.bind(this);
this.on = ['JOIN', 'NICK'];
this.onLoad = function() {
// Trigger updateNickLists to stat current users in channel
dbot.instance.addListener('366', 'users', function(event) {
var knownUsers = this.getServerUsers(event.server);
if(!_.has(knownUsers.channelUsers, event.channel.name)) {
knownUsers.channelUsers[event.channel.name] = [];
}
var channelUsers = knownUsers.channelUsers[event.channel.name];
_.each(event.channel.nicks, function(nick) {
nick = nick.name;
if(this.api.isKnownUser(event.server, nick)) {
nick = this.api.resolveUser(event.server, nick);
} else {
knownUsers.users.push(nick);
dbot.api.emit('new_user', [ event.server, nick ]);
}
if(!_.include(channelUsers, nick)) {
channelUsers.push(nick);
}
}, this);
}.bind(this));
var connections = dbot.instance.connections;
_.each(connections, function(connection) {
connection.updateNickLists();
});
};
};
exports.fetch = function(dbot) {
return new users(dbot);
};

42
modules/web/README.md Normal file
View File

@ -0,0 +1,42 @@
# Web
Web interface
## Description
It's a web interface for DBot. What of it?
## Requirements
###Express and Jade@0.25
```
npm install express
npm install jade@0.25
```
###Express Patch
Express currently needs to be patched, edit ~/node_modules/express/lib/express.js as thus;
```
52 for (var key in connect.middleware) {
**53 if( !Object.prototype.hasOwnProperty(key) ) {
54 Object.defineProperty(
55 exports
56 , key
57 , Object.getOwnPropertyDescriptor(connect.middleware, key));
**58 }
59 }
```
###Twitter Bootstrap
```
cd depressionbot/public/
wget http://twitter.github.com/bootstrap/assets/bootstrap.zip
unzip bootstrap.zip
rm bootstrap.zip
```
###d3.js
```
cd depressionbot/public/
mkdir d3
cd d3
wget http://d3js.org/d3.v3.zip
unzip d3.v3.zip
rm d3.v3.zip
```

View File

@ -1,4 +1,5 @@
{ {
"ignorable": false,
"webHost": "localhost", "webHost": "localhost",
"webPort": 8080 "webPort": 9001
} }

View File

@ -1,10 +1,11 @@
var express = require('express'); var express = require('express'),
_ = require('underscore')._,
fs = require('fs');
var webInterface = function(dbot) { var webInterface = function(dbot) {
var pub = 'public'; var pub = 'public';
var app = express.createServer(); var app = express();
app.use(express.compiler({ src: pub, enable: ['sass'] }));
app.use(express.static(pub)); app.use(express.static(pub));
app.set('view engine', 'jade'); app.set('view engine', 'jade');
@ -12,119 +13,33 @@ var webInterface = function(dbot) {
res.render('index', { 'name': dbot.config.name }); res.render('index', { 'name': dbot.config.name });
}); });
app.get('/connections', function(req, res) { var server = app.listen(dbot.config.web.webPort);
var connections = Object.keys(dbot.instance.connections);
res.render('connections', { 'name': dbot.config.name, 'connections': connections });
});
app.get('/channels/:connection', function(req, res) { this.reloadPages = function() {
var connection = req.params.connection; var pages = dbot.pages;
if(dbot.instance.connections.hasOwnProperty(connection)) { for(var p in pages) {
var channels = Object.keys(dbot.instance.connections[connection].channels); if(_.has(pages, p)) {
res.render('channels', { 'name': dbot.config.name, 'connection': connection, 'channels': channels}); var func = pages[p];
} else { var mod = func.module;
res.render('error', { 'name': dbot.config.name, 'message': 'No such connection.' }); app.get(p, (function(req, resp) {
} // Crazy shim to seperate module views.
}); var shim = Object.create(resp);
shim.render = (function(view, one, two) {
app.get('/users/:connection/:channel', function(req, res) { // Render with express.js
var connection = req.params.connection; resp.render(this.module + '/' + view, one, two);
var channel = '#' + req.params.channel; }).bind(this);
var connections = dbot.instance.connections; shim.render_core = resp.render;
this.call(this.module, req, shim);
if(connections.hasOwnProperty(connection) && }).bind(func));
connections[connection].channels.hasOwnProperty(channel)) {
var nicks = Object.keys(connections[connection].channels[channel].nicks);
res.render('users', { 'name': dbot.config.name, 'connection': connection,
'channel': channel, 'nicks': nicks });
} else {
res.render('error', { 'name': dbot.config.name, 'message': 'No such connection or channel.' });
}
});
app.get('/user/:connection/:channel/:user', function(req, res) {
var connection = req.params.connection;
var channel = '#' + req.params.channel;
var user = dbot.cleanNick(req.params.user);
var quoteCount = 'no';
if(dbot.db.quoteArrs.hasOwnProperty(user)) {
var quoteCount = dbot.db.quoteArrs[user].length;
}
if(!dbot.db.kicks.hasOwnProperty(req.params.user)) {
var kicks = '0';
} else {
var kicks = dbot.db.kicks[req.params.user];
}
if(!dbot.db.kickers.hasOwnProperty(req.params.user)) {
var kicked = '0';
} else {
var kicked = dbot.db.kickers[req.params.user];
}
res.render('user', { 'name': dbot.config.name, 'user': req.params.user,
'channel': channel, 'connection': connection, 'cleanUser': user,
'quotecount': quoteCount, 'kicks': kicks, 'kicked': kicked });
});
// Lists the quote categories
app.get('/quotes', function(req, res) {
res.render('quotelist', { 'name': dbot.config.name, 'quotelist': Object.keys(dbot.db.quoteArrs) });
});
// Lists quotes in a category
app.get('/quotes/:key', function(req, res) {
var key = req.params.key.toLowerCase();
if(dbot.db.quoteArrs.hasOwnProperty(key)) {
res.render('quotes', { 'name': dbot.config.name, 'quotes': dbot.db.quoteArrs[key], locals: { 'url_regex': RegExp.prototype.url_regex() } });
} else {
res.render('error', { 'name': dbot.config.name, 'message': 'No quotes under that key.' });
}
});
// Load random quote category page
app.get('/rq', function(req, res) {
var rCategory = Object.keys(dbot.db.quoteArrs).random();
res.render('quotes', { 'name': dbot.config.name, 'quotes': dbot.db.quoteArrs[rCategory], locals: { 'url_regex': RegExp.prototype.url_regex() } });
});
// Lists all of the polls
app.get('/polls', function(req, res) {
res.render('polllist', { 'name': dbot.config.name, 'polllist': Object.keys(dbot.db.polls) });
});
// Shows the results of a poll
app.get('/polls/:key', function(req, res) {
var key = req.params.key.toLowerCase();
if(dbot.db.polls.hasOwnProperty(key) && dbot.db.polls[key].hasOwnProperty('description')) {
// tally the votes
var totalVotes = 0;
for( var v in dbot.db.polls[key].votes ) {
var N = Number(dbot.db.polls[key].votes[v]);
if( !isNaN(N) ) {
totalVotes += N;
} }
} }
res.render('polls', { 'name': dbot.config.name, 'description': dbot.db.polls[key].description, 'votees': Object.keys(dbot.db.polls[key].votees), 'options': dbot.db.polls[key].votes, locals: { 'totalVotes': totalVotes, 'url_regex': RegExp.prototype.url_regex() } }); }.bind(this);
} else {
res.render('error', { 'name': dbot.config.name, 'message': 'No polls under that key.' }); this.onDestroy = function() {
server.close();
} }
});
app.listen(dbot.config.web.webPort);
return {
'name': 'web',
'ignorable': false,
'onDestroy': function() {
app.close();
}
};
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return webInterface(dbot); return new webInterface(dbot);
}; };

10
modules/youare/README.md Normal file
View File

@ -0,0 +1,10 @@
## youare
You're a loser!
### Description
This module occasionally comes back and says "You're a x" when somebody goes "y
is x" or "y are x," with the intention of annoying people who are calling
something crap. Warning: this module occasionally causes the bot to be nice to
people.

View File

@ -0,0 +1,3 @@
{
"ignorable": true
}

View File

@ -1,19 +1,14 @@
var youAre = function(dbot) { var youAre = function(dbot) {
return { this.listener = function(event) {
'name': 'youare',
'ignorable': false,
'listener': function(event) {
var key = event.message.valMatch(/(\bis\b|\bare\b)\s+([\w\s\d]*?)(\s+)?(,|\.|\band\b|$)/, 5); var key = event.message.valMatch(/(\bis\b|\bare\b)\s+([\w\s\d]*?)(\s+)?(,|\.|\band\b|$)/, 5);
if(key && key[2] != "" && Number.prototype.chanceIn(1, 100) && event.user != 'aisbot') { if(key && key[2] != "" && Number.prototype.chanceIn(1, 100) && event.user != 'aisbot') {
event.reply(event.user + ': You\'re ' + key[2] + '.'); event.reply(event.user + ': You\'re ' + key[2] + '.');
} }
}, }.bind(this);
'on': 'PRIVMSG' this.on = 'PRIVMSG';
};
}; };
exports.fetch = function(dbot) { exports.fetch = function(dbot) {
return youAre(dbot); return new youAre(dbot);
}; };

View File

@ -6,19 +6,19 @@
*/ */
html { html {
background: url("background.jpg") no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
} }
body { body {
padding: 25px; padding: 25px;
margin: 0; margin: 0;
font-family: "Lucida Grande"; font-family: "Source Sans Pro", sans-serif;
color: #444; color: #444;
text-shadow: 1px 1px 2px #2B2B2B; text-shadow: 1px 1px 2px #2B2B2B;
background: url("background.jpg") no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
} }
h1,h2 { h1,h2 {
@ -31,7 +31,6 @@ p {
} }
div#page { div#page {
width: 90%;
margin: 0 auto 0 auto; margin: 0 auto 0 auto;
} }
@ -56,8 +55,7 @@ div#title a {
div#main { div#main {
position: relative; position: relative;
padding: 15px 5px; padding: 10px 5px;
margin: 0px;
font-size: 21px; font-size: 21px;
text-align:center; text-align:center;
background: #FFF; background: #FFF;
@ -172,3 +170,58 @@ li.option-votes {
box-shadow: inset 0px 0px 3px #444; box-shadow: inset 0px 0px 3px #444;
} }
/**
* DataTables
* Based on http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables.css
*/
.sorting { background: url('http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/images/sort_both.png') no-repeat center right; }
.sorting_asc { background: url('http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/images/sort_asc.png') no-repeat center right; }
.sorting_desc { background: url('http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/images/sort_desc.png') no-repeat center right; }
.sorting_asc_disabled { background: url('http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/images/sort_asc_disabled.png') no-repeat center right; }
.sorting_desc_disabled { background: url('http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/images/sort_desc_disabled.png') no-repeat center right; }
/**
* Bootstrap Overrides
*/
.profile_page-header{
margin: 5px 0 10px 0;
}
.profile_row{
text-align: left;
margin-bottom: 10px;
}
/**
* spaceinvader's thumbnails
*/
span.nicks {
display: none;
color: #fff;
text-decoration: none;
position: absolute;
left: 0px;
top: 10px;
width: 100%;
word-wrap: break-word;
}
div.imgwrap {
background-color: #000;
position: relative;
}
div.imgwrap > img {
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
transition: all 0.2s ease-in-out;
}
.thumbnail:hover > div.imgwrap > img {
opacity: 0.5;
}
.thumbnail:hover > div.imgwrap > span.nicks {
display: inline;
}

267
run.js
View File

@ -1,47 +1,54 @@
var fs = require('fs'); var fs = require('fs'),
var timers = require('./timer'); _ = require('underscore')._,
var jsbot = require('./jsbot/jsbot'); jsbot = require('./jsbot/jsbot');
require('./snippets'); require('./snippets');
var DBot = function(timers) { var DBot = function() {
// Load external files
var requiredConfigKeys = [ 'name', 'servers', 'admins', 'moduleNames', 'language' ]; /*** Load the DB ***/
if(fs.existsSync('db.json')) {
try { try {
this.config = JSON.parse(fs.readFileSync('config.json', 'utf-8')); this.db = JSON.parse(fs.readFileSync('db.json', 'utf-8'));
} catch(err) { } catch(err) {
console.log('Config file is screwed up. Attempting to load defaults.'); console.log('Error loading db.json. Stopping: ' + err);
try {
this.config = JSON.parse(fs.readFileSync('config.json.sample', 'utf-8'));
} catch(err) {
console.log('Error loading sample config. Bugger off. Stopping.');
process.exit(); process.exit();
} }
} } else {
requiredConfigKeys.each(function(key) { this.db = {};
if(!this.config.hasOwnProperty(key)) {
console.log('Error: Please set a value for ' + key + ' in ' +
'config.json. Stopping.');
process.exit();
}
}.bind(this));
var rawDB;
try {
var rawDB = fs.readFileSync('db.json', 'utf-8');
} catch(err) {
this.db = {}; // If no db file, make empty one
} }
try { if(!_.has(this.db, 'config')) {
if(!this.db) { // If it wasn't empty this.db.config = {};
this.db = JSON.parse(rawDB);
} }
} catch(err) {
console.log('Syntax error in db.json. Stopping: ' + err); /*** Load the Config ***/
if(!fs.existsSync('config.json')) {
console.log('Error: config.json file does not exist. Stopping');
process.exit(); process.exit();
} }
// Load Strings file this.config = _.clone(this.db.config);
try {
_.defaults(this.config, JSON.parse(fs.readFileSync('config.json', 'utf-8')));
} catch(err) {
console.log('Config file is invalid. Stopping: ' + err);
process.exit();
}
try {
var defaultConfig = JSON.parse(fs.readFileSync('config.json.sample', 'utf-8'));
} catch(err) {
console.log('Error loading sample config. Bugger off this should not even be edited. Stopping.');
process.exit();
}
// Load missing config directives from sample file
_.defaults(this.config, defaultConfig);
/*** Load main strings ***/
try { try {
this.strings = JSON.parse(fs.readFileSync('strings.json', 'utf-8')); this.strings = JSON.parse(fs.readFileSync('strings.json', 'utf-8'));
} catch(err) { } catch(err) {
@ -51,15 +58,13 @@ var DBot = function(timers) {
// Initialise run-time resources // Initialise run-time resources
this.usage = {}; this.usage = {};
this.status = {};
this.sessionData = {}; this.sessionData = {};
this.timers = timers.create();
// Populate bot properties with config data // Populate bot properties with config data
// Create JSBot and connect to each server // Create JSBot and connect to each server
this.instance = jsbot.createJSBot(this.config.name); this.instance = jsbot.createJSBot(this.config.name);
for(var name in this.config.servers) { _.each(this.config.servers, function(server, name) {
if(this.config.servers.hasOwnProperty(name)) {
var server = this.config.servers[name];
this.instance.addConnection(name, server.server, server.port, this.instance.addConnection(name, server.server, server.port,
this.config.admin, function(event) { this.config.admin, function(event) {
var server = this.config.servers[event.server]; var server = this.config.servers[event.server];
@ -67,8 +72,7 @@ var DBot = function(timers) {
this.instance.join(event, server.channels[i]); this.instance.join(event, server.channels[i]);
} }
}.bind(this), server.nickserv, server.password); }.bind(this), server.nickserv, server.password);
} }, this);
}
// Load the modules and connect to the server // Load the modules and connect to the server
this.reloadModules(); this.reloadModules();
@ -83,9 +87,9 @@ DBot.prototype.say = function(server, channel, message) {
// Format given stored string in config language // Format given stored string in config language
DBot.prototype.t = function(string, formatData) { DBot.prototype.t = function(string, formatData) {
var formattedString; var formattedString;
if(this.strings.hasOwnProperty(string)) { if(_.has(this.strings, string)) {
var lang = this.config.language; var lang = this.config.language;
if(!this.strings[string].hasOwnProperty(lang)) { if(!_.has(this.strings[string], lang)) {
lang = "english"; lang = "english";
} }
@ -103,7 +107,7 @@ DBot.prototype.t = function(string, formatData) {
// Save the database file // Save the database file
DBot.prototype.save = function() { DBot.prototype.save = function() {
fs.writeFile('db.json', JSON.stringify(this.db, null, ' ')); fs.writeFileSync('db.json', JSON.stringify(this.db, null, ' '));
}; };
// Hot-reload module files. // Hot-reload module files.
@ -117,11 +121,16 @@ DBot.prototype.reloadModules = function() {
} }
this.rawModules = []; this.rawModules = [];
this.modules = []; this.pages = {};
this.status = {};
this.modules = {};
this.commands = {}; this.commands = {};
this.api = {};
this.commandMap = {}; // Map of which commands belong to which modules this.commandMap = {}; // Map of which commands belong to which modules
this.usage = {}; this.usage = {};
this.timers.clearTimers();
// Load config changes
_.extend(this.config, this.db.config);
try { try {
this.strings = JSON.parse(fs.readFileSync('strings.json', 'utf-8')); this.strings = JSON.parse(fs.readFileSync('strings.json', 'utf-8'));
@ -150,88 +159,150 @@ DBot.prototype.reloadModules = function() {
delete require.cache[cacheKey]; delete require.cache[cacheKey];
try { try {
// Load the module config data var webKey = require.resolve(moduleDir + 'web');
} catch(err) {
}
if(webKey) {
delete require.cache[webKey];
}
this.status[name] = true;
try { try {
var config = JSON.parse(fs.readFileSync(moduleDir + 'config.json', 'utf-8')) // Load the module config data
this.config[name] = config; var config = {};
for(var i=0;i<config.dbKeys.length;i++) {
if(!this.db.hasOwnProperty(config.dbKeys[i])) { if(_.has(this.db.config, name)) {
this.db[config.dbKeys[i]] = {}; config = _.clone(this.db.config[name]);
} }
try {
var defaultConfig = fs.readFileSync(moduleDir + 'config.json', 'utf-8');
try {
defaultConfig = JSON.parse(defaultConfig);
} catch(err) { // syntax error
this.status[name] = 'Error parsing config: ' + err + ' ' + err.stack.split('\n')[2].trim();
return;
} }
config = _.defaults(config, defaultConfig);
} catch(err) { } catch(err) {
// Invalid or no config data // Invalid or no config data
} }
// Don't shit out if dependencies not met
if(_.has(config, 'dependencies')) {
_.each(config.dependencies, function(dependency) {
if(!_.include(moduleNames, dependency)) {
console.log('Warning: Automatically loading ' + dependency);
moduleNames.push(dependency);
}
}, this);
}
// Generate missing DB keys
this.config[name] = config;
_.each(config.dbKeys, function(dbKey) {
if(!_.has(this.db, dbKey)) {
this.db[dbKey] = {};
}
}, this);
// Load the module itself // Load the module itself
var rawModule = require(moduleDir + name); var rawModule = require(moduleDir + name);
var module = rawModule.fetch(this); var module = rawModule.fetch(this);
module.name = name;
this.rawModules.push(rawModule); this.rawModules.push(rawModule);
module.config = this.config[name];
// Load the module data
_.each([ 'commands', 'pages', 'api' ], function(property) {
var propertyObj = {};
if(fs.existsSync(moduleDir + property + '.js')) {
try {
var propertyKey = require.resolve(moduleDir + property);
if(propertyKey) delete require.cache[propertyKey];
propertyObj = require(moduleDir + property).fetch(this);
} catch(err) {
this.status[name] = 'Error loading ' + propertyKey + ': ' + err + ' - ' + err.stack.split('\n')[1].trim();
console.log('Module error (' + module.name + ') in ' + property + ': ' + err);
}
}
if(!_.has(module, property)) module[property] = {};
_.extend(module[property], propertyObj);
_.each(module[property], function(item, itemName) {
item.module = name;
if(_.has(config, property) && _.has(config[property], itemName)) {
_.extend(item, config[property][itemName]);
}
module[property][itemName] = _.bind(item, module);
_.extend(module[property][itemName], item);
}, this);
if(property == 'api') {
this[property][name] = module[property];
} else {
_.extend(this[property], module[property]);
}
}, this);
// Load the module listener
if(module.listener) { if(module.listener) {
this.instance.addListener(module.on, module.name, module.listener); if(!_.isArray(module.on)) {
module.on = [ module.on ];
}
_.each(module.on, function(on) {
this.instance.addListener(on, module.name, module.listener);
}, this);
} }
if(module.onLoad) { // Load string data for the module
module.onLoad(); _.each([ 'usage', 'strings' ], function(property) {
} var propertyData = {};
// Load module commands
if(module.commands) {
var newCommands = module.commands;
for(key in newCommands) {
if(newCommands.hasOwnProperty(key) && Object.prototype.isFunction(newCommands[key])) {
this.commands[key] = newCommands[key];
this.commandMap[key] = name;
}
}
}
// Load the module usage data
try { try {
var usage = JSON.parse(fs.readFileSync(moduleDir + 'usage.json', 'utf-8')); propertyData = JSON.parse(fs.readFileSync(moduleDir + property + '.json', 'utf-8'));
for(key in usage) { } catch(err) {};
if(usage.hasOwnProperty(key)) { _.extend(this[property], propertyData);
if(this.usage.hasOwnProperty(key)) { }, this);
console.log('Usage key clash for ' + key + ' in ' + name);
} else { // Provide toString for module name
this.usage[key] = usage[key]; module.toString = function() {
} return this.name;
}
}
} catch(err) {
// Invalid or no usage info
} }
// Load the module string data this.modules[module.name] = module;
try {
var strings = JSON.parse(fs.readFileSync(moduleDir + 'strings.json', 'utf-8'));
for(key in strings) {
if(strings.hasOwnProperty(key)) {
if(this.strings.hasOwnProperty(key)) {
console.log('Strings key clash for ' + key + ' in ' + name);
} else {
this.strings[key] = strings[key];
}
}
}
} catch(err) {
// Invalid or no string info
}
this.modules.push(module);
} catch(err) { } catch(err) {
console.log(this.t('module_load_error', {'moduleName': name})); console.log(this.t('module_load_error', {'moduleName': name}));
console.log('MODULE ERROR: ' + name + ' ' + err); this.status[name] = err + ' - ' + err.stack.split('\n')[1].trim();
if(this.config.debugMode) {
console.log('MODULE ERROR (' + name + '): ' + err.stack );
} else {
console.log('MODULE ERROR (' + name + '): ' + err );
}
} }
}.bind(this)); }.bind(this));
if(_.has(this.modules, 'web')) this.modules.web.reloadPages();
_.each(this.modules, function(module, name) {
if(module.onLoad) {
try {
module.onLoad();
} catch(err) {
this.status[name] = 'Error in onLoad: ' + err + ' ' + err.stack.split('\n')[1].trim();
}
}
}, this);
this.save(); this.save();
}; };
DBot.prototype.cleanNick = function(key) { DBot.prototype.cleanNick = function(key) {
key = key.toLowerCase(); key = key.toLowerCase();
while(key.endsWith("_")) { while(key.endsWith("_")) {
if(this.db.quoteArrs.hasOwnProperty(key)) { if(_.has(this.db.quoteArrs, key)) {
return key; return key;
} }
key = key.substring(0, key.length-1); key = key.substring(0, key.length-1);
@ -239,4 +310,4 @@ DBot.prototype.cleanNick = function(key) {
return key; return key;
} }
new DBot(timers); new DBot();

View File

@ -35,19 +35,6 @@ Array.prototype.sum = function() {
return sum; return sum;
}; };
Array.prototype.allGroupings = function() {
if (this.length == 0) {
return []; /* short-circuit the empty-array case */
}
var groupings = [];
for(var n=1;n<=this.length;n++) {
for(var i=0;i<(this.length-(n-1));i++) {
groupings.push(this.slice(i, i+n));
}
}
return groupings;
}
Array.prototype.uniq = function() { Array.prototype.uniq = function() {
var hash = {} var hash = {}
var result = []; var result = [];
@ -79,66 +66,6 @@ String.prototype.startsWith = function(needle) {
return needle === this.slice(0, needle.length); return needle === this.slice(0, needle.length);
}; };
String.prototype.distance = function(s1, s2) {
// Calculate Levenshtein distance between two strings
//
// version: 1109.2015
// discuss at: http://phpjs.org/functions/levenshtein // + original by: Carlos R. L. Rodrigues (http://www.jsfromhell.com)
// + bugfixed by: Onno Marsman
// + revised by: Andrea Giammarchi (http://webreflection.blogspot.com)
// + reimplemented by: Brett Zamir (http://brett-zamir.me)
// + reimplemented by: Alexander M Beedie // * example 1: levenshtein('Kevin van Zonneveld', 'Kevin van Sommeveld');
// * returns 1: 3
if (s1 == s2) {
return 0;
}
var s1_len = s1.length;
var s2_len = s2.length;
if (s1_len === 0) {
return s2_len; }
if (s2_len === 0) {
return s1_len;
}
// BEGIN STATIC
var split = false;
try {
split = !('0')[0];
} catch (e) { split = true; // Earlier IE may not support access by string index
}
// END STATIC
if (split) {
s1 = s1.split(''); s2 = s2.split('');
}
var v0 = new Array(s1_len + 1);
var v1 = new Array(s1_len + 1);
var s1_idx = 0,
s2_idx = 0,
cost = 0;
for (s1_idx = 0; s1_idx < s1_len + 1; s1_idx++) { v0[s1_idx] = s1_idx;
}
var char_s1 = '',
char_s2 = '';
for (s2_idx = 1; s2_idx <= s2_len; s2_idx++) { v1[0] = s2_idx;
char_s2 = s2[s2_idx - 1];
for (s1_idx = 0; s1_idx < s1_len; s1_idx++) {
char_s1 = s1[s1_idx]; cost = (char_s1 == char_s2) ? 0 : 1;
var m_min = v0[s1_idx + 1] + 1;
var b = v1[s1_idx] + 1;
var c = v0[s1_idx] + cost;
if (b < m_min) { m_min = b;
}
if (c < m_min) {
m_min = c;
} v1[s1_idx + 1] = m_min;
}
var v_tmp = v0;
v0 = v1;
v1 = v_tmp; }
return v0[s1_len];
}
String.prototype.format = function() { // format takes either multiple indexed arguments, or a single object, whose keys/values will be used String.prototype.format = function() { // format takes either multiple indexed arguments, or a single object, whose keys/values will be used
var targetStr = this; var targetStr = this;
var replacements = [].splice.call(arguments, 0); var replacements = [].splice.call(arguments, 0);
@ -204,7 +131,15 @@ Object.prototype.filter = function(fun) {
} }
} }
return filtered; return filtered;
};
Object.prototype.each = function(fun) {
for(var key in this) {
if(this.hasOwnProperty(key)) {
fun(this[key]);
} }
}
};
/*** Integer ***/ /*** Integer ***/
@ -253,3 +188,18 @@ RegExp.prototype.url_regex = function() {
); );
return reg; return reg;
} }
Number.prototype.numberFormat = function(dec_places){
//TODO Possibly abstract this to some sort of localisation module in future?
var dec_point = '.';
var sep = ',';
var parts = this.toFixed(dec_places).toString().split(dec_point);
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, sep);
return parts.join(dec_point);
}
// http://simonwillison.net/2006/Jan/20/escape/#p-6
String.prototype.escape = function() {
return this.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}

View File

@ -1,27 +0,0 @@
var timers = function() {
var timers = [];
var timeouts = [];
return {
'addTimer': function(interval, callback) { // Because who puts the callback first. Really.
timers.push(setInterval(callback, interval));
},
'addOnceTimer': function(delay, callback) { // Because who seriously puts the callback first here too?
timeouts.push(setTimeout(callback, delay));
},
'clearTimers': function() {
for(var i;i<timers.length;i++) {
clearInterval(timers[i]);
}
for(var i;i<timeouts.length;i++) {
clearTimeout(timeouts[i]);
}
}
};
};
exports.create = function() {
return timers();
}

View File

@ -1,7 +0,0 @@
h3 Channels on #{connection}
div#backlink
a(href='/connections') &laquo; Connection List
ul#quotelist
-each channel in channels
a(href='/users/'+connection+'/'+channel.substr(1,channel.length))
li.quotes #{channel}

View File

@ -1,6 +0,0 @@
h3 Current Connections
div#backlink
a(href='/') &laquo; Home
#modulelinks
-each connection in connections
a.module(href='/channels/'+connection) #{connection}

View File

@ -1,3 +1,6 @@
extends layout
block content
#modulelinks #modulelinks
a.module(href='/quotes') Quotes a.module(href='/quotes') Quotes
a.module(href='/polls') Polls a.module(href='/polls') Polls

View File

@ -2,12 +2,18 @@
html(lang='en') html(lang='en')
head head
meta(charset='utf-8') meta(charset='utf-8')
script(type="text/javascript", src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js")
link(rel="stylesheet", type="text/css", href="http://fonts.googleapis.com/css?family=Source+Sans+Pro")
link(rel="stylesheet", type="text/css", href="/bootstrap/css/bootstrap.min.css")
link(rel='stylesheet', type='text/css', href='/styles.css') link(rel='stylesheet', type='text/css', href='/styles.css')
title #{name} web interface title #{name} web interface
body body
div.container
div#page div#page
div#title div#title
a(href='/') #{name} web interface a(href='/') #{name} web interface
div#main div.container#main
!{body} block content
script(type="text/javascript", src="/bootstrap/js/bootstrap.min.js")
script(type="text/javascript", src="/d3/d3.v3.min.js")
script(type="text/javascript", src="/script.js") script(type="text/javascript", src="/script.js")

12
views/poll/polllist.jade Normal file
View File

@ -0,0 +1,12 @@
extends ../layout
block content
div#backlink
a(href='/') &laquo; Home
div#controls
input(type="text", name="search", id="search-text", oninput="search(this.value)")
ul#quotelist
-each poll in polllist
a(href='/polls/'+poll)
li.quotes #{poll}

39
views/poll/polls.jade Normal file
View File

@ -0,0 +1,39 @@
extends ../layout
block content
div#backlink
a(href='/polls/') &laquo; Poll list
h2 #{description}
p Voters (#{locals.totalVotes}):
-each voter in votees
| #{voter}
ul#votelist
-var hasYouTubeVids=false
-each votes,option in options
-var percentage = votes/locals.totalVotes*100
-if(options.hasOwnProperty(option))
-if(option.match(locals.url_regex))
li.option
-if(option.match(/(jpg|png|gif|jpeg|tiff)$/))
a(href=option)
img(src=option)
-else if(option.match(/youtube.com\/watch/))
-hasYouTubeVids = true
span(class='ytplaceholder')
=option
-else
a(href=option)
=option
-else
li.option #{option}
li.option-votes
.vote-track
-if(!isNaN(percentage))
.vote-percentage(style="width: #{percentage}%")
case votes
when 1: #{votes} vote
default: #{votes} votes
-if(!isNaN(percentage))
|(#{percentage.toFixed(2)}%)
-if(hasYouTubeVids)
script(src='/ytembed.js')

View File

@ -1,9 +0,0 @@
div#backlink
a(href='/') &laquo; Home
div#controls
input(type="text", name="search", id="search-text", oninput="search(this.value)")
ul#quotelist
-each poll in polllist
a(href='/polls/'+poll)
li.quotes #{poll}

View File

@ -1,36 +0,0 @@
div#backlink
a(href='/polls/') &laquo; Poll list
h2 #{description}
p Voters (#{locals.totalVotes}):
-each voter in votees
| #{voter}
ul#votelist
-var hasYouTubeVids=false
-each votes,option in options
-var percentage = votes/locals.totalVotes*100
-if(options.hasOwnProperty(option))
-if(option.match(locals.url_regex))
li.option
-if(option.match(/(jpg|png|gif|jpeg|tiff)$/))
a(href=option)
img(src=option)
-else if(option.match(/youtube.com\/watch/))
-hasYouTubeVids = true
span(class='ytplaceholder')
=option
-else
a(href=option)
=option
-else
li.option #{option}
li.option-votes
.vote-track
-if(!isNaN(percentage))
.vote-percentage(style="width: #{percentage}%")
case votes
when 1: #{votes} vote
default: #{votes} votes
-if(!isNaN(percentage))
|(#{percentage.toFixed(2)}%)
-if(hasYouTubeVids)
script(src='/ytembed.js')

View File

@ -0,0 +1,98 @@
extends ../layout
block content
script(type="text/javascript", src="http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/jquery.dataTables.min.js")
script
$(document).ready(function(){
// Allowing forcing of string stats data to sort as numeric
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
"forcenum-pre": function ( a ) {
a = a.replace("\,", "");
return parseFloat( a );
},
"forcenum-asc": function ( a, b ) {
return a - b;
},
"forcenum-desc": function ( a, b ) {
return b - a;
}
} );
$('.tip').tooltip();
$('.data').dataTable({
"aoColumnDefs": [
{ "sType": "forcenum",
"asSorting": [ "desc", "asc" ],
"aTargets": [ 1, 2, 3, 4, 5 ] }
],
"bPaginate": false,
"bFilter": false,
"bLengthChange": false,
"oLanguage": {
"sInfo": "",
"sInfoEmpty": "",
"sInfoFiltered": ""
},
});
});
div.page-header.profile_page-header
h1
#{primary}
small
if(profile.tagline)
"#{profile.tagline}"
div#backlink
a(href='/profile/'+connection) &laquo; Profiles
div.row.profile_row#profile_data
div.span3
if profile.avatar
img.profile_avatar.img-polaroid(src="#{profile.avatar}")
else
img.profile_avatar.img-polaroid(src="http://placehold.it/270x180&text=Hello,%20World")
div.span9
h4 Bio
p #{profile.bio}
hr
h3 Channel Statistics
div#profile_datatable
table.table.table-hover.data
thead
tr
th Channel
th Lines
th Words
th Lincent
th Verbosity
th Mentions
tbody
if stats
for chan, key in stats
if stats.hasOwnProperty(key)
tr
td
a(href='/users/'+connection+'/'+encodeURIComponent(key))
#{key}
span
if chan.online
if chan.active.active
span.label.label-success.tip(data-original-title="#{chan.active.ago}", data-placement="right") Active
else
span.label.label-important.tip(data-original-title="#{chan.active.ago}", data-placement="right") Inactive
else
span.label.tip(data-original-title="#{chan.active.ago}", data-placement="right") Offline
td
#{chan.fields.lines.data}
td
#{chan.fields.words.data}
td
#{chan.fields.lincent.data}
td
#{chan.fields.wpl.data}
td
#{chan.fields.in_mentions.data}

View File

@ -0,0 +1,26 @@
extends ../layout
block content
script(src='http://masonry.desandro.com/jquery.masonry.min.js')
script
$(function() {
var $container = $('.thumbnails');
$container.imagesLoaded(function(){
$container.masonry({
itemSelector : '.thumbnails > li',
});
});
});
div.page-header.profile_page-header
h1
#{connection}
div#backlink
a(href='../connections') &laquo; Connections
ul.thumbnails
each nick in nicks
li.span2
a.thumbnail(href='/profile/'+connection+'/'+encodeURIComponent(nick))
div.imgwrap
img(src="#{profiles[nick].profile.avatar}", alt="#{profiles[nick].profile.primary}'s photo")
span.nicks #{profiles[nick].profile.primary}

View File

@ -1,9 +0,0 @@
div#backlink
a(href='/') &laquo; Home
div#controls
input(type="text", name="search", id="search-text", oninput="search(this.value)")
ul#quotelist
-each quote in quotelist
a(href='/quotes/'+quote)
li.quotes #{quote}

Some files were not shown because too many files have changed in this diff Show More