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
@@ -116,9 +116,10 @@ class Keywords < Plugin
116
116
  scan
117
117
 
118
118
  # import old format keywords into DBHash
119
- if(File.exist?("#{@bot.botclass}/keywords.rbot"))
119
+ olds = @bot.path 'keywords.rbot'
120
+ if File.exist? olds
120
121
  log "auto importing old keywords.rbot"
121
- IO.foreach("#{@bot.botclass}/keywords.rbot") do |line|
122
+ IO.foreach(olds) do |line|
122
123
  if(line =~ /^(.*?)\s*<=(is|are)?=?>\s*(.*)$/)
123
124
  lhs = $1
124
125
  mhs = $2
@@ -129,7 +130,7 @@ class Keywords < Plugin
129
130
  @keywords[lhs] = Keyword.new(mhs, values).dump
130
131
  end
131
132
  end
132
- File.rename("#{@bot.botclass}/keywords.rbot", "#{@bot.botclass}/keywords.rbot.old")
133
+ File.rename(olds, olds + ".old")
133
134
  end
134
135
  end
135
136
 
@@ -137,15 +138,12 @@ class Keywords < Plugin
137
138
  # have been added
138
139
  def scan
139
140
  # first scan for old DBHash files, and convert them
140
- Dir["#{@bot.botclass}/keywords/*"].each {|f|
141
+ Dir[datafile('*')].each {|f|
141
142
  next unless f =~ /\.db$/
142
143
  log "upgrading keyword db #{f} (rbot 0.9.5 or prior) database format"
143
144
  newname = f.gsub(/\.db$/, ".kdb")
144
- old = BDB::Hash.open f, nil,
145
- "r+", 0600
146
- new = BDB::CIBtree.open(newname, nil,
147
- BDB::CREATE | BDB::EXCL,
148
- 0600)
145
+ old = BDB::Hash.open f, nil, "r+", 0600
146
+ new = BDB::CIBtree.open(newname, nil, BDB::CREATE | BDB::EXCL, 0600)
149
147
  old.each {|k,v|
150
148
  new[k] = v
151
149
  }
@@ -155,7 +153,7 @@ class Keywords < Plugin
155
153
  }
156
154
 
157
155
  # then scan for current DBTree files, and load them
158
- Dir["#{@bot.botclass}/keywords/*"].each {|f|
156
+ Dir[@bot.path('keywords', '*')].each {|f|
159
157
  next unless f =~ /\.kdb$/
160
158
  hsh = DBTree.new @bot, f, true
161
159
  key = File.basename(f).gsub(/\.kdb$/, "")
@@ -164,7 +162,7 @@ class Keywords < Plugin
164
162
  }
165
163
 
166
164
  # then scan for non DB files, and convert/import them and delete
167
- Dir["#{@bot.botclass}/keywords/*"].each {|f|
165
+ Dir[@bot.path('keywords', '*')].each {|f|
168
166
  next if f =~ /\.kdb$/
169
167
  next if f =~ /CVS$/
170
168
  log "auto converting keywords from #{f}"
@@ -192,28 +190,28 @@ class Keywords < Plugin
192
190
 
193
191
  # upgrade data files found in old rbot formats to current
194
192
  def upgrade_data
195
- if File.exist?("#{@bot.botclass}/keywords.db")
193
+ olds = @bot.path 'keywords.db'
194
+ if File.exist? olds
196
195
  log "upgrading old keywords (rbot 0.9.5 or prior) database format"
197
- old = BDB::Hash.open "#{@bot.botclass}/keywords.db", nil,
198
- "r+", 0600
196
+ old = BDB::Hash.open olds, nil, "r+", 0600
199
197
  old.each {|k,v|
200
198
  @keywords[k] = v
201
199
  }
202
200
  old.close
203
201
  @keywords.flush
204
- File.rename("#{@bot.botclass}/keywords.db", "#{@bot.botclass}/keywords.db.old")
202
+ File.rename(olds, olds + ".old")
205
203
  end
206
204
 
207
- if File.exist?("#{@bot.botclass}/keyword.db")
205
+ olds.replace(@bot.path('keyword.db'))
206
+ if File.exist? olds
208
207
  log "upgrading old keywords (rbot 0.9.9 or prior) database format"
209
- old = BDB::CIBtree.open "#{@bot.botclass}/keyword.db", nil,
210
- "r+", 0600
208
+ old = BDB::CIBtree.open olds, nil, "r+", 0600
211
209
  old.each {|k,v|
212
210
  @keywords[k] = v
213
211
  }
214
212
  old.close
215
213
  @keywords.flush
216
- File.rename("#{@bot.botclass}/keyword.db", "#{@bot.botclass}/keyword.db.old")
214
+ File.rename(olds, olds + ".old")
217
215
  end
218
216
  end
219
217
 
@@ -223,7 +221,7 @@ class Keywords < Plugin
223
221
  end
224
222
 
225
223
  def oldsave
226
- File.open("#{@bot.botclass}/keywords.rbot", "w") do |file|
224
+ File.open(@bot.path("keywords.rbot"), "w") do |file|
227
225
  @keywords.each do |key, value|
228
226
  file.puts "#{key}<=#{value.type}=>#{value.dump}"
229
227
  end
@@ -327,6 +325,12 @@ class Keywords < Plugin
327
325
  case plugin
328
326
  when /keyword/
329
327
  case topic
328
+ when 'export'
329
+ 'keyword export => exports definitions to keyword_factoids.rbot'
330
+ when 'stats'
331
+ 'keyword stats => show statistics about static facts'
332
+ when 'wipe'
333
+ 'keyword wipe <keyword> => forgets everything about a keyword'
330
334
  when 'lookup'
331
335
  'keyword [lookup] <keyword> => look up the definition for a keyword; writing "lookup" is optional'
332
336
  when 'set'
@@ -350,7 +354,7 @@ class Keywords < Plugin
350
354
  when '<topic>'
351
355
  '<topic> => respond by setting the topic to the rest of the definition'
352
356
  else
353
- 'keyword module (fact learning and regurgitation) topics: lookup, set, forget, tell, search, listen, address, <reply>, <action>, <who>, <topic>'
357
+ 'keyword module (fact learning and regurgitation) topics: lookup, set, forget, tell, search, listen, address, stats, export, wipe, <reply>, <action>, <who>, <topic>'
354
358
  end
355
359
  when "forget"
356
360
  'forget <keyword> => forget a keyword'
@@ -484,7 +488,7 @@ class Keywords < Plugin
484
488
 
485
489
  # TODO check factoids config
486
490
  # also TODO: runtime export
487
- dir = File.join(@bot.botclass,"factoids")
491
+ dir = @bot.path 'factoids'
488
492
  fname = File.join(dir,"keyword_factoids.rbot")
489
493
 
490
494
  Dir.mkdir(dir) unless FileTest.directory?(dir)
@@ -41,12 +41,12 @@ class LartPlugin < Plugin
41
41
 
42
42
  # We may be on an old installation, so on the first run read non-language-specific larts
43
43
  unless defined?(@oldlart)
44
- @oldlart = "#{@bot.botclass}/lart/larts"
45
- @oldpraise = "#{@bot.botclass}/lart/praise"
44
+ @oldlart = datafile 'larts'
45
+ @oldpraise = datafile 'praise'
46
46
  end
47
47
 
48
- @lartfile.replace "#{@bot.botclass}/lart/larts-#{lang}"
49
- @praisefile.replace "#{@bot.botclass}/lart/praises-#{lang}"
48
+ @lartfile.replace(datafile("larts-#{lang}"))
49
+ @praisefile.replace(datafile("praises-#{lang}"))
50
50
  @larts.clear
51
51
  @praises.clear
52
52
  if File.exists? @lartfile
@@ -72,8 +72,7 @@ class LartPlugin < Plugin
72
72
 
73
73
  def save
74
74
  return unless @changed
75
- Dir.mkdir("#{@bot.botclass}/lart") if not FileTest.directory? "#{@bot.botclass}/lart"
76
- # TODO implement safe saving here too
75
+ Dir.mkdir(datafile) unless FileTest.directory? datafile
77
76
  Utils.safe_save(@lartfile) { |file|
78
77
  file.puts @larts
79
78
  }
@@ -6,10 +6,12 @@
6
6
  # Author:: Jeremy Voorhis
7
7
  # Author:: Giuseppe "Oblomov" Bilotta <giuseppe.bilotta@gmail.com>
8
8
  # Author:: Casey Link <unnamedrambler@gmail.com>
9
+ # Author:: Raine Virta <rane@kapsi.fi>
9
10
  #
10
11
  # Copyright:: (C) 2005 Jeremy Voorhis
11
12
  # Copyright:: (C) 2007 Giuseppe Bilotta
12
13
  # Copyright:: (C) 2008 Casey Link
14
+ # Copyright:: (C) 2009 Raine Virta
13
15
  #
14
16
  # License:: GPL v2
15
17
 
@@ -17,6 +19,8 @@ require 'rexml/document'
17
19
  require 'cgi'
18
20
 
19
21
  class ::LastFmEvent
22
+ attr_reader :attendance, :date
23
+
20
24
  def initialize(hash)
21
25
  @url = hash[:url]
22
26
  @date = hash[:date]
@@ -36,15 +40,26 @@ class ::LastFmEvent
36
40
  end
37
41
 
38
42
  def compact_display
39
- if @attendance
40
- return "%s %s @ %s (%s attending) %s" % [@date.strftime("%a %b, %d %Y"), @artist_string, @location, @attendance, @url]
43
+ unless @attendance.zero?
44
+ return "%s %s @ %s (%s attending) %s" % [@date.strftime("%a, %b %d"), @artist_string, @location, @attendance, @url]
41
45
  end
42
- return "%s %s @ %s %s" % [@date.strftime("%a %b, %d %Y"), @artist_string, @location, @url]
46
+ return "%s %s @ %s %s" % [@date.strftime("%a, %b %d"), @artist_string, @location, @url]
43
47
  end
44
48
  alias :to_s :compact_display
45
49
 
46
50
  end
47
51
 
52
+ define_structure :LastFmVenue, :id, :city, :street, :postal, :country, :name, :url, :lat, :long
53
+ class ::Struct::LastFmVenue
54
+ def to_s
55
+ str = self.name.dup
56
+ if self.country
57
+ str << " (" << [self.city, self.country].compact.join(", ") << ")"
58
+ end
59
+ str
60
+ end
61
+ end
62
+
48
63
  class LastFmPlugin < Plugin
49
64
  include REXML
50
65
  Config.register Config::IntegerValue.new('lastfm.max_events',
@@ -53,6 +68,18 @@ class LastFmPlugin < Plugin
53
68
  Config.register Config::IntegerValue.new('lastfm.default_events',
54
69
  :default => 3, :validate => Proc.new{|v| v > 1},
55
70
  :desc => "Default number of events to display.")
71
+ Config.register Config::IntegerValue.new('lastfm.max_shouts',
72
+ :default => 5, :validate => Proc.new{|v| v > 1},
73
+ :desc => "Maximum number of user shouts to display.")
74
+ Config.register Config::IntegerValue.new('lastfm.default_shouts',
75
+ :default => 3, :validate => Proc.new{|v| v > 1},
76
+ :desc => "Default number of user shouts to display.")
77
+ Config.register Config::IntegerValue.new('lastfm.max_user_data',
78
+ :default => 25, :validate => Proc.new{|v| v > 1},
79
+ :desc => "Maximum number of user data entries (except events and shouts) to display.")
80
+ Config.register Config::IntegerValue.new('lastfm.default_user_data',
81
+ :default => 10, :validate => Proc.new{|v| v > 1},
82
+ :desc => "Default number of user data entries (except events and shouts) to display.")
56
83
 
57
84
  APIKEY = "b25b959554ed76058ac220b7b2e0a026"
58
85
  APIURL = "http://ws.audioscrobbler.com/2.0/?api_key=#{APIKEY}&"
@@ -70,9 +97,10 @@ class LastFmPlugin < Plugin
70
97
  end
71
98
 
72
99
  def help(plugin, topic="")
100
+ period = _(", where <period> can be one of: 3|6|12 months, a year")
73
101
  case (topic.intern rescue nil)
74
102
  when :event, :events
75
- _("lastfm [<num>] events in <location> => show information on events in or near <location>. lastfm [<num>] events by <artist/group> => show information on events by <artist/group>. The number of events <num> that can be displayed is optional, defaults to %{d} and cannot be higher than %{m}") % {:d => @bot.config['lastfm.default_events'], :m => @bot.config['lastfm.max_events']}
103
+ _("lastfm [<num>] events in <location> => show information on events in or near <location>. lastfm [<num>] events by <artist/group> => show information on events by <artist/group>. lastfm [<num>] events at <venue> => show information on events at specific <venue>. The number of events <num> that can be displayed is optional, defaults to %{d} and cannot be higher than %{m}. Append 'sort by <what> [in <order> order]' to sort events. Events can be sorted by attendance or date (default) in ascending or descending order.") % {:d => @bot.config['lastfm.default_events'], :m => @bot.config['lastfm.max_events']}
76
104
  when :artist
77
105
  _("lastfm artist <name> => show information on artist <name> from last.fm")
78
106
  when :album
@@ -80,31 +108,113 @@ class LastFmPlugin < Plugin
80
108
  when :track
81
109
  _("lastfm track <name> => search tracks matching <name> on last.fm")
82
110
  when :now, :np
83
- _("lastfm now [<user>] => show the now playing track from last.fm. np [<user>] does the same.")
111
+ _("lastfm now [<nick>] => show the now playing track from last.fm. np [<nick>] does the same.")
84
112
  when :set
85
- _("lastfm set nick <user> => associate your current irc nick with a last.fm user. lastfm set verb <present> <past> => set your preferred now playing verb. default \"listening\" and \"listened\".")
113
+ _("lastfm set user <user> => associate your current irc nick with a last.fm user. lastfm set verb <present>, <past> => set your preferred now playing/just played verbs. default \"is listening to\" and \"listened to\".")
86
114
  when :who
87
- _("lastfm who [<nick>] => show who <nick> is at last.fm. if <nick> is empty, show who you are at lastfm.")
115
+ _("lastfm who [<nick>] => show who <nick> is on last.fm. if <nick> is empty, show who you are on lastfm.")
88
116
  when :compare
89
- _("lastfm compare <nick1> <nick2> => show musical taste compatibility between nick1 and nick2.")
117
+ _("lastfm compare [<nick1>] <nick2> => show musical taste compatibility between nick1 (or user if omitted) and nick2")
118
+ when :shouts
119
+ _("lastfm shouts [<nick>] => show shouts to <nick>")
120
+ when :friends
121
+ _("lastfm friends [<nick>] => show <nick>'s friends")
122
+ when :neighbors, :neighbours
123
+ _("lastfm neighbors [<nick>] => show people who share similar musical taste as <nick>")
124
+ when :lovedtracks
125
+ _("lastfm loved[tracks] [<nick>] => show tracks that <nick> has loved")
126
+ when :recenttracks, :recentracks
127
+ _("lastfm recent[tracks] [<nick>] => show tracks that <nick> has recently played")
128
+ when :topalbums
129
+ _("lastfm topalbums [<nick>] [over <period>] => show <nick>'s top albums%{p}") % { :p => period }
130
+ when :topartists
131
+ _("lastfm topartists [<nick>] [over <period>] => show <nick>'s top artists%{p}") % { :p => period }
132
+ when :toptracks
133
+ _("lastfm toptracks [<nick>] [over <period>] => show <nick>'s top tracks%{p}") % { :p => period }
134
+ when :weeklyalbumchart
135
+ _("lastfm weeklyalbumchart [<nick>] => show <nick>'s weekly album chart")
136
+ when :weeklyartistchart
137
+ _("lastfm weeklyartistchart [<nick>] => show <nick>'s weekly artist chart")
138
+ when :weeklytrackchart
139
+ _("lastfm weeklyartistchart [<nick>] => show <nick>'s weekly track chart")
90
140
  else
91
- _("lastfm [<user>] => show your or <user>'s now playing track at lastfm. np [<user>] => same as 'lastfm'. other topics: events, artist, album, track, now, set, who, compare")
141
+ _("last.fm plugin - topics: events, artist, album, track, now, set, who, compare, shouts, friends, neighbors, (loved|recent)tracks, top(albums|tracks|artists), weekly(album|artist|track)chart")
142
+ end
143
+ end
144
+
145
+ # TODO allow searching by country etc.
146
+ #
147
+ # Options: name, limit
148
+ def search_venue_by(options)
149
+ params = {}
150
+ params[:venue] = CGI.escape(options[:name])
151
+ options.delete(:name)
152
+ params.merge!(options)
153
+
154
+ uri = "#{APIURL}method=venue.search&"
155
+ uri << params.to_a.map {|e| e.join("=")}.join("&")
156
+
157
+ xml = @bot.httputil.get_response(uri)
158
+ doc = Document.new xml.body
159
+ results = []
160
+
161
+ doc.root.elements.each("results/venuematches/venue") do |v|
162
+ venue = LastFmVenue.new
163
+ venue.id = v.elements["id"].text.to_i
164
+ venue.url = v.elements["url"].text
165
+ venue.lat = v.elements["location/geo:point/geo:lat"].text.to_f
166
+ venue.long = v.elements["location/geo:point/geo:long"].text.to_f
167
+ venue.name = v.elements["name"].text
168
+ venue.city = v.elements["location/city"].text
169
+ venue.street = v.elements["location/street"].text
170
+ venue.postal = v.elements["location/postalcode"].text
171
+ venue.country = v.elements["location/country"].text
172
+
173
+ results << venue
92
174
  end
175
+ results
93
176
  end
94
177
 
95
178
  def find_events(m, params)
96
179
  num = params[:num] || @bot.config['lastfm.default_events']
97
180
  num = num.to_i.clip(1, @bot.config['lastfm.max_events'])
98
181
 
99
- location = artist = nil
100
- location = params[:location].to_s if params[:location]
101
- artist = params[:who].to_s if params[:who]
182
+ sort_by = params[:sort_by] || :date
183
+ sort_order = params[:sort_order]
184
+ sort_order = sort_order.to_sym unless sort_order.nil?
102
185
 
103
- uri = nil
104
- if artist == nil
105
- uri = "#{APIURL}method=geo.getevents&location=#{CGI.escape location}"
106
- else
107
- uri = "#{APIURL}method=artist.getevents&artist=#{CGI.escape artist}"
186
+ location = params[:location]
187
+ artist = params[:who]
188
+ venue = params[:venue]
189
+ user = resolve_username(m, params[:user])
190
+
191
+ if location
192
+ uri = "#{APIURL}method=geo.getevents&location=#{CGI.escape location.to_s}"
193
+ emptymsg = _("no events found in %{location}") % {:location => location.to_s}
194
+ elsif venue
195
+ begin
196
+ venues = search_venue_by(:name => venue.to_s, :limit => 1)
197
+ rescue Exception => e
198
+ error e
199
+ m.reply _("an error occurred looking for venue %{venue}: %{e}") % {
200
+ :venue => venue.to_s,
201
+ :e => e.message
202
+ }
203
+ end
204
+
205
+ if venues.empty?
206
+ m.reply _("no venue found matching %{venue}") % {:venue => venue.to_s}
207
+ return
208
+ end
209
+ venue = venues.first
210
+ uri = "#{APIURL}method=venue.getevents&venue=#{venue.id}"
211
+ emptymsg = _("no events found at %{venue}") % {:venue => venue.to_s}
212
+ elsif artist
213
+ uri = "#{APIURL}method=artist.getevents&artist=#{CGI.escape artist.to_s}"
214
+ emptymsg = _("no events found by %{artist}") % {:artist => artist.to_s}
215
+ elsif user
216
+ uri = "#{APIURL}method=user.getevents&user=#{CGI.escape user}"
217
+ emptymsg = _("%{user} is not attending any events") % {:user => user}
108
218
  end
109
219
  xml = @bot.httputil.get_response(uri)
110
220
 
@@ -113,7 +223,7 @@ class LastFmPlugin < Plugin
113
223
  if doc.root and doc.root.attributes["status"] == "failed"
114
224
  m.reply doc.root.elements["error"].text
115
225
  else
116
- m.reply _("Could not retrieve events")
226
+ m.reply _("could not retrieve events")
117
227
  end
118
228
  end
119
229
  disp_events = Array.new
@@ -129,11 +239,7 @@ class LastFmPlugin < Plugin
129
239
  h[:date] = Time.utc(date[3].to_i, date[2], date[1].to_i)
130
240
  h[:desc] = e.elements["description"].text
131
241
  h[:url] = e.elements["url"].text
132
- e.detect {|node|
133
- if node.kind_of? Element and node.attributes["name"] == "attendance" then
134
- h[:attendance] = node.text
135
- end
136
- }
242
+ h[:attendance] = e.elements["attendance"].text.to_i
137
243
  artists = Array.new
138
244
  e.elements.each("artists/artist"){ |a|
139
245
  artists << a.text
@@ -141,47 +247,54 @@ class LastFmPlugin < Plugin
141
247
  h[:artists] = artists
142
248
  events << LastFmEvent.new(h)
143
249
  }
250
+ if events.empty?
251
+ m.reply emptymsg
252
+ return
253
+ end
254
+
255
+ # sort order when sorted by date is ascending by default
256
+ # and descending when sorted by attendance
257
+ case sort_by.to_sym
258
+ when :attendance
259
+ events = events.sort_by { |e| e.attendance }.reverse
260
+ events.reverse! if [:ascending, :asc].include? sort_order
261
+ when :date
262
+ events = events.sort_by { |e| e.date }
263
+ events.reverse! if [:descending, :desc].include? sort_order
264
+ end
265
+
144
266
  events[0...num].each { |event|
145
267
  disp_events << event.to_s
146
268
  }
147
269
  m.reply disp_events.join(' | '), :split_at => /\s+\|\s+/
148
270
 
149
- end
271
+ end
150
272
 
151
273
  def tasteometer(m, params)
152
274
  opts = { :cache => false }
153
- user1 = params[:user1].to_s
154
- user2 = params[:user2].to_s
275
+ user1 = resolve_username(m, params[:user1])
276
+ user2 = resolve_username(m, params[:user2])
155
277
  xml = @bot.httputil.get_response("#{APIURL}method=tasteometer.compare&type1=user&type2=user&value1=#{CGI.escape user1}&value2=#{CGI.escape user2}", opts)
156
278
  doc = Document.new xml.body
157
279
  unless doc
158
280
  m.reply _("last.fm parsing failed")
159
281
  return
160
282
  end
161
- if xml.class == Net::HTTPInternalServerError
162
- if doc.root.elements["error"].attributes["code"] == "7" then
283
+ if xml.class == Net::HTTPBadRequest
284
+ if doc.root.elements["error"].attributes["code"] == "7" then
163
285
  error = doc.root.elements["error"].text
164
286
  error.match(/Invalid username: \[(.*)\]/);
165
- if @registry.has_key? $1 and not params[:recurs]
166
- if user1 == $1
167
- params[:user1] = @registry[ $1 ]
168
- elsif user2 == $1
169
- params[:user2] = @registry[ $1 ]
170
- end
171
- params[:recurs] = true
172
- tasteometer(m, params)
173
- else
174
- m.reply _("%{u} doesn't exist at last.fm. Perhaps you need to: lastfm set <username>") % {:u => baduser}
175
- return
176
- end
287
+ baduser = $1
288
+
289
+ m.reply _("%{u} doesn't exist on last.fm") % {:u => baduser}
290
+ return
177
291
  else
178
- m.reply _("Bad: %{e}") % {:e => doc.root.element["error"].text}
292
+ m.reply _("error: %{e}") % {:e => doc.root.element["error"].text}
179
293
  return
180
294
  end
181
295
  end
182
- now = artist = track = albumtxt = date = nil
183
296
  score = doc.root.elements["comparison/result/score"].text.to_f
184
- rating = nil
297
+ artists = doc.root.get_elements("comparison/result/artists/artist").map { |e| e.elements["name"].text}
185
298
  case
186
299
  when score >= 0.9
187
300
  rating = _("Super")
@@ -196,19 +309,26 @@ class LastFmPlugin < Plugin
196
309
  else
197
310
  rating = _("Very Low")
198
311
  end
199
- m.reply _("%{a}'s and %{b}'s musical compatibility rating is: %{r}") % {:a => user1, :b => user2, :r => rating}
312
+
313
+ common_artists = unless artists.empty?
314
+ _(" and music they have in common includes: %{artists}") % {
315
+ :artists => Utils.comma_list(artists) }
316
+ else
317
+ nil
318
+ end
319
+
320
+ m.reply _("%{a}'s and %{b}'s musical compatibility rating is %{bold}%{r}%{bold}%{common}") % {
321
+ :a => user1,
322
+ :b => user2,
323
+ :r => rating.downcase,
324
+ :bold => Bold,
325
+ :common => common_artists
326
+ }
200
327
  end
201
328
 
202
329
  def now_playing(m, params)
203
330
  opts = { :cache => false }
204
- user = nil
205
- if params[:who]
206
- user = params[:who].to_s
207
- elsif @registry.has_key? m.sourcenick
208
- user = @registry[ m.sourcenick ]
209
- else
210
- user = m.sourcenick
211
- end
331
+ user = resolve_username(m, params[:who])
212
332
  xml = @bot.httputil.get_response("#{APIURL}method=user.getrecenttracks&user=#{CGI.escape user}", opts)
213
333
  doc = Document.new xml.body
214
334
  unless doc
@@ -216,17 +336,13 @@ class LastFmPlugin < Plugin
216
336
  return
217
337
  end
218
338
  if xml.class == Net::HTTPBadRequest
219
- if doc.root.elements["error"].text == "Invalid user name supplied" then
220
- if @registry.has_key? user and not params[:recurs]
221
- params[:who] = @registry[ user ]
222
- params[:recurs] = true
223
- now_playing(m, params)
224
- else
225
- m.reply "#{user} doesn't exist at last.fm. Perhaps you need to: lastfm set <username>"
226
- return
227
- end
339
+ if doc.root.elements["error"].attributes["code"] == "6" then
340
+ m.reply _("%{user} doesn't exist on last.fm, perhaps they need to: lastfm user <username>") % {
341
+ :user => user
342
+ }
343
+ return
228
344
  else
229
- m.reply _("Error %{e}") % {:e => doc.root.element["error"].text}
345
+ m.reply _("error: %{e}") % {:e => doc.root.element["error"].text}
230
346
  return
231
347
  end
232
348
  end
@@ -240,46 +356,81 @@ class LastFmPlugin < Plugin
240
356
  artist = first.elements["artist"].text
241
357
  track = first.elements["name"].text
242
358
  albumtxt = first.elements["album"].text
243
- album = ""
244
- if albumtxt
359
+ album = if albumtxt
245
360
  year = get_album(artist, albumtxt)[2]
246
- album = "[#{albumtxt}, #{year}] " if year
361
+ if year
362
+ _(" [%{albumtext}, %{year}]") % { :albumtext => albumtxt, :year => year }
363
+ else
364
+ _(" [%{albumtext}]") % { :albumtext => albumtxt }
365
+ end
366
+ else
367
+ nil
368
+ end
369
+ past = nil
370
+ date = XPath.first(first, "//date")
371
+ if date != nil
372
+ time = date.attributes["uts"]
373
+ past = Time.at(time.to_i)
247
374
  end
248
- date = first.elements["date"].attributes["uts"]
249
- past = Time.at(date.to_i)
250
375
  if now == "true"
251
- verb = _("listening")
376
+ verb = _("is listening to")
252
377
  if @registry.has_key? "#{m.sourcenick}_verb_present"
253
378
  verb = @registry["#{m.sourcenick}_verb_present"]
254
379
  end
255
- m.reply _("%{u} is %{v} to \"%{t}\" by %{a} %{b}") % {:u => user, :v => verb, :t => track, :a => artist, :b => album}
380
+ reply = _("%{u} %{v} \"%{t}\" by %{bold}%{a}%{bold}%{b}") % {:u => user, :v => verb, :t => track, :a => artist, :b => album, :bold => Bold}
256
381
  else
257
- verb = _("listened")
382
+ verb = _("listened to")
258
383
  if @registry.has_key? "#{m.sourcenick}_verb_past"
259
384
  verb = @registry["#{m.sourcenick}_verb_past"]
260
385
  end
261
386
  ago = Utils.timeago(past)
262
- m.reply _("%{u} %{v} to \"%{t}\" by %{a} %{b}%{p}") % {:u => user, :v => verb, :t => track, :a => artist, :b => album, :p => ago}
387
+ reply = _("%{u} %{v} \"%{t}\" by %{bold}%{a}%{bold}%{b} %{p};") % {:u => user, :v => verb, :t => track, :a => artist, :b => album, :p => ago, :bold => Bold}
263
388
  end
389
+
390
+ if Object.const_defined?('Spotify')
391
+ if track = Spotify.search(:track, "#{artist} #{track}")
392
+ reply << _(" [%{u}%{url}%{u}]") % {:u => Underline, :url => track.url}
393
+ end
394
+ end
395
+
396
+ reply << _(" -- see %{uri} for more") % { :uri => "http://www.last.fm/user/#{CGI.escape user}"}
397
+ m.reply reply
264
398
  end
265
399
 
266
400
  def find_artist(m, params)
267
- xml = @bot.httputil.get("#{APIURL}method=artist.getinfo&artist=#{CGI.escape params[:artist].to_s}")
268
- unless xml
269
- m.reply _("I had problems getting info for %{a}.") % {:a => params[:artist]}
401
+ info_xml = @bot.httputil.get("#{APIURL}method=artist.getinfo&artist=#{CGI.escape params[:artist].to_s}")
402
+ unless info_xml
403
+ m.reply _("I had problems getting info for %{a}") % {:a => params[:artist]}
270
404
  return
271
405
  end
272
- doc = Document.new xml
273
- unless doc
406
+ info_doc = Document.new info_xml
407
+ unless info_doc
274
408
  m.reply _("last.fm parsing failed")
275
409
  return
276
410
  end
277
- first = doc.root.elements["artist"]
411
+ tags_xml = @bot.httputil.get("#{APIURL}method=artist.gettoptags&artist=#{CGI.escape params[:artist].to_s}")
412
+ tags_doc = Document.new tags_xml
413
+
414
+ first = info_doc.root.elements["artist"]
278
415
  artist = first.elements["name"].text
279
- playcount = first.elements["stats"].elements["plays"].text
280
- listeners = first.elements["stats"].elements["listeners"].text
416
+ url = first.elements["url"].text
417
+ stats = {}
418
+ %w(playcount listeners).each do |e|
419
+ t = first.elements["stats/#{e}"].text
420
+ stats[e.to_sym] = t.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1,")
421
+ end
281
422
  summary = first.elements["bio"].elements["summary"].text
282
- m.reply _("%{b}%{a}%{b} has been played %{c} times and is being listened to by %{l} people.") % {:b => Bold, :a => artist, :c => playcount, :l => listeners}
423
+ similar = first.get_elements("similar/artist").map { |a|
424
+ _("%{b}%{a}%{b}") % { :a => a.elements["name"].text, :b => Bold } }
425
+ tags = tags_doc.root.get_elements("toptags/tag")[0..4].map { |t|
426
+ _("%{u}%{t}%{u}") % { :t => t.elements["name"].text, :u => Underline } }
427
+ reply = _("%{b}%{a}%{b} <%{u}> has been played %{b}%{c}%{b} times and is being listened to by %{b}%{l}%{b} people") % {
428
+ :b => Bold, :a => artist, :u => url, :c => stats[:playcount], :l => stats[:listeners] }
429
+ reply << _(". Tagged as: %{t}") % {
430
+ :t => tags.join(", "), :b => Bold } unless tags.empty?
431
+ reply << _(". Similar artists: %{s}") % {
432
+ :s => similar.join(", "), :b => Bold } unless similar.empty?
433
+ m.reply reply
283
434
  m.reply summary.ircify_html
284
435
  end
285
436
 
@@ -287,7 +438,7 @@ class LastFmPlugin < Plugin
287
438
  track = params[:track].to_s
288
439
  xml = @bot.httputil.get("#{APIURL}method=track.search&track=#{CGI.escape track}")
289
440
  unless xml
290
- m.reply _("I had problems getting info for %{a}.") % {:a => track}
441
+ m.reply _("I had problems getting info for %{a}") % {:a => track}
291
442
  return
292
443
  end
293
444
  debug xml
@@ -319,6 +470,44 @@ class LastFmPlugin < Plugin
319
470
  end
320
471
  end
321
472
 
473
+ def find_venue(m, params)
474
+ venue = params[:venue].to_s
475
+ venues = search_venue_by(:name => venue, :limit => 1)
476
+ venue = venues.last
477
+
478
+ if venues.empty?
479
+ m.reply "sorry, can't find such venue"
480
+ return
481
+ end
482
+
483
+ reply = _("%{b}%{name}%{b}, %{street}, %{u}%{city}%{u}, %{country}, see %{url} for more info") % {
484
+ :u => Underline, :b => Bold, :name => venue.name, :city => venue.city, :street => venue.street,
485
+ :country => venue.country, :url => venue.url
486
+ }
487
+
488
+ if venue.street && venue.city
489
+ maps_uri = "http://maps.google.com/maps?q=#{venue.street},+#{venue.city}"
490
+ maps_uri << ",+#{venue.postal}" if venue.postal
491
+ elsif venue.lat && venue.long
492
+ maps_uri = "http://maps.google.com/maps?q=#{venue.lat},+#{venue.long}"
493
+ else
494
+ m.reply reply
495
+ return
496
+ end
497
+
498
+ maps_uri << "+(#{venue.name.gsub(" ", "%A0")})"
499
+
500
+ begin
501
+ require "shorturl"
502
+ maps_uri = ShortURL.shorten(CGI.escape(maps_uri))
503
+ rescue LoadError => e
504
+ error e
505
+ end
506
+
507
+ reply << _(" and %{maps} for maps") % { :maps => maps_uri, :b => Bold }
508
+ m.reply reply
509
+ end
510
+
322
511
  def get_album(artist, album)
323
512
  xml = @bot.httputil.get("#{APIURL}method=album.getinfo&artist=#{CGI.escape artist}&album=#{CGI.escape album}")
324
513
  unless xml
@@ -334,7 +523,7 @@ class LastFmPlugin < Plugin
334
523
  playcount = first.elements["playcount"].text
335
524
  album = first.elements["name"].text
336
525
  date = first.elements["releasedate"].text
337
- unless date.strip.length < 2
526
+ unless date.strip.length < 2
338
527
  year = date.strip.split[2].chop
339
528
  end
340
529
  result = [artist, album, year, playcount]
@@ -348,14 +537,14 @@ class LastFmPlugin < Plugin
348
537
  return
349
538
  end
350
539
  year = "(#{album[2]}) " unless album[2] == nil
351
- m.reply _("The album \"%{a}\" by %{r} %{y}has been played %{c} times.") % {:a => album[1], :r => album[0], :y => year, :c => album[3]}
540
+ m.reply _("the album \"%{a}\" by %{r} %{y}has been played %{c} times") % {:a => album[1], :r => album[0], :y => year, :c => album[3]}
352
541
  end
353
542
 
354
543
  def set_user(m, params)
355
544
  user = params[:who].to_s
356
545
  nick = m.sourcenick
357
546
  @registry[ nick ] = user
358
- m.reply _("Ok, I'll remember that %{n} is %{u} at last.fm") % {:n => nick, :u => user}
547
+ m.reply _("okay, I'll remember that %{n} is %{u} on last.fm") % {:n => nick, :u => user}
359
548
  end
360
549
 
361
550
  def set_verb(m, params)
@@ -364,71 +553,245 @@ class LastFmPlugin < Plugin
364
553
  key = "#{m.sourcenick}_verb_"
365
554
  @registry[ "#{key}past" ] = past
366
555
  @registry[ "#{key}present" ] = present
367
- m.reply _("Ok, I'll remember that %{n} prefers %{r} and %{p}.") % {:n => m.sourcenick, :p => past, :r => present}
556
+ m.reply _("okay, I'll remember that %{n} prefers \"%{r}\" and \"%{p}\"") % {:n => m.sourcenick, :p => past, :r => present}
368
557
  end
369
558
 
370
559
  def get_user(m, params)
371
560
  nick = ""
372
561
  if params[:who]
373
562
  nick = params[:who].to_s
374
- else
563
+ else
375
564
  nick = m.sourcenick
376
565
  end
377
566
  if @registry.has_key? nick
378
567
  user = @registry[ nick ]
379
- m.reply "#{nick} is #{user} at last.fm"
568
+ m.reply _("%{nick} is %{user} on last.fm") % {
569
+ :nick => nick,
570
+ :user => user
571
+ }
380
572
  else
381
- m.reply _("Sorry, I don't know who %{n} is at last.fm perhaps you need to: lastfm set <username>") % {:n => nick}
573
+ m.reply _("sorry, I don't know who %{n} is on last.fm, perhaps they need to: lastfm set user <username>") % {:n => nick}
382
574
  end
383
575
  end
384
576
 
385
- # TODO this user data retrieval should be upgraded to API 2.0 but it would need separate parsing
386
- # for each dataset, or almost
387
577
  def lastfm(m, params)
388
- action = params[:action].intern
389
- action = :neighbours if action == :neighbors
390
- action = :recenttracks if action == :recentracks
391
- action = :topalbums if action == :topalbum
392
- action = :topartists if action == :topartist
393
- action = :toptags if action == :toptag
394
- user = nil
395
- if params[:user] then
396
- user = params[:user].to_s
397
- elsif @registry.has_key? m.sourcenick
398
- user = @registry[ m.sourcenick ]
578
+ action = case params[:action]
579
+ when "neighbors" then "neighbours"
580
+ when "recentracks", "recent" then "recenttracks"
581
+ when "loved" then "lovedtracks"
582
+ when /^weekly(track|album|artist)s$/
583
+ "weekly#{$1}chart"
584
+ when "events"
585
+ find_events(m, params)
586
+ return
587
+ else
588
+ params[:action]
589
+ end.to_sym
590
+
591
+ if action == :shouts
592
+ num = params[:num] || @bot.config['lastfm.default_shouts']
593
+ num = num.to_i.clip(1, @bot.config['lastfm.max_shouts'])
399
594
  else
400
- # m.reply "I don't know who you are on last.fm. Use 'lastfm set username' to identify yourself."
401
- # return
402
- user = m.sourcenick
595
+ num = params[:num] || @bot.config['lastfm.default_user_data']
596
+ num = num.to_i.clip(1, @bot.config['lastfm.max_user_data'])
403
597
  end
598
+
599
+ user = resolve_username(m, params[:user])
600
+ uri = "#{APIURL}method=user.get#{action}&user=#{CGI.escape user}"
601
+
602
+ if period = params[:period]
603
+ period_uri = (period.last == "year" ? "12month" : period.first + "month")
604
+ uri << "&period=#{period_uri}"
605
+ end
606
+
404
607
  begin
405
- data = @bot.httputil.get("http://ws.audioscrobbler.com/1.0/user/#{user}/#{action}.txt")
406
- m.reply "#{action} for #{user}:"
407
- m.reply data.to_a[0..3].map{|l| l.split(',',2)[-1].chomp}.join(", ")
408
- rescue
409
- m.reply "could not find #{action} for #{user} (is #{user} a user?). perhaps you need to: lastfm set <username>"
608
+ res = @bot.httputil.get_response(uri)
609
+ raise _("no response body") unless res.body
610
+ rescue Exception => e
611
+ m.reply _("I had problems accessing last.fm: %{e}") % {:e => e.message}
612
+ return
613
+ end
614
+ doc = Document.new(res.body)
615
+ unless doc
616
+ m.reply _("last.fm parsing failed")
617
+ return
410
618
  end
619
+
620
+ case res
621
+ when Net::HTTPBadRequest
622
+ if doc.root and doc.root.attributes["status"] == "failed"
623
+ m.reply "error: " << doc.root.elements["error"].text.downcase
624
+ end
625
+ return
626
+ end
627
+
628
+ seemore = _("; see %{uri} for more")
629
+ case action
630
+ when :friends
631
+ friends = doc.root.get_elements("friends/user").map do |u|
632
+ u.elements["name"].text
633
+ end
634
+
635
+ if friends.empty?
636
+ reply = _("%{user} has no friends :(")
637
+ elsif friends.length <= num
638
+ reply = _("%{user} has %{total} friends: %{friends}")
639
+ else
640
+ reply = _("%{user} has %{total} friends, including %{friends}%{seemore}")
641
+ end
642
+ m.reply reply % {
643
+ :user => user,
644
+ :total => friends.size,
645
+ :friends => Utils.comma_list(friends.shuffle[0, num]),
646
+ :seemore => seemore % { :uri => "http://www.last.fm/user/#{CGI.escape user}/friends" }
647
+ }
648
+ when :lovedtracks
649
+ loved = doc.root.get_elements("lovedtracks/track").map do |track|
650
+ [track.elements["artist/name"].text, track.elements["name"].text].join(" - ")
651
+ end
652
+ loved_prep = loved.shuffle[0, num].to_enum(:each_with_index).collect { |e,i| (i % 2).zero? ? Underline+e+Underline : e }
653
+
654
+ if loved.empty?
655
+ reply = _("%{user} has not loved any tracks")
656
+ elsif loved.length <= num
657
+ reply = _("%{user} has loved %{total} tracks: %{tracks}")
658
+ else
659
+ reply = _("%{user} has loved %{total} tracks, including %{tracks}%{seemore}")
660
+ end
661
+
662
+ m.reply reply % {
663
+ :user => user,
664
+ :total => loved.size,
665
+ :tracks => Utils.comma_list(loved_prep),
666
+ :seemore => seemore % { :uri => "http://www.last.fm/user/#{CGI.escape user}/library/loved" }
667
+ }
668
+ when :neighbours
669
+ nbrs = doc.root.get_elements("neighbours/user").map do |u|
670
+ u.elements["name"].text
671
+ end
672
+
673
+ if nbrs.empty?
674
+ reply = _("no one seems to share %{user}'s musical taste")
675
+ elsif nbrs.length <= num
676
+ reply = _("%{user}'s musical neighbours are %{nbrs}")
677
+ else
678
+ reply = _("%{user}'s musical neighbours include %{nbrs}%{seemore}")
679
+ end
680
+ m.reply reply % {
681
+ :user => user,
682
+ :nbrs => Utils.comma_list(nbrs.shuffle[0, num]),
683
+ :seemore => seemore % { :uri => "http://www.last.fm/user/#{CGI.escape user}/neighbours" }
684
+ }
685
+ when :recenttracks
686
+ tracks = doc.root.get_elements("recenttracks/track").map do |track|
687
+ [track.elements["artist"].text, track.elements["name"].text].join(" - ")
688
+ end
689
+
690
+ counts = []
691
+ tracks.each do |track|
692
+ if t = counts.assoc(track)
693
+ counts[counts.rindex(t)] = [track, t[-1] += 1]
694
+ else
695
+ counts << [track, 1]
696
+ end
697
+ end
698
+
699
+ tracks_prep = counts[0, num].to_enum(:each_with_index).map do |e,i|
700
+ str = (i % 2).zero? ? Underline+e[0]+Underline : e[0]
701
+ str << " (%{i} times%{m})" % {
702
+ :i => e.last,
703
+ :m => counts.size == 1 ? _(" or more") : nil
704
+ } if e.last > 1
705
+ str
706
+ end
707
+
708
+ if tracks.empty?
709
+ m.reply _("%{user} hasn't played anything recently") % { :user => user }
710
+ else
711
+ m.reply _("%{user} has recently played %{tracks}") %
712
+ { :user => user, :tracks => Utils.comma_list(tracks_prep) }
713
+ end
714
+ when :shouts
715
+ shouts = doc.root.get_elements("shouts/shout")
716
+ if shouts.empty?
717
+ m.reply _("there are no shouts for %{user}") % { :user => user }
718
+ else
719
+ shouts[0, num].each do |shout|
720
+ m.reply _("<%{author}> %{body}") % {
721
+ :body => shout.elements["body"].text,
722
+ :author => shout.elements["author"].text,
723
+ }
724
+ end
725
+ end
726
+ when :toptracks, :topalbums, :topartists, :weeklytrackchart, :weeklyalbumchart, :weeklyartistchart
727
+ type = action.to_s.scan(/track|album|artist/).to_s
728
+ items = doc.root.get_elements("#{action}/#{type}").map do |item|
729
+ case action
730
+ when :weeklytrackchart, :weeklyalbumchart
731
+ format = "%{artist} - %{title} (%{bold}%{plays}%{bold})"
732
+ artist = item.elements["artist"].text
733
+ when :weeklyartistchart, :topartists
734
+ format = "%{artist} (%{bold}%{plays}%{bold})"
735
+ artist = item.elements["name"].text
736
+ when :toptracks, :topalbums
737
+ format = "%{artist} - %{title} (%{bold}%{plays}%{bold})"
738
+ artist = item.elements["artist/name"].text
739
+ end
740
+
741
+ _(format) % {
742
+ :artist => artist,
743
+ :title => item.elements["name"].text,
744
+ :plays => item.elements["playcount"].text,
745
+ :bold => Bold
746
+ }
747
+ end
748
+ if items.empty?
749
+ m.reply _("%{user} hasn't played anything in this period of time") % { :user => user }
750
+ else
751
+ m.reply items[0, num].join(", ")
752
+ end
753
+ end
754
+ end
755
+
756
+ def resolve_username(m, name)
757
+ name = m.sourcenick if name.nil?
758
+ @registry[name] or name
411
759
  end
412
760
  end
413
761
 
762
+ event_map_options = {
763
+ :action => :find_events,
764
+ :requirements => {
765
+ :num => /\d+/,
766
+ :sort_order => /(?:asc|desc)(?:ending)?/
767
+ },
768
+ :thread => true
769
+ }
770
+
414
771
  plugin = LastFmPlugin.new
415
- plugin.map 'lastfm [:num] event[s] in *location', :action => :find_events, :requirements => { :num => /\d+/ }, :thread => true
416
- plugin.map 'lastfm [:num] event[s] by *who', :action => :find_events, :requirements => { :num => /\d+/ }, :thread => true
417
- plugin.map 'lastfm [:num] event[s] [for] *who', :action => :find_events, :requirements => { :num => /\d+/ }, :thread => true
418
- plugin.map 'lastfm now :who', :action => :now_playing, :thread => true
419
- plugin.map 'lastfm now', :action => :now_playing, :thread => true
420
- plugin.map 'np :who', :action => :now_playing, :thread => true
772
+ plugin.map 'lastfm [:num] event[s] in *location [sort[ed] by :sort_by] [[in] :sort_order [order]]', event_map_options.dup
773
+ plugin.map 'lastfm [:num] event[s] by *who [sort[ed] by :sort_by] [[in] :sort_order [order]]', event_map_options.dup
774
+ plugin.map 'lastfm [:num] event[s] at *venue [sort[ed] by :sort_by] [[in] :sort_order [order]]', event_map_options.dup
775
+ plugin.map 'lastfm [:num] event[s] [for] *who [sort[ed] by :sort_by] [[in] :sort_order [order]]', event_map_options.dup
421
776
  plugin.map 'lastfm artist *artist', :action => :find_artist, :thread => true
422
777
  plugin.map 'lastfm album *album [by *artist]', :action => :find_album
423
778
  plugin.map 'lastfm track *track', :action => :find_track, :thread => true
424
- plugin.map 'lastfm set nick :who', :action => :set_user, :thread => true
425
- plugin.map 'lastfm set verb :present :past', :action => :set_verb, :thread => true
426
- plugin.map 'lastfm who :who', :action => :get_user, :thread => true
427
- plugin.map 'lastfm who', :action => :get_user, :thread => true
428
- plugin.map 'lastfm compare :user1 :user2', :action => :tasteometer, :thread => true
429
- plugin.map 'np', :action => :now_playing, :thread => true
430
- plugin.map "lastfm [user] :action [:user]", :thread => true,
431
- :requirements => { :action =>
432
- /^(?:events|friends|neighbou?rs|playlists|recent?tracks|top(?:album|artist|tag)s?|weekly(?:album|artist|track)chart|weeklychartlist)$/
779
+ plugin.map 'lastfm venue *venue', :action => :find_venue, :thread => true
780
+ plugin.map 'lastfm set user[name] :who', :action => :set_user, :thread => true
781
+ plugin.map 'lastfm set verb *present, *past', :action => :set_verb, :thread => true
782
+ plugin.map 'lastfm who [:who]', :action => :get_user, :thread => true
783
+ plugin.map 'lastfm compare to :user2', :action => :tasteometer, :thread => true
784
+ plugin.map 'lastfm compare [:user1] [to] :user2', :action => :tasteometer, :thread => true
785
+ plugin.map "lastfm [user] [:num] :action [:user]", :thread => true,
786
+ :requirements => {
787
+ :action => /^(?:events|shouts|friends|neighbou?rs|loved(?:tracks)?|recent(?:t?racks)?|top(?:album|artist|track)s?|weekly(?:albums?|artists?|tracks?)(?:chart)?)$/,
788
+ :num => /^\d+$/
789
+ }
790
+ plugin.map 'lastfm [user] [:num] :action [:user] over [*period]', :thread => true,
791
+ :requirements => {
792
+ :action => /^(?:top(?:album|artist|track)s?)$/,
793
+ :period => /^(?:(?:3|6|12) months)|(?:a\s|1\s)?year$/,
794
+ :num => /^\d+$/
433
795
  }
434
- plugin.map "lastfm [:who]", :action => :now_playing, :thread => true
796
+ plugin.map 'lastfm [now] [:who]', :action => :now_playing, :thread => true
797
+ plugin.map 'np [:who]', :action => :now_playing, :thread => true