diff --git bot.drush.inc bot.drush.inc new file mode 100644 index 0000000..85cf312 --- /dev/null +++ bot.drush.inc @@ -0,0 +1,283 @@ + 'Starts the IRC bot for this site.', + 'examples' => array( + 'drush bot-start &' => 'Connect the IRC bot as a background process.', + 'nohup drush bot-start &' => 'Connect the IRC bot as a background process using nohup.', + ), + ); + $items['bot-stop'] = array( + 'description' => 'Stops the IRC bot for this site.', + 'options' => array( + '--timeout' => 'Number of seconds to attempt the disconnect. Defaults to 45.', + ), + 'examples' => array( + 'drush bot-stop --timeout="60"' => 'Disconnect the IRC bot, allowing 60 seconds of processing time.', + ), + ); + $items['bot-status'] = array( + 'description' => 'Returns the IRC bot connection state for this site.', + ); + $items['bot-reset-status'] = array( + 'description' => 'Resets the IRC bot connection state for this site.', + ); + + return $items; +} + +/** + * Implementation of hook_drush_help(). + */ +function bot_drush_help($section) { + switch ($section) { + case 'drush:bot-start': + return dt('Starts the IRC bot for this site. It is recommended to start it as a background process.'); + case 'drush:bot-stop': + return dt('Stops the IRC bot for this site.'); + case 'drush:bot-status': + return dt('Returns the IRC bot connection state for this site.'); + case 'drush:bot-reset-status': + return dt('Resets the IRC bot connection state for this site.'); + } +} + +/** + * Callback for drush bot-start. + */ +function drush_bot_start() { + $status = _bot_get_status(); + if ($status == BOT_STATUS_DISCONNECTED) { + _drush_bot_start(); + } + else { + _bot_drush_log_status($status, 'warning'); + } +} + +/** + * Does the heavy lifting of starting the IRC bot. + */ +function _drush_bot_start() { + drush_log(dt('The IRC bot is connecting.')); + variable_set('bot_status', BOT_STATUS_CONNECTING); + // prevent MySQL timeouts on slow channels. + db_query('SET SESSION wait_timeout = %d', 24*60*60); + + require_once('Net/SmartIRC.php'); + + // initialize the bot with some sane defaults. + global $irc; // allow it to be slurped by Drupal modules if need be. + $bot = new drupal_wrapper(); // wrapper that integrates with Drupal hooks. + $irc = new Net_SmartIRC(); // MmmmmmM. The IRC object itself. Magick happens here. + + // Net_SmartIRC::nreplycodes is set from $_GLOBALS, which is unreliable. + $irc->nreplycodes = $SMARTIRC_nreplycodes; + $irc->setDebug( variable_get('bot_debugging', 0) ? SMARTIRC_DEBUG_ALL : SMARTIRC_DEBUG_NONE ); + // the (boolean) here is required, as Net_SmartIRC doesn't respect a FAPI checkbox value of 1, only TRUE. + $irc->setAutoReconnect((boolean) variable_get('bot_auto_reconnect', 1)); // reconnect to the server if disconnected. + $irc->setAutoRetry((boolean) variable_get('bot_auto_retry', 1)); // retry if a server connection fails. + $irc->setUseSockets((boolean) variable_get('bot_real_sockets', 1)); // socket_connect or fsockopen? + $irc->setChannelSyncing(TRUE); // keep a list of joined users per channel. + + // send every message type the library supports to our wrapper class. + // we can automate the creation of these actionhandlers, but not the + // class methods below (only PHP 5 supports default methods easily). + $irc_message_types = array( + 'UNKNOWN', 'CHANNEL', 'QUERY', 'CTCP', 'NOTICE', 'WHO', + 'JOIN', 'INVITE', 'ACTION', 'TOPICCHANGE', 'NICKCHANGE', 'KICK', + 'QUIT', 'LOGIN', 'INFO', 'LIST', 'NAME', 'MOTD', + 'MODECHANGE', 'PART', 'ERROR', 'BANLIST', 'TOPIC', 'NONRELEVANT', + 'WHOIS', 'WHOWAS', 'USERMODE', 'CHANNELMODE', 'CTCP_REQUEST', 'CTCP_REPLY', + ); + + foreach ($irc_message_types as $irc_message_type) { + $irc->registerActionhandler(constant('SMARTIRC_TYPE_' . $irc_message_type), '.*', $bot, 'invoke_irc_msg_' . strtolower($irc_message_type)); + } + + // set up a timers similar to Drupal's hook_cron(), multiple types. I would have + // liked to just pass a parameter to a single function, but SmartIRC can't do that. + $irc->registerTimehandler(300000, $bot, 'invoke_irc_bot_cron'); // 5 minutes. + $irc->registerTimehandler(60000, $bot, 'invoke_irc_bot_cron_faster'); // 1 minute. + $irc->registerTimehandler(15000, $bot, 'invoke_irc_bot_cron_fastest'); // 15 seconds. + + // connect and begin listening. + $irc->connect(variable_get('bot_server', 'irc.freenode.net'), variable_get('bot_server_port', 6667)); + $irc->login(variable_get('bot_nickname', 'bot_module'), variable_get('bot_nickname', 'bot_module') . ' :http://drupal.org/project/bot', 8, variable_get('bot_nickname', 'bot_module'), (variable_get('bot_password', '') != '') ? variable_get('bot_password', '') : NULL); + + // channel joining has moved to bot_irc_bot_cron_fastest(). + // read that function for the rationale, and what we gain from it. + + $irc->listen(); + $irc->disconnect(); // if we stop listening, disconnect properly. + variable_del('bot_status'); + drush_log(dt('The IRC bot has disconnected.')); +} + +/** + * Callback for drush bot-stop. + */ +function drush_bot_stop() { + $status = _bot_get_status(); + switch ($status) { + case BOT_STATUS_DISCONNECTING: + drush_log(dt('The bot is already disconnecting.'), 'warning'); + break; + + case BOT_STATUS_DISCONNECTED: + drush_log(dt('The bot is already disconnected.'), 'warning'); + break; + + default: + _drush_bot_stop(); + } +} + +/** + * Queue the IRC bot for stopping. + */ +function _drush_bot_stop() { + drush_log(dt('Disconnecting the IRC bot.'), 'ok'); + + $timeout = drush_get_option('timeout', 45); + $status = BOT_STATUS_DISCONNECTING; + + variable_set('bot_status', $status); + lock_acquire('bot_disconnect', $timeout); + + // Continue this loop until the lock expires or the bot disconnects. + while (!($status == BOT_STATUS_DISCONNECTED || lock_may_be_available('bot_disconnect'))) { + // Check the status every 5 seconds. + sleep(5); + $status = _bot_get_status(); + _bot_drush_log_status($status); + } + + $type = 'error'; + if ($status == BOT_STATUS_DISCONNECTED) { + lock_release('bot_disconnect'); + $type = 'success'; + } + _bot_drush_log_status($status, $type); +} + +/** + * Callback for drush bot-status. + */ +function drush_bot_status() { + // If we are in the backend context, print the status directly. + if (drush_get_context('DRUSH_BACKEND')) { + $status = variable_get('bot_status', BOT_STATUS_DISCONNECTED); + drush_print($status); + return; + } + else { + $status = _bot_get_status(); + } + + _bot_drush_log_status($status, 'ok'); +} + +/** + * Retrieve the current bot status, clearing the variable cache first. + */ +function _bot_get_status() { + // We first clear the variables cache and then invoke the command to get a + // fresh status. Otherwise the static variable may contain stale data. + cache_clear_all('variables', 'cache'); + $ret = drush_backend_invoke('bot-status', array(), 'GET', FALSE); + return $ret['output']; +} + +/** + * Helper function to log the current status to drush. + * + * @param $status + * The status to log. + * @param $type + * The type of message to be logged. Common types are 'warning', 'error', + * 'success' and 'notice'. Defaults to 'notice'. + */ +function _bot_drush_log_status($status, $type = 'notice') { + switch ($status) { + case BOT_STATUS_DISCONNECTED: + drush_log(dt('The bot is currently disconnected.'), $type); + break; + + case BOT_STATUS_CONNECTING: + drush_log(dt('The bot is currently connecting.'), $type); + break; + + case BOT_STATUS_CONNECTED: + drush_log(dt('The bot is currently connected.'), $type); + break; + + case BOT_STATUS_DISCONNECTING: + drush_log(dt('The bot is currently disconnecting.'), $type); + break; + } +} + +/** + * Callback for drush bot-reset-status + */ +function drush_bot_reset_status() { + // We check locks first to be double sure that this is safe. + if (lock_may_be_available('bot_disconnect') && drush_confirm(dt('Are you sure you want to reset the IRC bot status?'))) { + variable_del('bot_status'); + drush_log(dt('The bot connection status has been reset.'), 'ok'); + } + else { + drush_log(t('It appears the bot is currently disconnecting. Please wait.'), 'warning'); + } +} + +// pass off IRC messages to our modules via Drupal's hook system. +class drupal_wrapper { + function invoke_irc_bot_cron(&$irc) { module_invoke_all('irc_bot_cron'); } + function invoke_irc_bot_cron_faster(&$irc) { module_invoke_all('irc_bot_cron_faster'); } + function invoke_irc_bot_cron_fastest(&$irc) { module_invoke_all('irc_bot_cron_fastest'); } + function invoke_irc_msg_unknown(&$irc, &$data) { module_invoke_all('irc_msg_unknown', $data); } + function invoke_irc_msg_channel(&$irc, &$data) { module_invoke_all('irc_msg_channel', $data); } + function invoke_irc_msg_query(&$irc, &$data) { module_invoke_all('irc_msg_query', $data); } + function invoke_irc_msg_ctcp(&$irc, &$data) { module_invoke_all('irc_msg_ctcp', $data); } + function invoke_irc_msg_notice(&$irc, &$data) { module_invoke_all('irc_msg_notice', $data); } + function invoke_irc_msg_who(&$irc, &$data) { module_invoke_all('irc_msg_who', $data); } + function invoke_irc_msg_join(&$irc, &$data) { module_invoke_all('irc_msg_join', $data); } + function invoke_irc_msg_invite(&$irc, &$data) { module_invoke_all('irc_msg_invite', $data); } + function invoke_irc_msg_action(&$irc, &$data) { module_invoke_all('irc_msg_action', $data); } + function invoke_irc_msg_topicchange(&$irc, &$data) { module_invoke_all('irc_msg_topicchange', $data); } + function invoke_irc_msg_nickchange(&$irc, &$data) { module_invoke_all('irc_msg_nickchange', $data); } + function invoke_irc_msg_kick(&$irc, &$data) { module_invoke_all('irc_msg_kick', $data); } + function invoke_irc_msg_quit(&$irc, &$data) { module_invoke_all('irc_msg_quit', $data); } + function invoke_irc_msg_login(&$irc, &$data) { module_invoke_all('irc_msg_login', $data); } + function invoke_irc_msg_info(&$irc, &$data) { module_invoke_all('irc_msg_info', $data); } + function invoke_irc_msg_list(&$irc, &$data) { module_invoke_all('irc_msg_list', $data); } + function invoke_irc_msg_name(&$irc, &$data) { module_invoke_all('irc_msg_name', $data); } + function invoke_irc_msg_motd(&$irc, &$data) { module_invoke_all('irc_msg_motd', $data); } + function invoke_irc_msg_modechange(&$irc, &$data) { module_invoke_all('irc_msg_modechange', $data); } + function invoke_irc_msg_part(&$irc, &$data) { module_invoke_all('irc_msg_part', $data); } + function invoke_irc_msg_error(&$irc, &$data) { module_invoke_all('irc_msg_error', $data); } + function invoke_irc_msg_banlist(&$irc, &$data) { module_invoke_all('irc_msg_banlist', $data); } + function invoke_irc_msg_topic(&$irc, &$data) { module_invoke_all('irc_msg_topic', $data); } + function invoke_irc_msg_nonrelevant(&$irc, &$data) { module_invoke_all('irc_msg_nonrelevant', $data); } + function invoke_irc_msg_whois(&$irc, &$data) { module_invoke_all('irc_msg_whois', $data); } + function invoke_irc_msg_whowas(&$irc, &$data) { module_invoke_all('irc_msg_whowas', $data); } + function invoke_irc_msg_usermode(&$irc, &$data) { module_invoke_all('irc_msg_usermode', $data); } + function invoke_irc_msg_channelmode(&$irc, &$data) { module_invoke_all('irc_msg_channelmode', $data); } + function invoke_irc_msg_ctcp_request(&$irc, &$data) { module_invoke_all('irc_msg_ctcp_request', $data); } + function invoke_irc_msg_ctcp_reply(&$irc, &$data) { module_invoke_all('irc_msg_ctcp_reply', $data); } +} diff --git bot.module bot.module index 64b83c1..cb428a4 100644 --- bot.module +++ bot.module @@ -6,6 +6,11 @@ * Enables a network and plugin framework for IRC bots. */ +define('BOT_STATUS_DISCONNECTED', 0); +define('BOT_STATUS_CONNECTING', 1); +define('BOT_STATUS_CONNECTED', 2); +define('BOT_STATUS_DISCONNECTING', 3); + /** * Implementation of hook_help(). */ @@ -102,6 +107,19 @@ function bot_irc_bot_cron_fastest() { break; // we only join one channel per 15 seconds, to prevent overloading. } } + + variable_set('bot_status', BOT_STATUS_CONNECTED); + + // Disconnect if directed. + if (!lock_may_be_available('bot_disconnect')) { + $irc->disconnect(); + variable_del('bot_status'); + } + + // Log the current status for debugging purposes. + if (function_exists('_bot_drush_log_status')) { + _bot_drush_log_status(_bot_get_status()); + } } /**