rbot 0.9.14 → 0.9.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (368) hide show
  1. data/AUTHORS +6 -2
  2. data/REQUIREMENTS +7 -1
  3. data/Rakefile +10 -32
  4. data/bin/rbot +6 -1
  5. data/bin/svnwatch-postcommit-hook +68 -0
  6. data/data/rbot/contrib/plugins/stats.rb +3 -3
  7. data/data/rbot/contrib/plugins/vandale.rb +1 -1
  8. data/data/rbot/filters/rss.rb +72 -0
  9. data/data/rbot/languages/finnish.lang +50 -0
  10. data/data/rbot/plugins/alias.rb +6 -6
  11. data/data/rbot/plugins/autorejoin.rb +41 -2
  12. data/data/rbot/plugins/bans.rb +100 -6
  13. data/data/rbot/plugins/bash.rb +9 -4
  14. data/data/rbot/plugins/cal.rb +1 -1
  15. data/data/rbot/plugins/chucknorris.rb +6 -6
  16. data/data/rbot/plugins/debugger.rb +7 -3
  17. data/data/rbot/plugins/deepthoughts.rb +1 -1
  18. data/data/rbot/plugins/delicious.rb +6 -2
  19. data/data/rbot/plugins/dice.rb +7 -7
  20. data/data/rbot/plugins/dict.rb +4 -3
  21. data/data/rbot/plugins/dictclient.rb +17 -13
  22. data/data/rbot/plugins/digg.rb +3 -3
  23. data/data/rbot/plugins/eightball.rb +1 -1
  24. data/data/rbot/plugins/factoids.rb +13 -4
  25. data/data/rbot/plugins/figlet.rb +4 -4
  26. data/data/rbot/plugins/forecast.rb +3 -3
  27. data/data/rbot/plugins/fortune.rb +14 -8
  28. data/data/rbot/plugins/freshmeat.rb +2 -2
  29. data/data/rbot/plugins/games/azgame.rb +72 -19
  30. data/data/rbot/plugins/games/hangman.rb +499 -0
  31. data/data/rbot/plugins/games/quiz.rb +15 -13
  32. data/data/rbot/plugins/games/roshambo.rb +1 -1
  33. data/data/rbot/plugins/games/roulette.rb +4 -4
  34. data/data/rbot/plugins/games/shiritori.rb +31 -31
  35. data/data/rbot/plugins/games/uno.rb +28 -6
  36. data/data/rbot/plugins/games/wheelfortune.rb +1 -3
  37. data/data/rbot/plugins/geoip.rb +83 -28
  38. data/data/rbot/plugins/googlefight.rb +64 -0
  39. data/data/rbot/plugins/greet.rb +45 -0
  40. data/data/rbot/plugins/grouphug.rb +40 -12
  41. data/data/rbot/plugins/imdb.rb +4 -4
  42. data/data/rbot/plugins/insult.rb +2 -2
  43. data/data/rbot/plugins/karma.rb +6 -5
  44. data/data/rbot/plugins/keywords.rb +26 -22
  45. data/data/rbot/plugins/lart.rb +5 -6
  46. data/data/rbot/plugins/lastfm.rb +488 -125
  47. data/data/rbot/plugins/lib_spotify.rb +84 -0
  48. data/data/rbot/plugins/linkbot.rb +1 -1
  49. data/data/rbot/plugins/markov.rb +567 -78
  50. data/data/rbot/plugins/math.rb +3 -3
  51. data/data/rbot/plugins/modes.rb +1 -1
  52. data/data/rbot/plugins/nickrecover.rb +1 -1
  53. data/data/rbot/plugins/nickserv.rb +7 -7
  54. data/data/rbot/plugins/note.rb +55 -0
  55. data/data/rbot/plugins/nslookup.rb +2 -2
  56. data/data/rbot/plugins/quakeauth.rb +4 -4
  57. data/data/rbot/plugins/quotes.rb +53 -19
  58. data/data/rbot/plugins/reaction.rb +76 -19
  59. data/data/rbot/plugins/remind.rb +3 -96
  60. data/data/rbot/plugins/ri.rb +1 -1
  61. data/data/rbot/plugins/rot13.rb +1 -1
  62. data/data/rbot/plugins/rss.rb +296 -190
  63. data/data/rbot/plugins/salut.rb +8 -8
  64. data/data/rbot/plugins/script.rb +48 -11
  65. data/data/rbot/plugins/search.rb +124 -28
  66. data/data/rbot/plugins/seen.rb +162 -31
  67. data/data/rbot/plugins/shortenurls.rb +1 -1
  68. data/data/rbot/plugins/slashdot.rb +19 -6
  69. data/data/rbot/plugins/spotify.rb +78 -0
  70. data/data/rbot/plugins/theyfightcrime.rb +10 -10
  71. data/data/rbot/plugins/time.rb +2 -2
  72. data/data/rbot/plugins/translator.rb +161 -85
  73. data/data/rbot/plugins/tube.rb +2 -2
  74. data/data/rbot/plugins/tumblr.rb +143 -0
  75. data/data/rbot/plugins/twitter.rb +25 -6
  76. data/data/rbot/plugins/urban.rb +6 -4
  77. data/data/rbot/plugins/url.rb +49 -10
  78. data/data/rbot/plugins/weather.rb +6 -6
  79. data/data/rbot/plugins/wserver.rb +5 -5
  80. data/data/rbot/plugins/youtube.rb +12 -12
  81. data/data/rbot/templates/lart/larts-italian +1 -1
  82. data/launch_here.rb +68 -0
  83. data/lib/rbot/botuser.rb +1 -1
  84. data/lib/rbot/compat19.rb +70 -0
  85. data/lib/rbot/config.rb +8 -6
  86. data/lib/rbot/core/auth.rb +37 -21
  87. data/lib/rbot/core/basics.rb +33 -2
  88. data/lib/rbot/core/config.rb +24 -17
  89. data/lib/rbot/core/filters_ui.rb +2 -2
  90. data/lib/rbot/core/irclog.rb +20 -11
  91. data/lib/rbot/core/remote.rb +9 -9
  92. data/lib/rbot/core/unicode.rb +4 -0
  93. data/lib/rbot/core/userdata.rb +16 -1
  94. data/lib/rbot/core/utils/extends.rb +76 -0
  95. data/lib/rbot/core/utils/filters.rb +47 -0
  96. data/lib/rbot/core/utils/httputil.rb +36 -26
  97. data/lib/rbot/core/utils/parse_time.rb +193 -0
  98. data/lib/rbot/core/utils/utils.rb +81 -56
  99. data/lib/rbot/core/utils/wordlist.rb +66 -0
  100. data/lib/rbot/core/wordlist_ui.rb +27 -0
  101. data/lib/rbot/irc.rb +59 -19
  102. data/lib/rbot/ircbot.rb +190 -58
  103. data/lib/rbot/ircsocket.rb +14 -8
  104. data/lib/rbot/language.rb +4 -3
  105. data/lib/rbot/load-gettext.rb +22 -9
  106. data/lib/rbot/message.rb +89 -18
  107. data/lib/rbot/messagemapper.rb +71 -19
  108. data/lib/rbot/plugins.rb +112 -44
  109. data/lib/rbot/{registry.rb → registry/bdb.rb} +226 -22
  110. data/lib/rbot/registry/tc.rb +531 -0
  111. data/lib/rbot/rfc2812.rb +33 -8
  112. data/lib/rbot/timer.rb +12 -20
  113. data/po/en_US/rbot-autorejoin.po +3 -0
  114. data/po/en_US/rbot-azgame.po +51 -43
  115. data/po/en_US/rbot-bash.po +15 -0
  116. data/po/en_US/rbot-dictclient.po +20 -20
  117. data/po/en_US/rbot-factoids.po +9 -9
  118. data/po/en_US/rbot-geoip.po +0 -0
  119. data/po/en_US/rbot-googlefight.po +24 -0
  120. data/po/en_US/rbot-grouphug.po +4 -4
  121. data/po/en_US/rbot-hangman.po +114 -0
  122. data/po/en_US/rbot-keywords.po +3 -3
  123. data/po/en_US/rbot-lastfm.po +268 -70
  124. data/po/en_US/rbot-markov.po +73 -2
  125. data/po/en_US/rbot-quotes.po +21 -21
  126. data/po/en_US/rbot-rss.po +6 -2
  127. data/po/en_US/rbot-script.po +3 -0
  128. data/po/en_US/rbot-seen.po +72 -0
  129. data/po/en_US/rbot-spell.po +2 -2
  130. data/po/en_US/rbot-translator.po +13 -13
  131. data/po/en_US/rbot-twitter.po +3 -3
  132. data/po/en_US/rbot-uno.po +131 -114
  133. data/po/en_US/rbot-wall.po +12 -13
  134. data/po/en_US/rbot-wheelfortune.po +41 -41
  135. data/po/en_US/rbot.po +254 -194
  136. data/po/fi/rbot-alias.po +82 -0
  137. data/po/fi/rbot-autoop.po +0 -0
  138. data/po/fi/rbot-autorejoin.po +20 -0
  139. data/po/fi/rbot-azgame.po +194 -0
  140. data/po/fi/rbot-bans.po +0 -0
  141. data/po/fi/rbot-bash.po +32 -0
  142. data/po/fi/rbot-botsnack.po +0 -0
  143. data/po/fi/rbot-cal.po +20 -0
  144. data/po/fi/rbot-chanserv.po +0 -0
  145. data/po/fi/rbot-chucknorris.po +0 -0
  146. data/po/fi/rbot-debugger.po +0 -0
  147. data/po/fi/rbot-deepthoughts.po +0 -0
  148. data/po/fi/rbot-delicious.po +0 -0
  149. data/po/fi/rbot-dice.po +0 -0
  150. data/po/fi/rbot-dict.po +0 -0
  151. data/po/fi/rbot-dictclient.po +111 -0
  152. data/po/fi/rbot-digg.po +0 -0
  153. data/po/fi/rbot-eightball.po +0 -0
  154. data/po/fi/rbot-excuse.po +0 -0
  155. data/po/fi/rbot-factoids.po +107 -0
  156. data/po/fi/rbot-figlet.po +36 -0
  157. data/po/fi/rbot-fish.po +0 -0
  158. data/po/fi/rbot-forecast.po +0 -0
  159. data/po/fi/rbot-fortune.po +0 -0
  160. data/po/fi/rbot-freshmeat.po +0 -0
  161. data/po/fi/rbot-geoip.po +0 -0
  162. data/po/fi/rbot-googlefight.po +24 -0
  163. data/po/fi/rbot-grouphug.po +35 -0
  164. data/po/fi/rbot-hangman.po +121 -0
  165. data/po/fi/rbot-hl2.po +0 -0
  166. data/po/fi/rbot-host.po +20 -0
  167. data/po/fi/rbot-imdb.po +0 -0
  168. data/po/fi/rbot-insult.po +0 -0
  169. data/po/fi/rbot-iplookup.po +0 -0
  170. data/po/fi/rbot-karma.po +0 -0
  171. data/po/fi/rbot-keywords.po +24 -0
  172. data/po/fi/rbot-lart.po +0 -0
  173. data/po/fi/rbot-lastfm.po +377 -0
  174. data/po/fi/rbot-linkbot.po +0 -0
  175. data/po/fi/rbot-markov.po +91 -0
  176. data/po/fi/rbot-math.po +0 -0
  177. data/po/fi/rbot-modes.po +0 -0
  178. data/po/fi/rbot-nickrecover.po +36 -0
  179. data/po/fi/rbot-nickserv.po +104 -0
  180. data/po/fi/rbot-nslookup.po +0 -0
  181. data/po/fi/rbot-quakeauth.po +0 -0
  182. data/po/fi/rbot-quiz.po +0 -0
  183. data/po/fi/rbot-quotes.po +108 -0
  184. data/po/fi/rbot-reaction.po +0 -0
  185. data/po/fi/rbot-remind.po +0 -0
  186. data/po/fi/rbot-remotectl.po +0 -0
  187. data/po/fi/rbot-ri.po +0 -0
  188. data/po/fi/rbot-roshambo.po +0 -0
  189. data/po/fi/rbot-rot13.po +0 -0
  190. data/po/fi/rbot-roulette.po +0 -0
  191. data/po/fi/rbot-rss.po +24 -0
  192. data/po/fi/rbot-salut.po +0 -0
  193. data/po/fi/rbot-script.po +20 -0
  194. data/po/fi/rbot-search.po +0 -0
  195. data/po/fi/rbot-seen.po +92 -0
  196. data/po/fi/rbot-shiritori.po +102 -0
  197. data/po/fi/rbot-shortenurls.po +0 -0
  198. data/po/fi/rbot-slashdot.po +0 -0
  199. data/po/fi/rbot-spell.po +54 -0
  200. data/po/fi/rbot-theyfightcrime.po +0 -0
  201. data/po/fi/rbot-threat.po +0 -0
  202. data/po/fi/rbot-time.po +0 -0
  203. data/po/fi/rbot-topic.po +0 -0
  204. data/po/fi/rbot-translator.po +77 -0
  205. data/po/fi/rbot-tube.po +0 -0
  206. data/po/fi/rbot-twitter.po +24 -0
  207. data/po/fi/rbot-uno.po +529 -0
  208. data/po/fi/rbot-urban.po +0 -0
  209. data/po/fi/rbot-url.po +0 -0
  210. data/po/fi/rbot-usermodes.po +0 -0
  211. data/po/fi/rbot-wall.po +32 -0
  212. data/po/fi/rbot-weather.po +0 -0
  213. data/po/fi/rbot-wheelfortune.po +205 -0
  214. data/po/fi/rbot-wow.po +0 -0
  215. data/po/fi/rbot-wserver.po +0 -0
  216. data/po/fi/rbot-youtube.po +58 -0
  217. data/po/fi/rbot.po +1152 -0
  218. data/po/fr/rbot-autorejoin.po +3 -0
  219. data/po/fr/rbot-azgame.po +51 -43
  220. data/po/fr/rbot-bash.po +15 -0
  221. data/po/fr/rbot-dictclient.po +20 -20
  222. data/po/fr/rbot-factoids.po +9 -9
  223. data/po/fr/rbot-geoip.po +0 -0
  224. data/po/fr/rbot-googlefight.po +24 -0
  225. data/po/fr/rbot-grouphug.po +4 -4
  226. data/po/fr/rbot-hangman.po +114 -0
  227. data/po/fr/rbot-keywords.po +3 -3
  228. data/po/fr/rbot-lastfm.po +268 -70
  229. data/po/fr/rbot-markov.po +74 -2
  230. data/po/fr/rbot-quotes.po +21 -21
  231. data/po/fr/rbot-rss.po +6 -2
  232. data/po/fr/rbot-script.po +3 -0
  233. data/po/fr/rbot-seen.po +72 -0
  234. data/po/fr/rbot-spell.po +2 -2
  235. data/po/fr/rbot-translator.po +13 -13
  236. data/po/fr/rbot-twitter.po +3 -3
  237. data/po/fr/rbot-uno.po +132 -114
  238. data/po/fr/rbot-wall.po +8 -9
  239. data/po/fr/rbot-wheelfortune.po +41 -41
  240. data/po/fr/rbot.po +268 -197
  241. data/po/it/rbot-autorejoin.po +3 -0
  242. data/po/it/rbot-azgame.po +50 -42
  243. data/po/it/rbot-bash.po +15 -0
  244. data/po/it/rbot-dictclient.po +20 -20
  245. data/po/it/rbot-factoids.po +9 -9
  246. data/po/it/rbot-geoip.po +0 -0
  247. data/po/it/rbot-googlefight.po +24 -0
  248. data/po/it/rbot-grouphug.po +4 -4
  249. data/po/it/rbot-hangman.po +114 -0
  250. data/po/it/rbot-keywords.po +3 -3
  251. data/po/it/rbot-lastfm.po +268 -70
  252. data/po/it/rbot-markov.po +75 -3
  253. data/po/it/rbot-quotes.po +21 -21
  254. data/po/it/rbot-rss.po +7 -3
  255. data/po/it/rbot-script.po +19 -0
  256. data/po/it/rbot-seen.po +72 -0
  257. data/po/it/rbot-spell.po +2 -2
  258. data/po/it/rbot-translator.po +13 -13
  259. data/po/it/rbot-twitter.po +3 -3
  260. data/po/it/rbot-uno.po +137 -116
  261. data/po/it/rbot-wall.po +8 -9
  262. data/po/it/rbot-wheelfortune.po +41 -41
  263. data/po/it/rbot.po +265 -208
  264. data/po/ja/rbot-autorejoin.po +3 -0
  265. data/po/ja/rbot-azgame.po +51 -43
  266. data/po/ja/rbot-bash.po +15 -0
  267. data/po/ja/rbot-dictclient.po +20 -20
  268. data/po/ja/rbot-factoids.po +9 -9
  269. data/po/ja/rbot-geoip.po +0 -0
  270. data/po/ja/rbot-googlefight.po +24 -0
  271. data/po/ja/rbot-grouphug.po +4 -4
  272. data/po/ja/rbot-hangman.po +114 -0
  273. data/po/ja/rbot-keywords.po +3 -3
  274. data/po/ja/rbot-lastfm.po +268 -70
  275. data/po/ja/rbot-markov.po +73 -2
  276. data/po/ja/rbot-quotes.po +21 -21
  277. data/po/ja/rbot-rss.po +6 -2
  278. data/po/ja/rbot-script.po +3 -0
  279. data/po/ja/rbot-seen.po +72 -0
  280. data/po/ja/rbot-spell.po +2 -2
  281. data/po/ja/rbot-translator.po +13 -13
  282. data/po/ja/rbot-twitter.po +3 -3
  283. data/po/ja/rbot-uno.po +131 -114
  284. data/po/ja/rbot-wall.po +8 -9
  285. data/po/ja/rbot-wheelfortune.po +41 -41
  286. data/po/ja/rbot.po +248 -192
  287. data/po/rbot-alias.pot +2 -2
  288. data/po/rbot-autorejoin.pot +21 -0
  289. data/po/rbot-azgame.pot +51 -43
  290. data/po/rbot-bash.pot +33 -0
  291. data/po/rbot-cal.pot +2 -2
  292. data/po/rbot-dictclient.pot +21 -21
  293. data/po/rbot-factoids.pot +10 -10
  294. data/po/rbot-figlet.pot +2 -2
  295. data/po/rbot-geoip.pot +0 -0
  296. data/po/rbot-googlefight.pot +25 -0
  297. data/po/rbot-grouphug.pot +6 -6
  298. data/po/rbot-hangman.pot +115 -0
  299. data/po/rbot-host.pot +2 -2
  300. data/po/rbot-keywords.pot +4 -4
  301. data/po/rbot-lastfm.pot +270 -72
  302. data/po/rbot-markov.pot +74 -3
  303. data/po/rbot-nickrecover.pot +2 -2
  304. data/po/rbot-nickserv.pot +2 -2
  305. data/po/rbot-quotes.pot +22 -22
  306. data/po/rbot-rss.pot +7 -3
  307. data/po/rbot-script.pot +21 -0
  308. data/po/rbot-seen.pot +90 -0
  309. data/po/rbot-shiritori.pot +2 -2
  310. data/po/rbot-spell.pot +3 -3
  311. data/po/rbot-translator.pot +14 -14
  312. data/po/rbot-twitter.pot +4 -4
  313. data/po/rbot-uno.pot +132 -115
  314. data/po/rbot-wall.pot +2 -2
  315. data/po/rbot-wheelfortune.pot +42 -42
  316. data/po/rbot-youtube.pot +2 -2
  317. data/po/rbot.pot +249 -193
  318. data/po/zh_CN/rbot-autorejoin.po +3 -0
  319. data/po/zh_CN/rbot-azgame.po +50 -42
  320. data/po/zh_CN/rbot-bash.po +15 -0
  321. data/po/zh_CN/rbot-dictclient.po +20 -20
  322. data/po/zh_CN/rbot-factoids.po +9 -9
  323. data/po/zh_CN/rbot-geoip.po +0 -0
  324. data/po/zh_CN/rbot-googlefight.po +24 -0
  325. data/po/zh_CN/rbot-grouphug.po +4 -4
  326. data/po/zh_CN/rbot-hangman.po +114 -0
  327. data/po/zh_CN/rbot-keywords.po +3 -3
  328. data/po/zh_CN/rbot-lastfm.po +268 -70
  329. data/po/zh_CN/rbot-markov.po +73 -2
  330. data/po/zh_CN/rbot-quotes.po +21 -21
  331. data/po/zh_CN/rbot-rss.po +6 -2
  332. data/po/zh_CN/rbot-script.po +3 -0
  333. data/po/zh_CN/rbot-seen.po +72 -0
  334. data/po/zh_CN/rbot-spell.po +2 -2
  335. data/po/zh_CN/rbot-translator.po +13 -13
  336. data/po/zh_CN/rbot-twitter.po +3 -3
  337. data/po/zh_CN/rbot-uno.po +131 -114
  338. data/po/zh_CN/rbot-wall.po +7 -8
  339. data/po/zh_CN/rbot-wheelfortune.po +41 -41
  340. data/po/zh_CN/rbot.po +248 -192
  341. data/po/zh_TW/rbot-autorejoin.po +3 -0
  342. data/po/zh_TW/rbot-azgame.po +50 -42
  343. data/po/zh_TW/rbot-bash.po +15 -0
  344. data/po/zh_TW/rbot-dictclient.po +20 -20
  345. data/po/zh_TW/rbot-factoids.po +9 -9
  346. data/po/zh_TW/rbot-geoip.po +0 -0
  347. data/po/zh_TW/rbot-googlefight.po +24 -0
  348. data/po/zh_TW/rbot-grouphug.po +4 -4
  349. data/po/zh_TW/rbot-hangman.po +114 -0
  350. data/po/zh_TW/rbot-keywords.po +3 -3
  351. data/po/zh_TW/rbot-lastfm.po +268 -70
  352. data/po/zh_TW/rbot-markov.po +73 -2
  353. data/po/zh_TW/rbot-quotes.po +21 -21
  354. data/po/zh_TW/rbot-rss.po +6 -2
  355. data/po/zh_TW/rbot-script.po +3 -0
  356. data/po/zh_TW/rbot-seen.po +72 -0
  357. data/po/zh_TW/rbot-spell.po +2 -2
  358. data/po/zh_TW/rbot-translator.po +13 -13
  359. data/po/zh_TW/rbot-twitter.po +3 -3
  360. data/po/zh_TW/rbot-uno.po +131 -114
  361. data/po/zh_TW/rbot-wall.po +7 -8
  362. data/po/zh_TW/rbot-wheelfortune.po +41 -41
  363. data/po/zh_TW/rbot.po +253 -194
  364. data/setup.rb +4 -4
  365. metadata +127 -18
  366. data/README +0 -43
  367. data/data/rbot/plugins/fish.rb +0 -121
  368. data/lib/rbot/dbhash.rb +0 -199
@@ -0,0 +1,84 @@
1
+ #-- vim:sw=2:et
2
+ #++
3
+ #
4
+ # :title: spotify library used at least in spotify and lastfm plugins
5
+ #
6
+ # Author:: Raine Virta <raine.virta@gmail.com>
7
+ #
8
+ # Copyright:: (C) 2009 Raine Virta
9
+ #
10
+ # License:: GPL v2
11
+
12
+ require 'rexml/document'
13
+ require 'cgi'
14
+
15
+ module ::Spotify
16
+ class SpotifyObject
17
+ def initialize(xml)
18
+ @spotify_id = xml.attributes["href"]
19
+ end
20
+
21
+ def url
22
+ id = @spotify_id[@spotify_id.rindex(':')+1..-1]
23
+ method = self.class.to_s.split('::').last.downcase
24
+ return "http://open.spotify.com/#{method}/#{id}"
25
+ end
26
+ end
27
+
28
+ class Album < SpotifyObject
29
+ attr_reader :name, :released, :artist
30
+
31
+ def initialize(xml)
32
+ super
33
+ @name = xml.elements["name"].text
34
+ if e = xml.elements["artist"]
35
+ @artist = Artist.new(xml.elements["artist"])
36
+ end
37
+ if e = xml.elements["released"]
38
+ @released = e.text.to_i
39
+ end
40
+ end
41
+ end
42
+
43
+ class Artist < SpotifyObject
44
+ attr_reader :name
45
+
46
+ def initialize(xml)
47
+ super
48
+ @name = xml.elements["name"].text
49
+ end
50
+ end
51
+
52
+ class Track < SpotifyObject
53
+ attr_reader :name, :artist, :album, :track_number
54
+
55
+ def initialize(xml)
56
+ super
57
+ @name = xml.elements["name"].text
58
+ @artist = Artist.new(xml.elements["artist"])
59
+ @album = Album.new(xml.elements["album"])
60
+ @track_number = xml.elements["track-number"].text.to_i
61
+ @length = xml.elements["length"].text.to_f
62
+ end
63
+
64
+ def to_s
65
+ str = "#{artist.name} – #{name} [#{album.name}"
66
+ str << ", #{album.released}" if album.released
67
+ str << "]"
68
+ end
69
+ end
70
+
71
+ def self.get(service, method, query, page=1)
72
+ query.tr!('-','')
73
+ url = "http://ws.spotify.com/#{service}/1/#{method}?q=#{CGI.escape(query)}&page=#{page}"
74
+ xml = Irc::Utils.bot.httputil.get(url)
75
+ raise unless xml
76
+ return REXML::Document.new(xml).root
77
+ end
78
+
79
+ def self.search(method, query, page=1)
80
+ doc = get(:search, method, query, page)
81
+ return nil if doc.elements["opensearch:totalResults"].text.to_i.zero?
82
+ return Spotify.const_get(method.to_s.capitalize).new(doc.elements[method.to_s])
83
+ end
84
+ end
@@ -31,7 +31,7 @@ class LinkBot < Plugin
31
31
  :desc => "List of regexp which match linkbot messages; each regexp needs to have three captures, which in order are the nickname of the original speaker, network, and original message",
32
32
  :on_change => proc {|bot, v| bot.plugins['linkbot'].update_patterns})
33
33
  # TODO use template strings instead of regexp for user friendliness
34
-
34
+
35
35
  # Initialize the plugin
36
36
  def initialize
37
37
  super
@@ -20,10 +20,190 @@ class MarkovPlugin < Plugin
20
20
  Config.register Config::ArrayValue.new('markov.ignore',
21
21
  :default => [],
22
22
  :desc => "Hostmasks and channel names markov should NOT learn from (e.g. idiot*!*@*, #privchan).")
23
+ Config.register Config::ArrayValue.new('markov.readonly',
24
+ :default => [],
25
+ :desc => "Hostmasks and channel names markov should NOT talk to (e.g. idiot*!*@*, #privchan).")
23
26
  Config.register Config::IntegerValue.new('markov.max_words',
24
27
  :default => 50,
25
28
  :validate => Proc.new { |v| (0..100).include? v },
26
29
  :desc => "Maximum number of words the bot should put in a sentence")
30
+ Config.register Config::FloatValue.new('markov.learn_delay',
31
+ :default => 0.5,
32
+ :validate => Proc.new { |v| v >= 0 },
33
+ :desc => "Time the learning thread spends sleeping after learning a line. If set to zero, learning from files can be very CPU intensive, but also faster.")
34
+ Config.register Config::IntegerValue.new('markov.delay',
35
+ :default => 5,
36
+ :validate => Proc.new { |v| v >= 0 },
37
+ :desc => "Wait short time before contributing to conversation.")
38
+ Config.register Config::IntegerValue.new('markov.answer_addressed',
39
+ :default => 50,
40
+ :validate => Proc.new { |v| (0..100).include? v },
41
+ :desc => "Probability of answer when addressed by nick")
42
+ Config.register Config::ArrayValue.new('markov.ignore_patterns',
43
+ :default => [],
44
+ :desc => "Ignore these word patterns")
45
+
46
+ MARKER = :"\r\n"
47
+
48
+ # upgrade a registry entry from 0.9.14 and earlier, converting the Arrays
49
+ # into Hashes of weights
50
+ def upgrade_entry(k, logfile)
51
+ logfile.puts "\t#{k.inspect}"
52
+ logfile.flush
53
+ logfile.fsync
54
+
55
+ ar = @registry[k]
56
+
57
+ # wipe the current key
58
+ @registry.delete(k)
59
+
60
+ # discard empty keys
61
+ if ar.empty?
62
+ logfile.puts "\tEMPTY"
63
+ return
64
+ end
65
+
66
+ # otherwise, proceed
67
+ logfile.puts "\t#{ar.inspect}"
68
+
69
+ # re-encode key to UTF-8 and cleanup as needed
70
+ words = k.split.map do |w|
71
+ BasicUserMessage.strip_formatting(
72
+ @bot.socket.filter.in(w)
73
+ ).sub(/\001$/,'')
74
+ end
75
+
76
+ # old import that failed to split properly?
77
+ if words.length == 1 and words.first.include? '/'
78
+ # split at the last /
79
+ unsplit = words.first
80
+ at = unsplit.rindex('/')
81
+ words = [unsplit[0,at], unsplit[at+1..-1]]
82
+ end
83
+
84
+ # if any of the re-split/re-encoded words have spaces,
85
+ # or are empty, we would get a chain we can't convert,
86
+ # so drop it
87
+ if words.first.empty? or words.first.include?(' ') or
88
+ words.last.empty? or words.last.include?(' ')
89
+ logfile.puts "\tSKIPPED"
90
+ return
91
+ end
92
+
93
+ # former unclean CTCP, we can't convert this
94
+ if words.first[0] == 1
95
+ logfile.puts "\tSKIPPED"
96
+ return
97
+ end
98
+
99
+ # nonword CTCP => SKIP
100
+ # someword CTCP => nonword someword
101
+ if words.last[0] == 1
102
+ if words.first == "nonword"
103
+ logfile.puts "\tSKIPPED"
104
+ return
105
+ end
106
+ words.unshift MARKER
107
+ words.pop
108
+ end
109
+
110
+ # intern the old keys
111
+ words.map! do |w|
112
+ ['nonword', MARKER].include?(w) ? MARKER : w.chomp("\001")
113
+ end
114
+
115
+ newkey = words.join(' ')
116
+ logfile.puts "\t#{newkey.inspect}"
117
+
118
+ # the new key exists already, so we want to merge
119
+ if k != newkey and @registry.key? newkey
120
+ ar2 = @registry[newkey]
121
+ logfile.puts "\tMERGE"
122
+ logfile.puts "\t\t#{ar2.inspect}"
123
+ ar.push(*ar2)
124
+ # and get rid of the key
125
+ @registry.delete(newkey)
126
+ end
127
+
128
+ total = 0
129
+ hash = Hash.new(0)
130
+
131
+ @chains_mutex.synchronize do
132
+ if @chains.key? newkey
133
+ ar2 = @chains[newkey]
134
+ total += ar2.first
135
+ hash.update ar2.last
136
+ end
137
+
138
+ ar.each do |word|
139
+ case word
140
+ when :nonword
141
+ # former marker
142
+ sym = MARKER
143
+ else
144
+ # we convert old words into UTF-8, cleanup, resplit if needed,
145
+ # and only get the first word. we may lose some data for old
146
+ # missplits, but this is the best we can do
147
+ w = BasicUserMessage.strip_formatting(
148
+ @bot.socket.filter.in(word).split.first
149
+ )
150
+ case w
151
+ when /^\001\S+$/, "\001", ""
152
+ # former unclean CTCP or end of CTCP
153
+ next
154
+ else
155
+ # intern after clearing leftover end-of-actions if present
156
+ sym = w.chomp("\001")
157
+ end
158
+ end
159
+ hash[sym] += 1
160
+ total += 1
161
+ end
162
+ if hash.empty?
163
+ logfile.puts "\tSKIPPED"
164
+ return
165
+ end
166
+ logfile.puts "\t#{[total, hash].inspect}"
167
+ @chains[newkey] = [total, hash]
168
+ end
169
+ end
170
+
171
+ def upgrade_registry
172
+ # we load all the keys and then iterate over this array because
173
+ # running each() on the registry and updating it at the same time
174
+ # doesn't work
175
+ keys = @registry.keys
176
+ # no registry, nothing to do
177
+ return if keys.empty?
178
+
179
+ ki = 0
180
+ log "starting markov database conversion thread (v1 to v2, #{keys.length} keys)"
181
+
182
+ keys.each { |k| @upgrade_queue.push k }
183
+ @upgrade_queue.push nil
184
+
185
+ @upgrade_thread = Thread.new do
186
+ logfile = File.open(@bot.path('markov-conversion.log'), 'a')
187
+ logfile.puts "=== conversion thread started #{Time.now} ==="
188
+ while k = @upgrade_queue.pop
189
+ ki += 1
190
+ logfile.puts "Key #{ki} (#{@upgrade_queue.length} in queue):"
191
+ begin
192
+ upgrade_entry(k, logfile)
193
+ rescue Exception => e
194
+ logfile.puts "=== ERROR ==="
195
+ logfile.puts e.pretty_inspect
196
+ logfile.puts "=== EREND ==="
197
+ end
198
+ sleep @bot.config['markov.learn_delay'] unless @bot.config['markov.learn_delay'].zero?
199
+ end
200
+ logfile.puts "=== conversion thread stopped #{Time.now} ==="
201
+ logfile.close
202
+ end
203
+ @upgrade_thread.priority = -1
204
+ end
205
+
206
+ attr_accessor :chains
27
207
 
28
208
  def initialize
29
209
  super
@@ -41,54 +221,156 @@ class MarkovPlugin < Plugin
41
221
  @bot.config['markov.ignore'] = @bot.config['markov.ignore_users'].dup
42
222
  @bot.config.delete('markov.ignore_users'.to_sym)
43
223
  end
224
+
225
+ @chains = @registry.sub_registry('v2')
226
+ @chains.set_default([])
227
+ @rchains = @registry.sub_registry('v2r')
228
+ @rchains.set_default([])
229
+
230
+ @upgrade_queue = Queue.new
231
+ @upgrade_thread = nil
232
+ upgrade_registry
233
+
44
234
  @learning_queue = Queue.new
45
235
  @learning_thread = Thread.new do
46
236
  while s = @learning_queue.pop
47
- learn s
48
- sleep 0.5
237
+ learn_line s
238
+ sleep @bot.config['markov.learn_delay'] unless @bot.config['markov.learn_delay'].zero?
49
239
  end
50
240
  end
51
241
  @learning_thread.priority = -1
52
242
  end
53
243
 
54
244
  def cleanup
245
+ if @upgrade_thread and @upgrade_thread.alive?
246
+ debug 'closing conversion thread'
247
+ @upgrade_queue.clear
248
+ @upgrade_queue.push nil
249
+ @upgrade_thread.join
250
+ debug 'conversion thread closed'
251
+ end
252
+
55
253
  debug 'closing learning thread'
254
+ @learning_queue.clear
56
255
  @learning_queue.push nil
57
256
  @learning_thread.join
58
257
  debug 'learning thread closed'
258
+ @chains.close
259
+ @rchains.close
260
+ super
261
+ end
262
+
263
+ # pick a word from the registry using the pair as key.
264
+ def pick_word(word1, word2=MARKER, chainz=@chains)
265
+ k = "#{word1} #{word2}"
266
+ return MARKER unless chainz.key? k
267
+ wordlist = chainz[k]
268
+ pick_word_from_list wordlist
269
+ end
270
+
271
+ # pick a word from weighted hash
272
+ def pick_word_from_list(wordlist)
273
+ total = wordlist.first
274
+ hash = wordlist.last
275
+ return MARKER if total == 0
276
+ return hash.keys.first if hash.length == 1
277
+ hit = rand(total)
278
+ ret = MARKER
279
+ hash.each do |k, w|
280
+ hit -= w
281
+ if hit < 0
282
+ ret = k
283
+ break
284
+ end
285
+ end
286
+ return ret
59
287
  end
60
288
 
61
289
  def generate_string(word1, word2)
62
290
  # limit to max of markov.max_words words
63
- output = word1 + " " + word2
64
-
65
- # try to avoid :nonword in the first iteration
66
- wordlist = @registry["#{word1} #{word2}"]
67
- wordlist.delete(:nonword)
68
- if not wordlist.empty?
69
- word3 = wordlist[rand(wordlist.length)]
70
- output = output + " " + word3
71
- word1, word2 = word2, word3
291
+ if word2
292
+ output = [word1, word2]
293
+ else
294
+ output = word1
295
+ keys = []
296
+ @chains.each_key(output) do |key|
297
+ if key.downcase.include? output
298
+ keys << key
299
+ else
300
+ break
301
+ end
302
+ end
303
+ return nil if keys.empty?
304
+ output = keys[rand(keys.size)].split(/ /)
72
305
  end
73
-
74
- (@bot.config['markov.max_words'] - 1).times do
75
- wordlist = @registry["#{word1} #{word2}"]
76
- break if wordlist.empty?
77
- word3 = wordlist[rand(wordlist.length)]
78
- break if word3 == :nonword
79
- output = output + " " + word3
80
- word1, word2 = word2, word3
306
+ output = output.split(/ /) unless output.is_a? Array
307
+ input = [word1, word2]
308
+ while output.length < @bot.config['markov.max_words'] and (output.first != MARKER or output.last != MARKER) do
309
+ if output.last != MARKER
310
+ output << pick_word(output[-2], output[-1])
311
+ end
312
+ if output.first != MARKER
313
+ output.insert 0, pick_word(output[0], output[1], @rchains)
314
+ end
315
+ end
316
+ output.delete MARKER
317
+ if output == input
318
+ nil
319
+ else
320
+ output.join(" ")
81
321
  end
82
- return output
83
322
  end
84
323
 
85
324
  def help(plugin, topic="")
86
- "markov plugin: listens to chat to build a markov chain, with which it can (perhaps) attempt to (inanely) contribute to 'discussion'. Sort of.. Will get a *lot* better after listening to a lot of chat. usage: 'markov' to attempt to say something relevant to the last line of chat, if it can. other options to markov: 'ignore' => ignore a hostmask (accept no input), 'status' => show current status, 'probability [<chance>]' => set the % chance of rbot responding to input, or display the current probability, 'chat' => try and say something intelligent, 'chat about <foo> <bar>' => riff on a word pair (if possible)"
325
+ topic, subtopic = topic.split
326
+
327
+ case topic
328
+ when "delay"
329
+ "markov delay <value> => Set message delay"
330
+ when "ignore"
331
+ case subtopic
332
+ when "add"
333
+ "markov ignore add <hostmask|channel> => ignore a hostmask or a channel"
334
+ when "list"
335
+ "markov ignore list => show ignored hostmasks and channels"
336
+ when "remove"
337
+ "markov ignore remove <hostmask|channel> => unignore a hostmask or channel"
338
+ else
339
+ "ignore hostmasks or channels -- topics: add, remove, list"
340
+ end
341
+ when "readonly"
342
+ case subtopic
343
+ when "add"
344
+ "markov readonly add <hostmask|channel> => read-only a hostmask or a channel"
345
+ when "list"
346
+ "markov readonly list => show read-only hostmasks and channels"
347
+ when "remove"
348
+ "markov readonly remove <hostmask|channel> => unreadonly a hostmask or channel"
349
+ else
350
+ "restrict hostmasks or channels to read only -- topics: add, remove, list"
351
+ end
352
+ when "status"
353
+ "markov status => show if markov is enabled, probability and amount of messages in queue for learning"
354
+ when "probability"
355
+ "markov probability [<percent>] => set the % chance of rbot responding to input, or display the current probability"
356
+ when "chat"
357
+ case subtopic
358
+ when "about"
359
+ "markov chat about <word> [<another word>] => talk about <word> or riff on a word pair (if possible)"
360
+ else
361
+ "markov chat => try to say something intelligent"
362
+ end
363
+ else
364
+ "markov plugin: listens to chat to build a markov chain, with which it can (perhaps) attempt to (inanely) contribute to 'discussion'. Sort of.. Will get a *lot* better after listening to a lot of chat. Usage: 'chat' to attempt to say something relevant to the last line of chat, if it can -- help topics: ignore, readonly, delay, status, probability, chat, chat about"
365
+ end
87
366
  end
88
367
 
89
- def clean_str(s)
90
- str = s.dup
91
- str.gsub!(/^\S+[:,;]/, "")
368
+ def clean_message(m)
369
+ str = m.plainmessage.dup
370
+ str =~ /^(\S+)([:,;])/
371
+ if $1 and m.target.is_a? Irc::Channel and m.target.user_nicks.include? $1.downcase
372
+ str.gsub!(/^(\S+)([:,;])\s+/, "")
373
+ end
92
374
  str.gsub!(/\s{2,}/, ' ') # fix for two or more spaces
93
375
  return str.strip
94
376
  end
@@ -99,15 +381,21 @@ class MarkovPlugin < Plugin
99
381
 
100
382
  def status(m,params)
101
383
  if @bot.config['markov.enabled']
102
- m.reply "markov is currently enabled, #{probability?}% chance of chipping in"
384
+ reply = _("markov is currently enabled, %{p}% chance of chipping in") % { :p => probability? }
385
+ l = @learning_queue.length
386
+ reply << (_(", %{l} messages in queue") % {:l => l}) if l > 0
387
+ l = @upgrade_queue.length
388
+ reply << (_(", %{l} chains to upgrade") % {:l => l}) if l > 0
103
389
  else
104
- m.reply "markov is currently disabled"
390
+ reply = _("markov is currently disabled")
105
391
  end
392
+ m.reply reply
106
393
  end
107
394
 
108
395
  def ignore?(m=nil)
109
396
  return false unless m
110
- return true if m.address? or m.private?
397
+ return true if m.private?
398
+ return true if m.prefixed?
111
399
  @bot.config['markov.ignore'].each do |mask|
112
400
  return true if m.channel.downcase == mask.downcase
113
401
  return true if m.source.matches?(mask)
@@ -115,34 +403,74 @@ class MarkovPlugin < Plugin
115
403
  return false
116
404
  end
117
405
 
406
+ def readonly?(m=nil)
407
+ return false unless m
408
+ @bot.config['markov.readonly'].each do |mask|
409
+ return true if m.channel.downcase == mask.downcase
410
+ return true if m.source.matches?(mask)
411
+ end
412
+ return false
413
+ end
414
+
118
415
  def ignore(m, params)
119
416
  action = params[:action]
120
417
  user = params[:option]
121
418
  case action
122
- when 'remove':
419
+ when 'remove'
123
420
  if @bot.config['markov.ignore'].include? user
124
421
  s = @bot.config['markov.ignore']
125
422
  s.delete user
126
423
  @bot.config['ignore'] = s
127
- m.reply "#{user} removed"
424
+ m.reply _("%{u} removed") % { :u => user }
128
425
  else
129
- m.reply "not found in list"
426
+ m.reply _("not found in list")
130
427
  end
131
- when 'add':
428
+ when 'add'
132
429
  if user
133
430
  if @bot.config['markov.ignore'].include?(user)
134
- m.reply "#{user} already in list"
431
+ m.reply _("%{u} already in list") % { :u => user }
135
432
  else
136
433
  @bot.config['markov.ignore'] = @bot.config['markov.ignore'].push user
137
- m.reply "#{user} added to markov ignore list"
434
+ m.reply _("%{u} added to markov ignore list") % { :u => user }
138
435
  end
139
436
  else
140
- m.reply "give the name of a person or channel to ignore"
437
+ m.reply _("give the name of a person or channel to ignore")
141
438
  end
142
- when 'list':
143
- m.reply "I'm ignoring #{@bot.config['markov.ignore'].join(", ")}"
439
+ when 'list'
440
+ m.reply _("I'm ignoring %{ignored}") % { :ignored => @bot.config['markov.ignore'].join(", ") }
144
441
  else
145
- m.reply "have markov ignore the input from a hostmask or a channel. usage: markov ignore add <mask or channel>; markov ignore remove <mask or channel>; markov ignore list"
442
+ m.reply _("have markov ignore the input from a hostmask or a channel. usage: markov ignore add <mask or channel>; markov ignore remove <mask or channel>; markov ignore list")
443
+ end
444
+ end
445
+
446
+ def readonly(m, params)
447
+ action = params[:action]
448
+ user = params[:option]
449
+ case action
450
+ when 'remove'
451
+ if @bot.config['markov.readonly'].include? user
452
+ s = @bot.config['markov.readonly']
453
+ s.delete user
454
+ @bot.config['markov.readonly'] = s
455
+ m.reply _("%{u} removed") % { :u => user }
456
+ else
457
+ m.reply _("not found in list")
458
+ end
459
+ when 'add'
460
+ if user
461
+ if @bot.config['markov.readonly'].include?(user)
462
+ m.reply _("%{u} already in list") % { :u => user }
463
+ else
464
+ @bot.config['markov.readonly'] = @bot.config['markov.readonly'].push user
465
+ m.reply _("%{u} added to markov readonly list") % { :u => user }
466
+ end
467
+ else
468
+ m.reply _("give the name of a person or channel to read only")
469
+ end
470
+ when 'list'
471
+ m.reply _("I'm only reading %{readonly}") % { :readonly => @bot.config['markov.readonly'].join(", ") }
472
+ else
473
+ m.reply _("have markov not answer to input from a hostmask or a channel. usage: markov readonly add <mask or channel>; markov readonly remove <mask or channel>; markov readonly list")
146
474
  end
147
475
  end
148
476
 
@@ -165,97 +493,258 @@ class MarkovPlugin < Plugin
165
493
  m.okay
166
494
  end
167
495
 
168
- def should_talk
496
+ def should_talk(m)
169
497
  return false unless @bot.config['markov.enabled']
170
- prob = probability?
498
+ prob = m.address? ? @bot.config['markov.answer_addressed'] : probability?
171
499
  return true if prob > rand(100)
172
500
  return false
173
501
  end
174
502
 
175
- def delay
176
- 1 + rand(5)
503
+ # Generates all sequence pairs from array
504
+ # seq_pairs [1,2,3,4] == [ [1,2], [2,3], [3,4]]
505
+ def seq_pairs(arr)
506
+ res = []
507
+ 0.upto(arr.size-2) do |i|
508
+ res << [arr[i], arr[i+1]]
509
+ end
510
+ res
511
+ end
512
+
513
+ def set_delay(m, params)
514
+ if params[:delay] == "off"
515
+ @bot.config["markov.delay"] = 0
516
+ m.okay
517
+ elsif !params[:delay]
518
+ m.reply _("Message delay is %{delay}" % { :delay => @bot.config["markov.delay"]})
519
+ else
520
+ @bot.config["markov.delay"] = params[:delay].to_i
521
+ m.okay
522
+ end
523
+ end
524
+
525
+ def reply_delay(m, line)
526
+ m.replied = true
527
+ if @bot.config['markov.delay'] > 0
528
+ @bot.timer.add_once(1 + rand(@bot.config['markov.delay'])) {
529
+ m.reply line, :nick => false, :to => :public
530
+ }
531
+ else
532
+ m.reply line, :nick => false, :to => :public
533
+ end
177
534
  end
178
535
 
179
536
  def random_markov(m, message)
180
- return unless should_talk
181
-
182
- word1, word2 = message.split(/\s+/)
183
- return unless word1 and word2
184
- line = generate_string(word1, word2)
185
- return unless line
186
- # we do nothing if the line we return is just an initial substring
187
- # of the line we received
188
- return if message.index(line) == 0
189
- @bot.timer.add_once(delay) {
190
- m.plainreply line
191
- }
537
+ return unless should_talk(m)
538
+
539
+ words = clean_message(m).split(/\s+/)
540
+ if words.length < 2
541
+ line = generate_string words.first, nil
542
+
543
+ if line and message.index(line) != 0
544
+ reply_delay m, line
545
+ return
546
+ end
547
+ else
548
+ pairs = seq_pairs(words).sort_by { rand }
549
+ pairs.each do |word1, word2|
550
+ line = generate_string(word1, word2)
551
+ if line and message.index(line) != 0
552
+ reply_delay m, line
553
+ return
554
+ end
555
+ end
556
+ words.sort_by { rand }.each do |word|
557
+ line = generate_string word.first, nil
558
+ if line and message.index(line) != 0
559
+ reply_delay m, line
560
+ return
561
+ end
562
+ end
563
+ end
192
564
  end
193
565
 
194
566
  def chat(m, params)
195
567
  line = generate_string(params[:seed1], params[:seed2])
196
- if line != "#{params[:seed1]} #{params[:seed2]}"
197
- m.reply line
568
+ if line and line != [params[:seed1], params[:seed2]].compact.join(" ")
569
+ m.reply line
198
570
  else
199
- m.reply "I can't :("
571
+ m.reply _("I can't :(")
200
572
  end
201
573
  end
202
574
 
203
575
  def rand_chat(m, params)
204
576
  # pick a random pair from the db and go from there
205
- word1, word2 = :nonword, :nonword
577
+ word1, word2 = MARKER, MARKER
206
578
  output = Array.new
207
- 50.times do
208
- wordlist = @registry["#{word1} #{word2}"]
209
- break if wordlist.empty?
210
- word3 = wordlist[rand(wordlist.length)]
211
- break if word3 == :nonword
579
+ @bot.config['markov.max_words'].times do
580
+ word3 = pick_word(word1, word2)
581
+ break if word3 == MARKER
212
582
  output << word3
213
583
  word1, word2 = word2, word3
214
584
  end
215
585
  if output.length > 1
216
586
  m.reply output.join(" ")
217
587
  else
218
- m.reply "I can't :("
588
+ m.reply _("I can't :(")
219
589
  end
220
590
  end
221
-
222
- def message(m)
591
+
592
+ def learn(*lines)
593
+ lines.each { |l| @learning_queue.push l }
594
+ end
595
+
596
+ def unreplied(m)
223
597
  return if ignore? m
224
598
 
225
599
  # in channel message, the kind we are interested in
226
- message = clean_str m.plainmessage
600
+ message = m.plainmessage
227
601
 
228
602
  if m.action?
229
603
  message = "#{m.sourcenick} #{message}"
230
604
  end
231
-
232
- @learning_queue.push message
233
- random_markov(m, message) unless m.replied?
605
+
606
+ random_markov(m, message) unless readonly? m or m.replied?
607
+ learn clean_message(m)
234
608
  end
235
609
 
236
- def learn(message)
237
- # debug "learning #{message}"
238
- wordlist = message.split(/\s+/)
610
+
611
+ def learn_triplet(word1, word2, word3)
612
+ k = "#{word1} #{word2}"
613
+ rk = "#{word2} #{word3}"
614
+ total = 0
615
+ hash = Hash.new(0)
616
+ if @chains.key? k
617
+ t2, h2 = @chains[k]
618
+ total += t2
619
+ hash.update h2
620
+ end
621
+ hash[word3] += 1
622
+ total += 1
623
+ @chains[k] = [total, hash]
624
+ # Reverse
625
+ total = 0
626
+ hash = Hash.new(0)
627
+ if @rchains.key? rk
628
+ t2, h2 = @rchains[rk]
629
+ total += t2
630
+ hash.update h2
631
+ end
632
+ hash[word1] += 1
633
+ total += 1
634
+ @rchains[rk] = [total, hash]
635
+ end
636
+
637
+
638
+ def learn_line(message)
639
+ # debug "learning #{message.inspect}"
640
+ wordlist = message.split(/\s+/).reject do |w|
641
+ @bot.config['markov.ignore_patterns'].map do |pat|
642
+ w =~ Regexp.new(pat.to_s)
643
+ end.select{|v| v}.size != 0
644
+ end
239
645
  return unless wordlist.length >= 2
240
- word1, word2 = :nonword, :nonword
646
+ word1, word2 = MARKER, MARKER
647
+ wordlist << MARKER
241
648
  wordlist.each do |word3|
242
- k = "#{word1} #{word2}"
243
- @registry[k] = @registry[k].push(word3)
649
+ learn_triplet(word1, word2, word3.to_sym)
244
650
  word1, word2 = word2, word3
245
651
  end
246
- k = "#{word1} #{word2}"
247
- @registry[k] = @registry[k].push(:nonword)
248
652
  end
653
+
654
+ # TODO allow learning from URLs
655
+ def learn_from(m, params)
656
+ begin
657
+ path = params[:file]
658
+ file = File.open(path, "r")
659
+ pattern = params[:pattern].empty? ? nil : Regexp.new(params[:pattern].to_s)
660
+ rescue Errno::ENOENT
661
+ m.reply _("no such file")
662
+ return
663
+ end
664
+
665
+ if file.eof?
666
+ m.reply _("the file is empty!")
667
+ return
668
+ end
669
+
670
+ if params[:testing]
671
+ lines = []
672
+ range = case params[:lines]
673
+ when /^\d+\.\.\d+$/
674
+ Range.new(*params[:lines].split("..").map { |e| e.to_i })
675
+ when /^\d+$/
676
+ Range.new(1, params[:lines].to_i)
677
+ else
678
+ Range.new(1, [@bot.config['send.max_lines'], 3].max)
679
+ end
680
+
681
+ file.each do |line|
682
+ next unless file.lineno >= range.begin
683
+ lines << line.chomp
684
+ break if file.lineno == range.end
685
+ end
686
+
687
+ lines = lines.map do |l|
688
+ pattern ? l.scan(pattern).to_s : l
689
+ end.reject { |e| e.empty? }
690
+
691
+ if pattern
692
+ unless lines.empty?
693
+ m.reply _("example matches for that pattern at lines %{range} include: %{lines}") % {
694
+ :lines => lines.map { |e| Underline+e+Underline }.join(", "),
695
+ :range => range.to_s
696
+ }
697
+ else
698
+ m.reply _("the pattern doesn't match anything at lines %{range}") % {
699
+ :range => range.to_s
700
+ }
701
+ end
702
+ else
703
+ m.reply _("learning from the file without a pattern would learn, for example: ")
704
+ lines.each { |l| m.reply l }
705
+ end
706
+
707
+ return
708
+ end
709
+
710
+ if pattern
711
+ file.each { |l| learn(l.scan(pattern).to_s) }
712
+ else
713
+ file.each { |l| learn(l.chomp) }
714
+ end
715
+
716
+ m.okay
717
+ end
718
+
719
+ def stats(m, params)
720
+ m.reply "Markov status: chains: #{@chains.length} forward, #{@rchains.length} reverse, queued phrases: #{@learning_queue.size}"
721
+ end
722
+
249
723
  end
250
724
 
251
725
  plugin = MarkovPlugin.new
726
+ plugin.map 'markov delay :delay', :action => "set_delay"
727
+ plugin.map 'markov delay', :action => "set_delay"
252
728
  plugin.map 'markov ignore :action :option', :action => "ignore"
253
729
  plugin.map 'markov ignore :action', :action => "ignore"
254
730
  plugin.map 'markov ignore', :action => "ignore"
731
+ plugin.map 'markov readonly :action :option', :action => "readonly"
732
+ plugin.map 'markov readonly :action', :action => "readonly"
733
+ plugin.map 'markov readonly', :action => "readonly"
255
734
  plugin.map 'markov enable', :action => "enable"
256
735
  plugin.map 'markov disable', :action => "disable"
257
736
  plugin.map 'markov status', :action => "status"
258
- plugin.map 'chat about :seed1 :seed2', :action => "chat"
737
+ plugin.map 'markov stats', :action => "stats"
738
+ plugin.map 'chat about :seed1 [:seed2]', :action => "chat"
259
739
  plugin.map 'chat', :action => "rand_chat"
260
740
  plugin.map 'markov probability [:probability]', :action => "probability",
261
741
  :requirements => {:probability => /^\d+%?$/}
742
+ plugin.map 'markov learn from :file [:testing [:lines lines]] [using pattern *pattern]', :action => "learn_from", :thread => true,
743
+ :requirements => {
744
+ :testing => /^testing$/,
745
+ :lines => /^(?:\d+\.\.\d+|\d+)$/ }
746
+
747
+ plugin.default_auth('ignore', false)
748
+ plugin.default_auth('probability', false)
749
+ plugin.default_auth('learn', false)
750
+