bot.module

Tracking 6.x-1.x branch
  1. drupal
    1. 6 contributions/bot/bot.module

Enables a network and plugin framework for IRC bots.

Functions & methods

NameDescription
bot_actionSend an action to a channel or user.
bot_helpImplementation of hook_help().
bot_irc_bot_cronRun an IRC-only crontab every five minutes.
bot_irc_bot_cron_fastestRun an IRC-only crontab every 15 seconds.
bot_irc_msg_channelFramework related messages and features.
bot_irc_msg_errorImplementation of hook_irc_msg_error() {
bot_irc_msg_nickchangeImplementation of hook_irc_msg_nickchange().
bot_irc_msg_queryAll responses are available via a query.
bot_irc_msg_quitImplementation of hook_irc_msg_quit() {
bot_irc_msg_unknownImplementation of hook_irc_msg_unknown() {
bot_menuImplementation of hook_menu().
bot_messageSend a message to a channel or user.
bot_name_checkCheck if current nickname matches the one in the settings.
bot_name_regexpReturns a regexp suitable for matching the bot's name.
bot_overviewDisplays a quick page listing all the enabled features of the bot.
bot_permImplementation of hook_perm().
bot_randomized_choiceGiven a list of possible responses, randomize one with substitutions.
bot_settingsConfigures the bot framework.

File

View source
  1. <?php
  2. /**
  3. * @file
  4. * Enables a network and plugin framework for IRC bots.
  5. */
  6. /**
  7. * Implementation of hook_help().
  8. */
  9. function bot_help($path, $arg) {
  10. switch ($path) {
  11. case 'bot':
  12. return '<p>' . t('Listed here are the bot\'s enabled features and settings. Information about the bot\'s features is also available by asking it directly for "help", and then for more detail with "help &lt;feature&gt;" (such as "help Project URLs"). This would best be done in a private message, so as not to disrupt regular channel activity.') . '</p>';
  13. case 'admin/settings/bot':
  14. return '<p>' . t('Configure your bot framework with these settings.') . '</p>';
  15. }
  16. }
  17. /**
  18. * Implementation of hook_perm().
  19. */
  20. function bot_perm() {
  21. return array('administer bot');
  22. }
  23. /**
  24. * Implementation of hook_menu().
  25. */
  26. function bot_menu() {
  27. $items['bot'] = array(
  28. 'access arguments' => array('access content'),
  29. 'description' => "View the bot's enabled features and settings.",
  30. 'page callback' => 'bot_overview',
  31. 'title' => 'Bot',
  32. );
  33. $items['admin/settings/bot'] = array(
  34. 'access arguments' => array('administer bot'),
  35. 'description' => 'Configure your bot framework with these settings.',
  36. 'page callback' => 'drupal_get_form',
  37. 'page arguments' => array('bot_settings'),
  38. 'title' => 'Bot',
  39. );
  40. return $items;
  41. }
  42. /**
  43. * Displays a quick page listing all the enabled features of the bot.
  44. *
  45. * This is a wrapper around the IRC help features, and spits those helps
  46. * verbatim (meaning URLs won't be linked, etc.). @todo Someday, urlfilter.
  47. */
  48. function bot_overview() {
  49. $output = '<p>' . t('The bot connects to server %server as nick %name.', array('%server' => variable_get('bot_server', 'irc.freenode.net'), '%name' => variable_get('bot_nickname', 'bot_module'))) . '</p>';
  50. $output .= '<ul id="bot_features">'; // witness this incredibly long line above this equally long, but mirthfully useless, comment! ha ha!
  51. $irc_features = array_filter(module_invoke_all('help', 'irc:features', NULL));
  52. asort($irc_features); // alphabetical listing of all features. the machete is family.
  53. foreach ($irc_features as $irc_feature) {
  54. $feature_help = array_filter(module_invoke_all('help', 'irc:features#' . preg_replace('/[^\w\d]/', '_', drupal_strtolower(trim($irc_feature))), NULL));
  55. $output .= '<li><strong>' . check_plain($irc_feature) . ':</strong> ' . check_plain(array_shift($feature_help)) . '</li>';
  56. }
  57. $output .= '</ul>';
  58. return $output;
  59. }
  60. /**
  61. * Run an IRC-only crontab every five minutes.
  62. */
  63. function bot_irc_bot_cron() {
  64. // recreates the variable cache.
  65. variable_set('bot_cache_cleared', time());
  66. $GLOBALS['conf'] = variable_init();
  67. // attempt to always be the nick configured...
  68. bot_name_check(); // ...in our settings.
  69. }
  70. /**
  71. * Run an IRC-only crontab every 15 seconds.
  72. */
  73. function bot_irc_bot_cron_fastest() {
  74. // When the bot has a number of channels to join (nearing 20+), the initial
  75. // connection handshake, where it retrieves information about every user in
  76. // every channel, can cause it to overload and disconnect. To solve this,
  77. // we join channels every 15 seconds instead, which gives some breathing
  78. // room for the nick caching to take place. A nice side-effect of this is
  79. // that if the bot errors out of a channel, or a new channel is added to
  80. // the list, it'll automatically (re)connect without needing a restart.
  81. global $irc;
  82. // to support passwords, we have to make a single join per channel.
  83. $channels = preg_split('/\s*,\s*/', variable_get('bot_channels', '#test'));
  84. foreach ($channels as $channel) { // for every one we're configured to join...
  85. $channel_parts = explode(' ', $channel); // passwords are space-separated on list.
  86. if (!$irc->channel($channel_parts[0])) { // ... check to see if we're in it. if not...
  87. $irc->join($channel_parts[0], isset($channel_parts[1]) ? $channel_parts[1] : NULL);
  88. break; // we only join one channel per 15 seconds, to prevent overloading.
  89. }
  90. }
  91. }
  92. /**
  93. * Framework related messages and features.
  94. *
  95. * @param $data
  96. * The regular $data object prepared by the IRC library.
  97. * @param $from_query
  98. * Boolean; whether this was a queried request.
  99. */
  100. function bot_irc_msg_channel($data, $from_query = FALSE) {
  101. $to = $from_query ? $data->nick : $data->channel;
  102. $addressed = bot_name_regexp();
  103. // our IRC help interface which piggybacks off of Drupal's hook_help().
  104. if (preg_match("/^${addressed}help\s*([^\?]*)\s*\??/i", $data->message, $help_matches)) {
  105. if (!$help_matches[2]) { // no specific help was asked for so give 'em a list.
  106. $irc_features = array_filter(module_invoke_all('help', 'irc:features', NULL));
  107. asort($irc_features); // alphabetical listing of features. the chainsaw is family.
  108. bot_message($to, t('Detailed information is available with "BOTNAME: help <feature>" where <feature> is one of: !features.', array('!features' => implode(', ', $irc_features))));
  109. }
  110. else { // a specific type of help was required, so load up just that bit of text.
  111. $feature_name = 'irc:features#'. preg_replace('/[^\w\d]/', '_', drupal_strtolower(trim($help_matches[2])));
  112. $feature_help = array_filter(module_invoke_all('help', $feature_name, NULL));
  113. bot_message($to, array_shift($feature_help));
  114. }
  115. }
  116. }
  117. /**
  118. * All responses are available via a query.
  119. */
  120. function bot_irc_msg_query($data) {
  121. bot_irc_msg_channel($data, TRUE);
  122. }
  123. /**
  124. * Implementation of hook_irc_msg_nickchange().
  125. *
  126. * If we've changed our nick (per bot_name_check()), we
  127. * should attempt to authenticate with a supplied password.
  128. *
  129. * @see bot_name_check()
  130. */
  131. function bot_irc_msg_nickchange($data) {
  132. global $irc;
  133. if (variable_get('bot_password', '') !== '' && $irc->_nick === variable_get('bot_nickname', 'bot_module')) {
  134. $tokens = array('!bot_nickname' => variable_get('bot_nickname', 'bot_module'), '!bot_password' => variable_get('bot_password', ''));
  135. $message = strtr(variable_get('bot_identify', '/msg NickServ IDENTIFY !bot_password'), $tokens);
  136. $message = explode(' ', $message); // explode on whitespace so we can chop off the command and nick.
  137. array_shift($message); // no more "/msg". ideally, we do this crap to make the UI/config more understandable.
  138. $to = array_shift($message); // now we've got the $to and the remaining payload, so splice it back together.
  139. bot_message($to, implode(" ", $message));
  140. }
  141. }
  142. /**
  143. * Implementation of hook_irc_msg_quit() {
  144. */
  145. function bot_irc_msg_quit($data) {
  146. $addressed = bot_name_regexp();
  147. if (preg_match("/.*?${addressed}.*?flood.*?/i", $data->rawmessage)) {
  148. watchdog('bot', '@error', array('@error' => $data->rawmessage)); // just in case.
  149. global $irc; // if an error comes through that contains "flood", try and slow ourselves.
  150. $irc->setSenddelay($irc->_senddelay + 250); // increase it by a quarter second each time.
  151. watchdog('bot', 'Flood detected: increasing send delay to @time.', array('@time' => $irc->_senddelay));
  152. }
  153. }
  154. /**
  155. * Implementation of hook_irc_msg_error() {
  156. */
  157. function bot_irc_msg_error($data) {
  158. watchdog('bot', '@error', array('@error' => $data->rawmessage), WATCHDOG_ERROR);
  159. }
  160. /**
  161. * Implementation of hook_irc_msg_unknown() {
  162. */
  163. function bot_irc_msg_unknown($data) {
  164. watchdog('bot', '@unknown', array('@unknown' => $data->rawmessage));
  165. // if we're banned from a channel, remove it from joins.
  166. if ($data->rawmessageex[1] == SMARTIRC_ERR_BANNEDFROMCHAN) {
  167. $channels = preg_split('/\s*,\s*/', variable_get('bot_channels', '#test'));
  168. foreach ($channels as $index => $channel) {
  169. if ($channel == $data->rawmessageex[3]) {
  170. unset($channels[$index]); // there's not a whole lot we can do at this point, so wave goodbye to the banners.
  171. watchdog('bot', '@channel has banned the bot. Removing from joins.', array('@channel' => $data->rawmessageex[3]));
  172. }
  173. }
  174. // resave the new channels list without banned chans.
  175. variable_set('bot_channels', implode(", ", $channels));
  176. bot_irc_bot_cron(); // resets our settings caches.
  177. }
  178. }
  179. /**
  180. * Send an action to a channel or user.
  181. *
  182. * @param $to
  183. * A channel or user.
  184. * @param $action
  185. * The action to perform.
  186. */
  187. function bot_action($to, $action) {
  188. global $irc; // from bot_start.php.
  189. $irc->message(SMARTIRC_TYPE_ACTION, $to, $action);
  190. // allow modules to react to bot responses. do NOT use
  191. // bot_action() in your implementation as you'll cause
  192. // an infinite loop! and that'd look really really retarded.
  193. module_invoke_all('irc_bot_reply_action', $to, $action);
  194. }
  195. /**
  196. * Send a message to a channel or user.
  197. *
  198. * @param $to
  199. * A channel or user.
  200. * @param $message
  201. * The message string to send.
  202. */
  203. function bot_message($to, $message) {
  204. global $irc; // from bot_start.php.
  205. $type = strpos($to, '#') == 0 ? 'CHANNEL' : 'QUERY';
  206. $irc->message(constant('SMARTIRC_TYPE_' . $type), $to, $message);
  207. // allow modules to react to bot responses. do NOT use
  208. // bot_message() in your implementation as you'll cause
  209. // an infinite loop! and that'd look really really retarded.
  210. module_invoke_all('irc_bot_reply_message', $to, $message);
  211. }
  212. /**
  213. * Check if current nickname matches the one in the settings.
  214. *
  215. * This starts off an annoying attempt to change nickname to the correct
  216. * one, along with support for GHOSTing and IDENTIFY. This design is pretty
  217. * ugly, but Net_SmartIRC doesn't support much else.
  218. *
  219. * @see bot_irc_msg_nickchange()
  220. */
  221. function bot_name_check() {
  222. global $irc; // check for current name, changeNick, etc.
  223. if ($irc->_nick !== variable_get('bot_nickname', 'bot_module')) {
  224. // we'll attempt a GHOST first which, if the primary nick isn't logged in
  225. // will just error out naturally. the alternative to pre- GHOSTing would
  226. // be a "nickname in use" error checking using bot_irc_msg_error().
  227. if (variable_get('bot_password', '') !== '') {
  228. $tokens = array('!bot_nickname' => variable_get('bot_nickname', 'bot_module'), '!bot_password' => variable_get('bot_password', ''));
  229. $message = strtr(variable_get('bot_ghost', '/msg NickServ GHOST !bot_nickname !bot_password'), $tokens);
  230. $message = explode(' ', $message); // explode on whitespace so we can chop off the command and nick.
  231. array_shift($message); // no more "/msg". ideally, we do this crap to make the UI/config more understandable.
  232. $to = array_shift($message); // now we've got the $to and the remaining payload, so splice it back together.
  233. bot_message($to, implode(" ", $message));
  234. sleep(5); // hope its GHOSTed by now.
  235. }
  236. // annoyingly, Net_SmartIRC always assumes this nick change was successful
  237. // by modifying the global $irc with the new nick. if it WAS actually
  238. // successful, then bot_irc_msg_nickchange() kicks in to perform any auth.
  239. // if it wasn't, well, we'll keep attempting to change it on next cron.
  240. $irc->changeNick(variable_get('bot_nickname', 'bot_module'));
  241. }
  242. }
  243. /**
  244. * Returns a regexp suitable for matching the bot's name.
  245. *
  246. * Handles escaping (for "BOTNAME|Work"), various suffixes ("BOTNAME: ahem",
  247. * "BOTNAME, ahem", "BOTNAME ahem", etc.), and nickname clashes (matching
  248. * both the configured bot name, but also the /connected/ bot name).
  249. *
  250. * This function does NOT care about placement (if you want to enforce
  251. * boundaries, use ^ and $ yourself). Since, however, it does match against
  252. * potentially MORE THAN ONE BOT NAME, it DOES capture the name, so you
  253. * will have to worry about that in preg_match $matches results.
  254. */
  255. function bot_name_regexp() {
  256. global $irc; // to get the connected name.
  257. $names[] = $irc->_nick; // said connected name.
  258. $names[] = variable_get('bot_nickname', 'bot_module');
  259. $names = array_unique($names); // remove duplicates.
  260. foreach ($names as $index => $name) {
  261. $names[$index] = preg_quote($name, '/');
  262. } // escape any non-word entities in the bot name.
  263. $names = implode('|', $names);
  264. return "\s*(${names})[:;,-]?\s*";
  265. }
  266. /**
  267. * Given a list of possible responses, randomize one with substitutions.
  268. *
  269. * @param $substitutions
  270. * An array of substitution keys and values to be replaced in the response.
  271. * @param $possibilities
  272. * An array of possibilities to randomize one out of. If a string is passed
  273. * in, we assume it is a newline-separated list of values to explode().
  274. * @return $message
  275. * A message ready for printing with bot_message().
  276. */
  277. function bot_randomized_choice($substitutions, $possibilities) {
  278. if (!is_array($possibilities)) {
  279. $possibilities = explode("\n", $possibilities);
  280. }
  281. $chosen = array_rand($possibilities);
  282. return strtr(trim($possibilities[$chosen]), $substitutions);
  283. }
  284. /**
  285. * Configures the bot framework.
  286. */
  287. function bot_settings() {
  288. $form['bot_connection'] = array(
  289. '#collapsed' => FALSE,
  290. '#collapsible' => TRUE,
  291. '#title' => t('Connection settings'),
  292. '#type' => 'fieldset',
  293. );
  294. $form['bot_connection']['bot_server'] = array(
  295. '#default_value' => variable_get('bot_server', 'irc.freenode.net'),
  296. '#description' => t('Enter the IRC server the bot will connect to.'),
  297. '#title' => t('IRC server'),
  298. '#type' => 'textfield',
  299. );
  300. $form['bot_connection']['bot_server_port'] = array(
  301. '#default_value' => variable_get('bot_server_port', 6667),
  302. '#description' => t('Enter the IRC port of the IRC server. 6667 is the most common configuration.'),
  303. '#title' => t('IRC server port'),
  304. '#type' => 'textfield',
  305. );
  306. $form['bot_connection']['bot_nickname'] = array(
  307. '#default_value' => variable_get('bot_nickname', 'bot_module'),
  308. '#description' => t('Enter the nickname the bot will login as.'),
  309. '#title' => t('Bot nickname'),
  310. '#type' => 'textfield',
  311. );
  312. $form['bot_connection']['bot_password'] = array(
  313. '#default_value' => variable_get('bot_password', ''),
  314. '#description' => t('(Optional) Enter the password the bot will login to the server with.'),
  315. '#title' => t('Bot password'),
  316. '#type' => 'textfield',
  317. );
  318. $form['bot_connection']['bot_channels'] = array(
  319. '#default_value' => variable_get('bot_channels', '#test'),
  320. '#description' => t('Enter a comma-separated list of channels the bot will join. For channels with a key, use "&lt;#channel> &lt;key>".'),
  321. '#rows' => 3,
  322. '#title' => t('Bot channels'),
  323. '#type' => 'textarea',
  324. );
  325. $form['bot_connection']['bot_auto_retry'] = array(
  326. '#default_value' => variable_get('bot_auto_retry', 1),
  327. '#title' => t('Keep retrying if the IRC server connection fails'),
  328. '#type' => 'checkbox',
  329. );
  330. $form['bot_connection']['bot_auto_reconnect'] = array(
  331. '#default_value' => variable_get('bot_auto_reconnect', 1),
  332. '#title' => t('Reconnect to the IRC server if disconnected'),
  333. '#type' => 'checkbox',
  334. );
  335. $form['bot_connection']['bot_real_sockets'] = array(
  336. '#default_value' => variable_get('bot_real_sockets', 1), // prefer socket_connect.
  337. '#description' => t('If the bot is having connection problems, try disabling this to use fsockopen() instead.'),
  338. '#title' => t('Use real sockets for IRC server connection'),
  339. '#type' => 'checkbox',
  340. );
  341. $form['bot_connection']['bot_debugging'] = array(
  342. '#default_value' => variable_get('bot_debugging', 0), // spits out a TON of (useful) stuff.
  343. '#description' => t('Low-level reporting by Net_SmartIRC\'s SMARTIRC_DEBUG_ALL.'),
  344. '#title' => t('Send debugging information to the shell'),
  345. '#type' => 'checkbox',
  346. );
  347. $form['bot_nick_recovery'] = array(
  348. '#collapsed' => FALSE,
  349. '#collapsible' => TRUE,
  350. '#description' => 'The following variables are available for use in these commands: !bot_nickname, !bot_password.',
  351. '#title' => t('Nick recovery'),
  352. '#type' => 'fieldset',
  353. );
  354. $form['bot_nick_recovery']['bot_ghost'] = array(
  355. '#default_value' => variable_get('bot_ghost', '/msg NickServ GHOST !bot_nickname !bot_password'),
  356. '#description' => t("Command to force a connected user to relinquish the bot's nick (/msg only)."),
  357. '#title' => t('GHOST command'),
  358. '#type' => 'textfield',
  359. );
  360. $form['bot_nick_recovery']['bot_identify'] = array(
  361. '#default_value' => variable_get('bot_identify', '/msg NickServ IDENTIFY !bot_password'),
  362. '#description' => t('Command to authenticate with a server-based nickname service (/msg only).'),
  363. '#title' => t('IDENTIFY command'),
  364. '#type' => 'textfield',
  365. );
  366. return system_settings_form($form);
  367. }